Redis分布式锁剖析和几种客户端的实现

news2025/7/13 8:12:27

1. 背景

 在传统的单体项目中,即部署到单个IIS上,针对并发问题,比如进销存中的出库和入库问题,多个人同时操作,属于一个IIS进程中多个线程并发操作的问题,这个时候可以引入线程锁lock/Monitor等,轻松解决这类问题。但是随着业务量的逐渐增大,比如"秒杀业务", 肯定是集群部署,这个时候线程锁已经没用了, 必须引入分布式锁。

 常见的分布式锁有:数据库、zookeeper、redis. 本节重点介绍redis的分布式锁.

如下图:

2. 分布式锁需要满足的条件

 (1).在分布式系统环境下,一个锁在同一时间只能被一个服务器获取;(这是所有分布式锁的基础)

 (2).高性能的获取锁和释放锁;(锁用完了,要及时释放,以供别人继续使用)

 (3).具备锁失效机制,防止死锁;(防止因为某些意外,锁没有得到释放,那别人将永远无法使用)

 (4).具备非阻塞锁特性,即没有获取到锁将直接返回获取锁失败。(满足等待锁的同时,也要满足非阻塞锁特性,便于多样性的业务场景使用)

3. 分布式锁种类/原理

(1).阻塞锁

  尝试在redis中创建一个字符串结构缓存,方法传入key和过期时间(AcquireLock), 其中key对应的value为锁的过期时间timeout的时间戳

  若redis中没有这个key,则创建成功(即抢到锁),然后立即返回;若已经有这个key,则先watch,然后校验value中的时间戳是否已经超过当前时间

  若已超过,则尝试使用提交事务的方式覆盖新的时间戳,事务提交成功(即抢到锁),然后立即返回;若未超过当前时间或事务提交失败(即被别人抢到锁),则进入一个内部优化过的微循环,不断重试。

 传入的timeout还有一个作用,就是控制重试时间,重试超时后则抛异常,using完成方法调用或者显式调用dispose,都会直接清除key。

总结:

  timeout有两个意思:一是如果成功加锁后锁的过期时间, 二是未成功加锁后阻塞等待的时间。数据锁服务通过检查value中时间戳来判断是否过期,并不是利用redis在key上设置expire time来通过key的过期实现的

(2).非阻塞锁

  尝试在redis中创建一个字符串结构缓存项,方法传入key、value、timeout(Add),其中value无实际意义,过期时间为传入的timeout。

  若redis中没有这个key,则创建成功(即抢到锁),然后立即返回true.若已经有这个key,则立即返回false。以上过程为全局单线程原子操作,整个过程为独占式操作。IsLock可以检测key是否存在。

注意:

  timeout即成功加锁后锁的过期时间,利用redis在key上设置expire time来通过key的过期实现。不要先用IsLock判断是否有锁再用Add加锁,因为这两个操作非原子性操作,期间会被其他操作干扰。

(3).底层实现主要用到以下几个指令

A.setnx

 setnx key val:当且仅当key不存在时,set一个key为val的字符串,返回1;若key存在,则什么都不做,返回0

B.expire

 expire key timeout:为key设置一个超时时间,单位为second,超过这个时间锁会自动释放,避免死锁

C.delete

 delete key:删除key

二. 案例模拟实现

1.场景模拟分析

 模拟多个用户进行秒杀业务,扣减库存→创建订单。 (PS:这里只是为了演示分布式锁而已,实际场景可以利用redis自减Api原子性实现扣减库存,从而干掉锁的问题)

总结:真正的秒杀是不会用分布式锁的, 因为用锁会存在等待的问题,会产生大量无响应的情况, 实际情况下可以利用Lua脚本结合redis原子性的特点,编写秒杀业务。详见:https://www.cnblogs.com/yaopengfei/p/13749772.html

下面分享3个不同的程序集实现分布式锁的业务.

C/C++Linux服务器开发高级架构师/C++后台开发架构师​免费学习地址

【文章福利】另外还整理一些C++后台开发架构师 相关学习资料,面试题,教学视频,以及学习路线图,免费分享有需要的可以点击领取

2. ServiceStack.Redis

(1).阻塞锁

代码分享

   /// <summary>
    /// 阻塞锁
    /// </summary>
    public class BlockingLock
    {

        public static void Show(int i, string key, TimeSpan timeout)
        {
            using var client = new RedisClient("119.45.174.249", 6379, "123456");
            using (var myLock = client.AcquireLock(key, timeout))  //获取锁  (此处阻塞,其它线程等待)
            {
                var goodNum = client.Get<int>("goodNum");
                if (goodNum > 0)
                {
                    client.Set<int>("goodNum", goodNum - 1);  //扣减库存
                    var orderNum = client.Incr("orderNum");
                    Console.WriteLine($"{i}抢购成功,此时的库存为{goodNum - 1},订单数量为:{orderNum}");

                }
                else
                {
                    Console.WriteLine($"商品已经卖光了");
                }
            }
        }
    }

调用

 static void Main(string[] args)
 {

            Console.WriteLine("请输入开始抢购的时间:");
            int minute = int.Parse(Console.ReadLine());
            using var client = new RedisClient("119.45.174.249", 6379, "123456");
            //商品数量(设置为10)
            client.Set<int>("goodNum", 10);
            //订单数(默认为0)
            client.Set<int>("orderNum", 0);

            //开启30个线程去抢购
            Console.WriteLine($"在{minute}分0秒正式开启秒杀!");
            var flag = true;
            while (flag)
            {
                if (DateTime.Now.Minute == minute)
                //if (true)
                {
                    flag = false;
                    Parallel.For(0, 30, (i) =>
                    {
                        int temp = i;
                        Task.Run(() =>
                        {
                            BlockingLock.Show(i, "akey", TimeSpan.FromSeconds(100));   //阻塞锁

                            //NoBlockingLock.Show(i, "akey", TimeSpan.FromSeconds(100));   //非阻塞锁
                        });
                    });
                }
            }
            Console.ReadKey();
  }

copy两套程序同时运行

(2).非阻塞锁

代码分享:

  /// <summary>
    /// 非阻塞锁
    /// </summary>
    public class NoBlockingLock
    {
        public static void Show(int i, string key, TimeSpan timeout)
        {
            using var client = new RedisClient("119.45.174.249", 6379, "123456");
            bool isLocked = client.Add<string>(key, "xxxx", timeout);
            if (isLocked)
            {
                try
                {
                    var goodNum = client.Get<int>("goodNum");
                    if (goodNum > 0)
                    {
                        client.Set<int>("goodNum", goodNum - 1);  //扣减库存
                        var orderNum = client.Incr("orderNum");   //订单数量自增1
                        Console.WriteLine($"{i}抢购成功,此时的库存为{goodNum - 1},订单数量为:{orderNum}");
                    }
                    else
                    {
                        Console.WriteLine($"{i}商品已经卖光了");
                    }
                }
                catch (Exception ex)
                {
                    Console.WriteLine($"{i}报错了{ex.Message}");
                }
                finally
                {
                    client.Remove(key);
                }
            }
            else
            {
                Console.WriteLine($"{i}抢购失败:原因:没有拿到锁");
            }
        }
    }

调用

 static void Main(string[] args)
 {

            Console.WriteLine("请输入开始抢购的时间:");
            int minute = int.Parse(Console.ReadLine());
            using var client = new RedisClient("119.45.174.249", 6379, "123456");
            //商品数量(设置为10)
            client.Set<int>("goodNum", 10);
            //订单数(默认为0)
            client.Set<int>("orderNum", 0);

            //开启30个线程去抢购
            Console.WriteLine($"在{minute}分0秒正式开启秒杀!");
            var flag = true;
            while (flag)
            {
                if (DateTime.Now.Minute == minute)
                //if (true)
                {
                    flag = false;
                    Parallel.For(0, 30, (i) =>
                    {
                        int temp = i;
                        Task.Run(() =>
                        {
                            //BlockingLock.Show(i, "akey", TimeSpan.FromSeconds(100));   //阻塞锁

                            NoBlockingLock.Show(i, "akey", TimeSpan.FromSeconds(100));   //非阻塞锁
                        });
                    });
                }
            }
            Console.ReadKey();
  }

copy两套程序同时运行

3. StackExchange.Redis

代码分享

 public class MyLock1
    {
        public static void Show(int i, string key, TimeSpan timeout)
        {

            RedisHelp redis = new RedisHelp("119.45.174.249:6379,password=123456");
            var client = redis.GetDatabase();

            bool isLocked = client.LockTake(key, Environment.MachineName, timeout); //timeout秒后自动释放
            if (isLocked)
            {
                try
                {
                    var goodNum = int.Parse(client.StringGet("goodNum"));
                    if (goodNum > 0)
                    {
                        client.StringSet("goodNum", goodNum - 1);  //扣减库存
                        var orderNum = client.StringIncrement("orderNum");   //订单数量自增1
                        Console.WriteLine($"{i}抢购成功,此时的库存为{goodNum - 1},订单数量为:{orderNum}");
                    }
                    else
                    {
                        Console.WriteLine($"{i}商品已经卖光了");
                    }
                }
                catch (Exception ex)
                {
                    Console.WriteLine($"{i}报错了{ex.Message}");
                }
                finally
                {
                    client.LockRelease(key, Environment.MachineName);
                }
            }
            else
            {
                Console.WriteLine($"{i}抢购失败:原因:没有拿到锁");
            }
        }
    }

调用:

static void Main(string[] args)
{

            Console.WriteLine("请输入开始抢购的时间:");
            int minute = int.Parse(Console.ReadLine());
            RedisHelp redis = new RedisHelp("119.45.174.249:6379,password=123456");
            var client = redis.GetDatabase();
            //商品数量(设置为10)
            client.StringSet("goodNum", 10);
            //订单数(默认为0)
            client.StringSet("orderNum", 0);

            //开启30个线程去抢购
            Console.WriteLine($"在{minute}分0秒正式开启秒杀!");
            var flag = true;
            while (flag)
            {
                if (DateTime.Now.Minute == minute)
                //if (true)
                {
                    flag = false;
                    Parallel.For(0, 30, (i) =>
                    {
                        int temp = i;
                        Task.Run(() =>
                        {
                            MyLock1.Show(i, "akey", TimeSpan.FromSeconds(2));
                        });
                    });
                }
            }
            Console.ReadKey();
}

4. CSRedisCore

代码分享

public class MyLock1
    {
        public static void Show(int i, string key, int timeout)
        {

            RedisHelper.Initialization(new CSRedis.CSRedisClient("119.45.174.249:6379,password=123456,defaultDatabase=0"));
            var isLocked = RedisHelper.Lock(key, timeout, true); //timeout秒后自动释放
            if (isLocked != null)  //获取超时则返回null
            {
                try
                {
                    var goodNum = int.Parse(RedisHelper.Get("goodNum"));
                    if (goodNum > 0)
                    {
                        RedisHelper.Set("goodNum", goodNum - 1);  //扣减库存
                        var orderNum = RedisHelper.IncrBy("orderNum");   //订单数量自增1
                        Console.WriteLine($"{i}抢购成功,此时的库存为{goodNum - 1},订单数量为:{orderNum}");
                    }
                    else
                    {
                        Console.WriteLine($"商品已经卖光了");
                    }
                }
                catch (Exception ex)
                {
                    Console.WriteLine($"报错了{ex.Message}");
                }
                finally
                {
                    RedisHelper.Del(key);  //上面可以自动删除,还需要手动删除吗?
                }
            }
            else
            {
                Console.WriteLine($"{i}抢购失败:原因:没有拿到锁");
            }
        }
    }

调用

  static void Main(string[] args)
  {

            Console.WriteLine("请输入开始抢购的时间:");
            int minute = int.Parse(Console.ReadLine());
            var client = new CSRedis.CSRedisClient("119.45.174.249:6379,password=123456,defaultDatabase=0");
            //商品数量(设置为10)
            client.Set("goodNum", 10);
            //订单数(默认为0)
            client.Set("orderNum", 0);

            //开启30个线程去抢购
            Console.WriteLine($"在{minute}分0秒正式开启秒杀!");
            var flag = true;
            while (flag)
            {
                if (DateTime.Now.Minute == minute)
                //if (true)
                {
                    flag = false;
                    Parallel.For(0, 30, (i) =>
                    {
                        int temp = i;
                        Task.Run(() =>
                        {

                            MyLock1.Show(i, "akey", 2);
                        });
                    });
                }
            }
            Console.ReadKey();
     }

原文链接:第十五节:Redis分布式锁剖析和几种客户端的实现 - Yaopengfei - 博客园

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/17808.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

信息论随笔(三)交互信息量

之前讨论了一个事件的自信息量&#xff0c;但是实际情况下往往有多个事件发生&#xff0c;而且这些事件之间相互是有联系的。比如知道一个人踢足球&#xff0c;那么这个人很有可能会看世界杯。也就是说&#xff0c;我们可以通过一个事件获得另外一个事件的信息&#xff0c;或者…

解决Android Studio等开发软件出现更新TKK失败的两种方案

解决Android Studio等开发软件出现更新TKK失败的两种方案方案一 配置hosts1. 配置域名与IP2.扫描国内可用的IP方案二 替换翻译引擎百度翻译引擎在Android Studio等开发软件中利用Translation等翻译插件时&#xff0c;出现无法翻译的提示&#xff1a;更新TKK失败&#xff0c;请检…

数据结构之栈的实现及相关OJ题

&#x1f57a;作者启明星使 &#x1f383;专栏&#xff1a;《数据库》《C语言》 &#x1f3c7;分享一句话&#xff1a; 对的人会站在你的前途里 志同道合的人才看得懂同一片风景 大家一起加油&#x1f3c4;‍♂️&#x1f3c4;‍♂️&#x1f3c4;‍♂️ 希望得到大家的支持&am…

【毕业设计】新闻分类系统 - 深度学习 机器学习

文章目录0 前言1 简介2 参与及比较算法3 先说结论4 实现过程4.1 数据爬取4.2 数据预处理5 CNN文本分类6 最后0 前言 &#x1f525; Hi&#xff0c;大家好&#xff0c;这里是丹成学长的毕设系列文章&#xff01; &#x1f525; 对毕设有任何疑问都可以问学长哦! 这两年开始&a…

事件总线EventBus

事件总线是对发布-订阅模式的一种实现&#xff0c;是一种集中式事件处理机制&#xff0c;允许不同的组件之间进行彼此通信而又不需要相互依赖&#xff0c;达到一种解耦的目的。 什么是“总线”&#xff1a;一个集中式的事件处理机制。同时服务多个事件和多个观察者。相当于一个…

C#编程深入研究变量,类型和方法

编写正确的C#代码 简单的调试技术 变量的语法 声明类型和值 仅声明类型 访问修饰符 使用类型 通用内置类型 类型转换 推断式声明 自定义类型 类型综述 命名变量 变量的作用域 运算符 定义方法 指定参数 指定返回值 常见的Unity方法 Start方法 Update方法 …

金山云:基于 JuiceFS 的 Elasticsearch 温冷热数据管理实践

01 Elasticsearch 广泛使用带来的成本问题 Elasticsearch&#xff08;下文简称“ES”&#xff09;是一个分布式的搜索引擎&#xff0c;还可作为分布式数据库来使用&#xff0c;常用于日志处理、分析和搜索等场景&#xff1b;在运维排障层面&#xff0c;ES 组成的 ELK&#xff…

MMDetection3D库中的一些模块介绍

本文目前仅包含2个体素编码器、2个中间编码器、1个主干网络、1个颈部网络和1个检测头。如果有机会&#xff0c;会继续补充更多模型。 若发现内容有误&#xff0c;欢迎指出。 MMDetection3D的点云数据一般会经历如下步骤/模块&#xff1a; #mermaid-svg-q9Wy2NQvFHfuPWKs {font-…

骨传导原理是什么,佩戴骨传导耳机的过程中对于耳道有无损害

随着新时代的到来&#xff0c;我们周围的数码产品逐渐被新产物所替代&#xff0c;以往在耳机市面上&#xff0c;普遍都是入耳式耳机&#xff0c;但长时间佩戴这种耳机的话对于我们耳道来说是有着不可逆的伤害&#xff0c;而在近几年骨传导耳机的出现&#xff0c;打破了传统耳机…

18.Redis系列之AOF方式持久化

本文学习redis7两大持久化技术之一&#xff1a;AOF&#xff08;Append Only File&#xff09;日志追加方式持久化备份与还原&#xff0c;重写以及AOF方式的优缺点 1. AOF相关配置 首先我们先简单了解下Redis7中AOF相关配置 // 开启AOF方式持久化&#xff0c;默认no appendon…

基于真实场景解读 K8s Pod 的各种异常

在 K8s 中&#xff0c;Pod 作为工作负载的运行载体&#xff0c;是最为核心的一个资源对象。Pod 具有复杂的生命周期&#xff0c;在其生命周期的每一个阶段&#xff0c;可能发生多种不同的异常情况。K8s 作为一个复杂系统&#xff0c;异常诊断往往要求强大的知识和经验储备。结合…

骚戴独家笔试---SQL笔试

SQL笔试训练 查询结果去重 两种答案 查找某个年龄段的用户信息 查找除复旦大学的用户信息 三种答案 用where过滤空值练习 三种答案 查询NULL时&#xff0c;不能使用比较运算符(或者< >)&#xff0c;需要使用IS NULL运算符或者IS NOT NULL运算符。 操作符混合运用 我这里…

力扣 792. 匹配子序列的单词数

题目 给定字符串 s 和字符串数组 words, 返回 words[i] 中是s的子序列的单词个数 。 字符串的 子序列 是从原始字符串中生成的新字符串&#xff0c;可以从中删去一些字符(可以是none)&#xff0c;而不改变其余字符的相对顺序。 例如&#xff0c; “ace” 是 “abcde” 的子序…

java spring引用外部jar包并使用

spring引用外部jar包并使用1、将jar包放到src/main/resources/lib2、编辑pom.xml文件build下面加入resources&#xff0c;不加话的打包会找不到资源3、project structure中引入该lib1、将jar包放到src/main/resources/lib 2、编辑pom.xml文件 打开pom文件&#xff0c;找到相应…

计算机网络基本知识

计算机网络基本知识 计算机网络定义&#xff1a;是一个将分散的、具有独立功能的计算机系统&#xff0c;通过通信设备与线路连接起来&#xff0c;由功能完善的软件实现资源共享和信息传递的系统。 1.1计算机网络在信息时代作用 1.2因特网概述 1.2.1网络、互联网、因特网 网…

DeepLab V1学习笔记

DeepLab V1摘要相关的工作遇到的问题和解决的方法信号下采样空间不变性(spatial insensitivity/invariance)论文的优点(贡献)网络的模型空洞卷积CRF多尺度预测模型总结实验结果Semantic Image Segmentation with Deep Convolutional Nets and Fully Connected CRFs论文地址 : D…

[附源码]java毕业设计乒乓球俱乐部管理系统

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

【光学】基于Matlab模拟干涉条纹图

✅作者简介&#xff1a;热爱科研的Matlab仿真开发者&#xff0c;修心和技术同步精进&#xff0c;matlab项目合作可私信。 &#x1f34e;个人主页&#xff1a;Matlab科研工作室 &#x1f34a;个人信条&#xff1a;格物致知。 更多Matlab仿真内容点击&#x1f447; 智能优化算法 …

亿级万物互联新时代的物联网消息中间件EMQX调研

简介 最近去某餐厅吃饭&#xff0c;进门时智能门自动打开房门同时来一句"欢迎光临"&#xff0c;然后伸手到门口的洗手台&#xff0c;水管无接触自动出水&#xff0c;端起菜盘走向台子选择自己喜欢的菜品&#xff0c;最后将菜盘放在智能结账机上&#xff0c;智能结账…

代码随想录算法训练营第三天|LeetCode 203.移除链表元素 、707.设计链表 、206.反转链表

LeetCode 203.移除链表元素 题目链接&#xff1a;203.移除链表元素 链表的定义&#xff1a; // 单链表 struct ListNode {int val; // 节点上存储的元素ListNode *next; // 指向下一个节点的指针ListNode(int x) : val(x), next(NULL) {} // 节点的构造函数 };ListNode(i…