前言

上一篇老猫和小伙伴们分享了为什么要使用分布式锁以及分布式锁的实现思路原理,目前我们主要采用第三方的组件作为分布式锁的工具。上一篇运用了Mysql中的select ...for update实现了分布式锁,但是我们说这种实现方式并不常用,因为当大并发量的时候,会给数据库带来比较大的压力。当然也有小伙伴给老猫留言说“ 在quartz的集群模式中,就是使用了基于mysql的分布式锁,select for update ”。没错,其实quartz的集群模式中,任务执行的节点个数是可预知的,而且没有那么大的量级,所以是没有问题的。但是如果像千万级别的并发秒杀场景的情况下,那么这种方案其实是不可行的。因为mysql操作是需要IO的,IO的速度比内存速度慢,因此mysql如果在那种场景下使用的话是会存在系统瓶颈的。所以本篇就和小伙伴们分享基于内存操作的比较常用的分布式锁——redis分布式锁。

手撸Redis分布式锁

实现原理

redis分布式锁实现原理其实也是比较简单的,主要是依赖于redis的 set nx命令,我们来看一下完整的设置redis的命令:“Set resource_name my_random_value NX PX 30000”。看到这串命令,了解redis的小伙伴应该都看得懂这条命令是在redis中存入一个带有过期时间的值。具体上述设值语句解释如下:

  1. resource_name:资源名称,可以根据不同的业务区分不同的锁。(其实就是对应我们上一篇myql锁中的business_code)。
  2. my_random_value:随机值,每个线程的随机值都不相同,主要用于释放锁的时候用来校验。
  3. NX:key不存在的时候设置成功,key存在则设置不成功。
  4. PX:自动失效时间,如果出现异常情况,锁可以过期实现,因此达到了自动释放。

那么为什么可以使用这个思路呢?其实很简单,主要就是利用了set nx的原子性,在多个线程并发执行时,只有一个线程可以设置成功,如果设置成功,那么就代表着获得了锁,就可以执行后续的业务。如果出现了异常,过了锁的有效期,锁会自动释放,释放锁主要采用了redis的delete命令,释放锁之前会校验当前redis存储的随机数,只有当前的随机数和存储的随机数一致的时候才允许释放。具体的redis的删除,我们可以通过lua脚本进行删除,具体Lua脚本如下:

if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end

那么我们为什么要采用这种方式释放锁呢?其实使用这种方式释放锁可以避免删除别的客户端获取成功的锁 。

如下图:

客户端A取得资源锁,但是紧接着被一个其他操作阻塞了,当客户端A运行完毕其他操作后要释放锁时,原来的锁早已超时并且被Redis自动释放,并且在这期间资源锁又被客户端B再次获取到。如果仅使用DEL命令将key删除,那么这种情况就会把客户端B的锁给删除掉。使用Lua脚本就不会存在这种情况,因为脚本仅会删除value等于客户端A的value的key(value相当于客户端的一个签名)(说明:其实这些例子在redis的官网都有介绍)。

代码实现方式

老猫对redis锁机制进行了相关的抽取,并且封装成了工具类,核心工具类代码如下:

/**
* @author kdaddy@163.com
* @date 2021/1/7 22:36
* 公众号“程序员老猫”
*/
@Service
public class RedisLockUtil { @Autowired
private RedisTemplate redisTemplate; private String value = UUID.randomUUID().toString(); public Boolean lock(String key){
RedisCallback<Boolean> redisCallback = redisConnection -> {
//表示set nx 存在key的话就不设置,不存在则设置
RedisStringCommands.SetOption setOption = RedisStringCommands.SetOption.ifAbsent();
//设置过期时间
Expiration expiration = Expiration.seconds(30);
byte[] redisKey = redisTemplate.getKeySerializer().serialize(key);
byte[] redisValue = redisTemplate.getKeySerializer().serialize(value);
Boolean result = redisConnection.set(redisKey,redisValue,expiration,setOption);
return result;
};
//获取分布式锁
Boolean lock = (Boolean)redisTemplate.execute(redisCallback);
return lock;
}
//释放分布式锁
public Boolean releaseLock(String key){
String script = "if redis.call(\"get\",KEYS[1]) == ARGV[1] then\n" +
" return redis.call(\"del\",KEYS[1])\n" +
"else\n" +
" return 0\n" +
"end";
RedisScript<Boolean> redisScript = RedisScript.of(script,Boolean.class);
List<String> keys = Arrays.asList(key); boolean result = (Boolean) redisTemplate.execute(redisScript,keys,value);
return result;
}
}

当然相关的业务代码,老猫还是使用了之前并发扣减库存的例子,在此相关的代码以及最终运行的结果也不一一进行举例。小伙伴们可以自行去老猫的github获取相关的示例源码信息,然后运行一下即可。github地址:https://github.com/maoba/kd-distribute。代码已经完成了更新。

Redisson分布式锁

介绍和使用

那么Redisson究竟为何物呢?Redisson 是架设在Redis基础上的一个Java驻内存数据网格(In-Memory Data Grid)。 充分的利用了Redis键值数据库提供的一系列优势,基于Java实用工具包中常用接口,为使用者提供了一系列具有分布式特性的常用工具类。使得原本作为协调单机多线程并发程序的工具包获得了协调分布式多机多线程并发系统的能力,大大降低了设计和研发大规模分布式系统的难度。同时结合各富特色的分布式服务,更进一步简化了分布式环境中程序相互之间的协作。 (摘自redisson官网:https://redisson.org/)

下面我们来看一下具体用redisson实现分布式锁实战,其实是相当简单的,redisson已经给我们进行了相关的封装,我们开箱即用。

/**
* @author kdaddy@163.com
* @date 2021/1/9 14:23
* @公众号“程序员老猫”
*/
public Integer createOrder() throws Exception{
log.info("进入了方法");
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379").setPassword("ktdaddy");
RedissonClient redissonClient = Redisson.create(config);
RLock rlock = redissonClient.getLock(ORDER_KEY);
rlock.lock(30, TimeUnit.SECONDS); try {
log.info("拿到了锁");
//....具体可以参考老猫的github
return order.getId();
}catch (Exception e){
e.printStackTrace();
}finally {
rlock.unlock();
}
return null;
}

原理

老猫上文中自己实现redis锁的时候用到了lua脚本,redisson实现的时候其实所有的指令都是通过lua脚本去实现的。上述为redisson的简单架构图,画的比较粗糙。老猫稍微作一下解释。上图中有个看门狗(watchdog)概念。其实这就是一个定时任务,在线程获取锁之后,它会每隔10s帮忙将key的超时时间设置为30s,这样就不会出现线程一直持有锁从而影响其他线程获取锁的问题。小伙伴们可以发现该功能其实就是set px,只是换成了定时任务去实现。当然看门狗的存在保证了出现死锁的情况下会自动释放。

以上只是针对redisson做了一个简单的应用介绍,redisson其实是相当强大的,首先说配置,老猫上述连接redis的方式其实很简单,由于搭建的是单机redis,所以就使用了单机redis的连接方式,当然redisson还支持主从、哨兵、集群等等连接方式;当然锁的种类也相当丰富,以上老猫提供的是可重入锁的流程。其实还包括公平锁、联锁、红锁、读写锁等等,另外的redisson对分布式的容器、队列等等进行了特有的封装,包括分布式的Blocking Queue、分布式Map、分布式Set、分布式List等等。redisson的强大之处老猫在此不一一枚举,有兴趣的小伙伴可以深入研究一下。

缺陷

redis锁可以比较完美地解决高并发的时候分布式系统的线程安全性的问题,但是这种锁机制也并不是完美的。在哨兵模式下,客户端对master节点加了锁,此时会异步复制给slave节点,此时如果master发生宕机,主备切换,slave变成了master。因为之前是异步复制,所以此时正好又有个线程来尝试加锁的时候,就会导致多个客户端对同一个分布式锁完成了加锁操作,这时候业务上会出现脏数据了。关于redis的相关知识,大家可以访问老猫之前的一些文章,包括redis的哨兵模式、持久化等等。

写在最后

本篇主要和小伙伴们分享了redis锁,从老猫自己实现的乞丐版的redis锁到大牛实现的redisson。相信大家也会有一定的收货。其实关于分布式锁,出了redis锁之外还有基于zookeeper的实现。后续老猫会整理并且分享给大家,敬请期待。

当然更多技术干货也欢迎大家搜索关注公众号“程序员老猫”

手撕redis分布式锁,隔壁张小帅都看懂了!的更多相关文章

  1. Redis分布式锁的一点小理解

    1.在分布式系统中,我们使用锁机制只能保证同一个JVM中一次只有一个线程访问,但是在分布式的系统中锁就不起作用了,这时候就要用到分布式锁(有多种,这里指 redis) 2.在 redis当中可以使用命 ...

  2. Redisson实现Redis分布式锁的N种姿势(转)

    Redis几种架构 Redis发展到现在,几种常见的部署架构有: 单机模式: 主从模式: 哨兵模式: 集群模式: 我们首先基于这些架构讲解Redisson普通分布式锁实现,需要注意的是,只有充分了解普 ...

  3. Redis分布式锁的实现

    前段时间,我在的项目组准备做一个类似美团外卖的拼手气红包[第X个领取的人红包最大],基本功能实现后,就要考虑这一操作在短时间内多个用户争抢同一资源的并发问题了,类似于很多应用如淘宝.京东的秒杀活动场景 ...

  4. Redis专题(3):锁的基本概念到Redis分布式锁实现

    拓展阅读:Redis闲谈(1):构建知识图谱 Redis专题(2):Redis数据结构底层探秘 近来,分布式的问题被广泛提及,比如分布式事务.分布式框架.ZooKeeper.SpringCloud等等 ...

  5. Redisson实现Redis分布式锁的底层原理

    一.写在前面 现在面试,一般都会聊聊分布式系统这块的东西.通常面试官都会从服务框架(Spring Cloud.Dubbo)聊起,一路聊到分布式事务.分布式锁.ZooKeeper等知识.所以咱们这篇文章 ...

  6. Zookeeper——基本使用以及应用场景(手写实现分布式锁和rpc框架)

    文章目录 Zookeeper的基本使用 Zookeeper单机部署 Zookeeper集群搭建 JavaAPI的使用 Zookeeper的应用场景 分布式锁的实现 独享锁 可重入锁 实现RPC框架 基 ...

  7. 面试必问:如何实现Redis分布式锁

    摘要:今天我们来聊聊分布式锁这块知识,具体的来看看Redis分布式锁的实现原理. 一.写在前面 现在面试,一般都会聊聊分布式系统这块的东西.通常面试官都会从服务框架(Spring Cloud.Dubb ...

  8. Redis分布式锁 (图解-秒懂-史上最全)

    文章很长,而且持续更新,建议收藏起来,慢慢读! 高并发 发烧友社群:疯狂创客圈(总入口) 奉上以下珍贵的学习资源: 疯狂创客圈 经典图书 : 极致经典 + 社群大片好评 < Java 高并发 三 ...

  9. jedisLock—redis分布式锁实现

    一.使用分布式锁要满足的几个条件: 系统是一个分布式系统(关键是分布式,单机的可以使用ReentrantLock或者synchronized代码块来实现) 共享资源(各个系统访问同一个资源,资源的载体 ...

随机推荐

  1. SQL Injection (Blind) Low

    SQL盲注分析 盲注较普通注入难度会有所增加,根据页面响应不同大概分为以下几种:布尔型盲注:时间盲注:报错注入 普通注入与盲注的对比: 普通注入:                            ...

  2. Elements-of-Python_03_LanguageFeature

    (内容包括Python语法概述,流程控制,条件表达式) 1 Python语法 1.1 Python的特点 Python是一种完全面向对象的.解释性的.可移植的.开源的脚本编程高级语言,具有丰富的库,允 ...

  3. pandas 处理缺失值(连续值取平均,离散值fillna"<unk>")

    # 2.1处理缺失值,连续值用均值填充 continuous_fillna_number = [] for i in train_null_ix: if(i in continuous_ix): me ...

  4. 使用eslint将项目中的代码修改统一的缩进

    背景 继承了组里师兄师姐写的项目的前端代码,但是是两个人写的,有两格缩进的,有四格缩进的,有字符串外用单引号的,有用双引号的. 于是搜索了一下,可以用eslint强制转化. eslint在github ...

  5. 谷歌浏览器debugger技巧

    1.Pause/Resume script execution:暂停/恢复脚本执行(程序执行到下一断点停止). 2.Step over next function call:逐步执行. 跳过按钮.它会 ...

  6. 微信小程序-卡券开发(前端)

    刚完成一个微信小程序卡券开发的项目.下面记录开发前,自己困惑的几个问题. 因为我只负责了前端.所以下面主要是前端的工作. 项目概述:按照设计图开发好首页上的优惠券列表,点击某个优惠券,输入手机号,点击 ...

  7. Day5 - 05 函数的参数-关键字参数

    可变参数可以传入任意个参数,并在函数调用时自动组为一个tuple,而关键字参数允许传入任意个携带参数名的参数,这些关键字参数在函数内部自动组为一个dict.         >>> ...

  8. js下 Day11、案例

    一.成绩分类 效果图: 功能思路分析: 1. 渲染数据 2. 鼠标按下开启拖拽 \1. 给成绩盒子绑定鼠标按下事件(mousedown),用事件委托做多个标签的拖拽 \2. 开启控制拖拽的变量 \3. ...

  9. 每日CSS_发光文本效果

    每日CSS_发光文本效果 2020_12_22 源码 1. 代码解析 1.1 html 代码片段 <h1> <span>今</span> <span>天 ...

  10. windows下python3和python2虚拟环境配置

    Python3 被越来越多的开发者所接受,同时让人尴尬的是很多遗留的老系统依旧运行在 Python2 的环境中,因此有时你不得不同时在两个版本中进行开发,调试. 如何在系统中同时共存 Python2 ...