写在最前面

我在之前总结幂等性的时候,写过一种分布式锁的实现,可惜当时没有真正应用过,着实的心虚啊。正好这段时间对这部分实践了一下,也算是对之前填坑了。

分布式锁按照网上的结论,大致分为三种:1、数据库乐观锁; 2、基于Redis的分布式锁;3.、基于ZooKeeper的分布式锁;

关于乐观锁的实现其实在之前已经讲的很清楚了,有兴趣的移步:使用mysql乐观锁解决并发问题 。今天先简单总结下redis的实现方法,后面详细研究过ZooKeeper的实现原理后再具体说说ZooKeeper的实现。

为什么需要分布式锁?

在传统单体应用单机部署的情况下,可以使用Java并发相关的锁,如ReentrantLcok或synchronized进行互斥控制。但是,随着业务发展的需要,原单体单机部署的系统,渐渐的被部署在多机器多JVM上同时提供服务,这使得原单机部署情况下的并发控制锁策略失效了,为了解决这个问题就需要一种跨JVM的互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题。

分布式锁的实现条件

1、互斥性,和单体应用一样,要保证任意时刻,只能有一个客户端持有锁

2、可靠性,要保证系统的稳定性,不能产生死锁

3、一致性,要保证锁只能由加锁人解锁,不能产生A的加锁被B用户解锁的情况

Redis分布式锁的实现

Redis实现分布式锁不同的人可能有不同的实现逻辑,但是核心就是下面三个方法。

SETNX
SETNX key val
当且仅当key不存在时,set一个key为val的字符串,返回1;若key存在,则什么都不做,返回0。
Expire
expire key timeout
为key设置一个超时时间,单位为second,超过这个时间锁会自动释放,避免死锁。
Delete
delete key
删除key

获取锁

首先讲一个目前网上应用最多的一种实现

实现思路:

1.获取锁的时候,使用setnx加锁,并使用expire命令为锁添加一个超时时间,超过该时间则自动释放锁以免产生死锁,锁的value值为一个随机生成的UUID,通过此在释放锁的时候进行判断。

2.获取锁的时候还设置一个获取的超时时间,若超过这个时间则放弃获取锁。

3.释放锁的时候,通过UUID判断是不是该锁,若是该锁,则执行delete进行锁释放。

    public String getRedisLock(Jedis jedis, String lockKey, Long acquireTimeout, Long timeOut) {try {// 定义 redis 对应key 的value值(uuid) 作用 释放锁 随机生成value,根据项目情况修改String identifierValue = UUID.randomUUID().toString();// 定义在获取锁之后的超时时间int expireLock = (int) (timeOut / 1000);// 以秒为单位// 定义在获取锁之前的超时时间//使用循环机制 如果没有获取到锁,要在规定acquireTimeout时间 保证重复进行尝试获取锁// 使用循环方式重试的获取锁Long endTime = System.currentTimeMillis() + acquireTimeout;while (System.currentTimeMillis() < endTime) {// 获取锁// 使用setnx命令插入对应的redislockKey ,如果返回为1 成功获取锁if (jedis.setnx(lockKey, identifierValue) == 1) {// 设置对应key的有效期
                    jedis.expire(lockKey, expireLock);return identifierValue;}}} catch (Exception e) {e.printStackTrace();} return null;}

这种实现方法也是目前应用最多的实现,我一直以为这确实是正确的。然而由于这是两条Redis命令,不具有原子性,如果程序在执行完setnx()之后突然崩溃,导致锁没有设置过期时间。那么还是会发生死锁的情况。网上之所以有人这样实现,是因为低版本的jedis并不支持多参数的set()方法。

当然这种情况Jedis的设计者也显然想到了,新版的Jedis可以同时set多个参数,具体实现如下:

实现思路:

基本上和原来的逻辑类似,只是将setnx和expire的操作合并为一步,改为使用新的set多参的方法

set(final String key, final String value, final String nxxx, final String expx,final long time)

keyvalue自然不用多说。

nxxx参数只可以传String 类型的NX(仅在不存在的情况下设置)和XX(和普通的set操作一样会做更新操作)两种。

expx是指到期时间单位,可传参数为EX (秒)和 PX (毫秒)

time就是具体的过期时间了,单位为前面expx所指定的。

然后我们对上面的代码进行改造如下:

    /*** @param acquireTimeout*            在获取锁之前的超时时间* @param timeOut*            在获取锁之后的超时时间*/public String getRedisLock(Jedis jedis, String lockKey, Long acquireTimeout, Long timeOut) {try {// 定义 redis 对应key 的value值(uuid) 作用 释放锁 随机生成value,根据项目情况修改String identifierValue = UUID.randomUUID().toString();// 定义在获取锁之前的超时时间//使用循环机制 如果没有获取到锁,要在规定acquireTimeout时间 保证重复进行尝试获取锁// 使用循环方式重试的获取锁Long endTime = System.currentTimeMillis() + acquireTimeout;while (System.currentTimeMillis() < endTime) {// 获取锁// set使用NX参数的方式就等同于 setnx()方法,成功返回OK.PX以毫秒为单位if ("OK".equals(jedis.set(lockKey, lockKey, "NX", "PX", timeOut))) {return identifierValue;}}} catch (Exception e) {e.printStackTrace();} return null;}

好了,获取锁的操作基本上就上面这些,有同学可能要问,为什么不直接返回一个Boolean型的true或false呢?

正如我前面所说的,要保证解锁的一致性,所以就需要通过value值来保证解锁人就是加锁人,而不能直接返回true或false了。

下面在说下解锁的过程。

释放锁

还是先举一个错误的例子:

实现思路:

释放锁的时候,通过传入key和加锁时返回的value值,判断传入的value是否和key从redis中取出的相等。相等则证明解锁人就是加锁人,执行delete释放锁的操作。

    // 释放redis锁public void unRedisLock(Jedis jedis, String lockKey, String identifierValue) {try {// 如果该锁的id 等于identifierValue 是同一把锁情况才可以删除if (jedis.get(lockKey).equals(identifierValue)) {jedis.del(lockKey);}} catch (Exception e){e.printStackTrace();}}

看着好像没啥问题哈。然而仔细想想又总感觉哪里不对。

如果在执行jedis.del(lockKey)操作之前,刚好锁的过期时间到了,而这个时候又有别的客户端取到了锁,我们在此时执行删除操作,不是又不符合一致性的要求了吗。

然后我们修改为下述方案:

修改后的代码为:

public void unRedisLock(Jedis jedis, String lockKey, String identifierValue) {try {String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";Long result = (Long) jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(identifierValue));//0释放锁失败。1释放成功if (1 == result) {//如果你想返回删除成功还是失败,可以在这里返回System.out.println(result+"释放锁成功");} if (0 == result){System.out.println(result+"释放锁失败");}} catch (Exception e){e.printStackTrace();}}

实现思路:

我们将Lua代码传到jedis.eval()方法里,并使参数KEYS[1]赋值为lockKey,ARGV[1]赋值为identifierValue。eval()方法是将Lua代码交给Redis服务端执行。

那么这段Lua代码的功能是什么呢?其实很简单,首先获取锁对应的value值,检查是否与identifierValue相等,如果相等则删除锁(解锁)。那么为什么要使用Lua语言来实现呢?因为要确保上述操作是原子性的。

那么为什么执行eval()方法可以确保原子性?源于Redis的特性,因为Redis是单线程,在eval命令执行Lua代码的时候,Lua代码将被当成一个命令去执行,并且直到eval命令执行完成,Redis才会执行其他命令。

总结

本文对Redis实现分布式锁做了比较详细的总结。我个人也对上述代码做了实践检验。其实我在使用时,一直用的错误的案例。直到看到园友Ruthless的一篇文章才晓得稀疏平常的写法竟然漏洞百出,感谢博客园,感谢Ruthless。编码是在现实生活中最容易产生成就感的工作,在学习过程中更要时刻保持着质疑精神,多思考多验证。

最后附上Ruthless的原文链接:https://www.cnblogs.com/linjiqin/p/8003838.html

转载于:https://www.cnblogs.com/laoyeye/p/10011695.html

分布式锁实践(一)-Redis编程实现总结相关推荐

  1. redis分布式锁实践 并实现看门狗锁续期机制

    redis分布式锁最佳实践(并实现锁续期机制) 文章目录 redis分布式锁最佳实践(并实现锁续期机制) 1. 分布式锁是什么? 2. setnx 和 AQS state 3. jedis完成分布式锁 ...

  2. 分布式锁实现:Redis

    前言 单机环境下我们可以通过JAVA的Synchronized和Lock来实现进程内部的锁,但是随着分布式应用和集群环境的出现,系统资源的竞争从单进程多线程的竞争变成了多进程的竞争,这时候就需要分布式 ...

  3. 分布式锁(基于redis和zookeeper)详解

    分布式锁(基于redis和zookeeper)详解 https://blog.csdn.net/a15835774652/article/details/81775044 为什么写这篇文章? 目前网上 ...

  4. zookeeper 分布式锁_关于redis分布式锁,zookeeper分布式锁原理的一些学习与思考

    编辑:业余草来源:https://www.xttblog.com/?p=4946 首先分布式锁和我们平常讲到的锁原理基本一样,目的就是确保,在多个线程并发时,只有一个线程在同一刻操作这个业务或者说方法 ...

  5. ZooKeeper(三) 什么是分布式锁以及使用Redis手写实现

    一.什么是分布式锁? 分布式锁是相对于单体单机应用而言的一种锁机制.在单机应用时由于共享一个jvm,可以使用同一个java Lock对象进行获取锁,解锁操作.当为分布式集群时存在跨机器请求执行,无法共 ...

  6. python redis分布式锁_Python 使用 Redis 实现分布式锁

    前言 随着互联网技术的不断发展,用户量的不断增加,越来越多的业务场景需要用到分布式系统.而在分布式系统中访问共享资源就需要一种互斥机制,来防止彼此之间的互相干扰,以保证一致性,这个时候就需要使用分布式 ...

  7. 阿里面试官:分布式锁到底用Redis好?还是Zookeeper好?

    首先,分布式锁和我们平常讲到的锁原理基本一样,目的就是确保,在多个线程并发时,只有一个线程在同一刻操作这个业务或者说方法.变量. 在一个进程中,也就是一个jvm 或者说应用中,我们很容易去处理控制,在 ...

  8. Redis教程--redis分布式锁+企业解决方案+redis实战

    Redis,目前全国甚至是全球最常用的缓存中间件之一,在现在公司的开发中,可以说是离不开Redis. 在企业越来越注重用户体验的今天,Redis因具有高性能.高响应的特性,大大提升应用的响应速度和用户 ...

  9. php使用redis分布式锁,php基于redis的分布式锁实例详解

    在使用分布式锁进行互斥资源访问时候,我们很多方案是采用redis的实现. 固然,redis的单节点锁在极端情况也是有问题的,假设你的业务允许偶尔的失效,使用单节点的redis锁方案就足够了,简单而且效 ...

  10. 数据结构(字典,跳跃表)、使用场景(计数器、缓存、查找表、消息队列、会话缓存、分布式锁)、Redis 与 Memcached、 键的过期时间、数据淘汰策略、持久化(RDB、AOF)

    1. 数据结构 1.1 字典 dictht 是一个散列表结构,使用拉链法保存哈希冲突的 dictEntry /* This is our hash table structure. Every dic ...

最新文章

  1. 下拉框处理(select)
  2. 用一条sql取得第10到第20条的记录-Mssql数据库
  3. 成功解决在excel表中通过数学函数转换后,接着去掉公式转为不再随着变化的数值
  4. MySQL-06:pyMySQL增删改查基本命令笔记
  5. 学习,工作,编程必看:130 个相见恨晚的神器网站
  6. 无人车研发实力哪家强?Google只能排第十
  7. IntelliJ IDEA,WebStorm,PyCharm 2017+缓存位置修改
  8. 大数据工程师简历_大数据工程师简历专业技能怎么写
  9. ubuntu20.04.1下安装qt4相关依赖库
  10. win10桌面右键一直转圈是什么原因
  11. 从键盘输入n个数 求其中的最大数
  12. 摄像头ip分享论坛_谁的SIP软交换呼叫中心终端摄像头正在公网裸奔
  13. java中的arrayList(动态数组)与静态数组
  14. 腾讯云轻量级应用服务器的配置搭建及网站
  15. 自学人工智能编程难吗?
  16. 华为手机文件在内部存储路径_Android手机自带内部存储路径的获取
  17. 剑三重置版找不到服务器,剑网3重制版客户端常见问题处理方案整理
  18. 有哪些小巧好用的pdf阅读器
  19. mysql cbo_SparkSQL2.4中启用CBO时JoinReorder分析
  20. 黑苹果安装EP-DB1608无线网卡驱动

热门文章

  1. spring源码:@Configuration源码
  2. 异常的分类以及什么异常触发回滚
  3. clock_gettime接口和linux时间系统
  4. 调整数组使奇数全部都位于偶数前面
  5. 《人月神话》之外科手术队伍
  6. shell字符串的截取的问题
  7. PLSQL提交带有模板的报表的方法
  8. 19_完成“我的订单”
  9. 机器人 知乎碧桂园_杨国强森林城市与机器人跃进后的“梦醒时分”
  10. HDU6072 Logical Chain