文章目录

  • 一、基础版(含自动释放锁)
  • 二、改良版(含过期时间)
  • 三、进阶版(含唯一性验证)
  • 四、单节点版(含Redisson)
  • 五、多节点版(含RedLock)

写在前面:
既然已经上升到了分布式场景,那么传统单机下保证线程安全的锁自然就不起作用了,如synchronized、AQS、ThrealLocal等,因为他们锁的都是JVM级别的,分布式项目已经跨越了JVM级别,所以他们都会失效。


一、基础版(含自动释放锁)

使用setnx完成一个最基础的分布式锁

该锁能够对指定的key完成加锁,在执行完业务逻辑后,也能释放对应的锁。

public class ClientController {private static StringRedisTemplate stringRedisTemplate = new StringRedisTemplate();public static void main(String[] args) {// 模拟前台接收到的idString keyId = "mobian_100";try {Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(keyId, "test_lock_value"); // 类比理解 jedis.setnx(key,value)if (!result) {System.out.println("获取锁失败,直接返回前台");}// 业务逻辑TimeUnit.SECONDS.sleep(5);} catch (InterruptedException e) {e.printStackTrace();} finally {stringRedisTemplate.delete(keyId);}System.out.printf("单次请求处理完成");}
}

问题:

如果在执行业务代码的过程中,遇到宕机,那么当程序再次启动,对应的锁已经存在,此时就会出现死锁问题。基于此,我们想要达到的目的是,添加的锁能在自动释放,于是就引出了我们的第二版,在添加分布式锁的时候,给指定key增加一个过期时间。


二、改良版(含过期时间)

在设置添加锁和设置过期时间的时候,我们不要使用两条命令去执行,因为如果不是原子操作,依然还会出现第一版中的问题,我们可以使用setIfAbsent的含有过期时间的重载方法。如下面的代码

public class ClientController2 {private static StringRedisTemplate stringRedisTemplate = new StringRedisTemplate();public static void main(String[] args) {// 模拟前台接收到的idString keyId = "mobian_100";try {// 超时时间和设置key一步操作Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(keyId, "test_lock_value", 20, TimeUnit.SECONDS);if (!result) {System.out.println("获取锁失败,直接返回前台");}// 业务逻辑TimeUnit.SECONDS.sleep(5);} catch (InterruptedException e) {e.printStackTrace();} finally {stringRedisTemplate.delete(keyId);}System.out.printf("单次请求处理完成");}
}

问题:

我们给锁设置了20S的过期时间,即20S后锁自动释放。试想一下,如果对应的业务逻辑执行了25S,此时会出现什么样的问题?

线程1添加了一个20S的分布式锁,业务执行了25S,即在第20.0001S的时候,线程2又添加了一个分布式锁,在第25.0001S的时候,线程1执行到了finally中的代码,此时他会去释放对应的锁信息。我们要明确的是,如果线程1的锁没有因为锁过期,那么线程2就不可能加锁成功,那么释放的所信息就是线程1的,现在线程1的过期了,那么释放的锁信息就变成了释放线程2的锁信息。当线程2的锁信息释放后,线程3又开始加锁成功,等到线程2来释放锁的时候,就变成释放线程3的锁了。基于此我们不难发现,线程释放错了别人的锁会引发一些的雪崩效应,最终照成业务数据混乱。

如果解决呢?我们可以给锁的value设置一个随机值,在删除锁的时候再进行value的判定,是自己的锁才释放,于是代码可以写成第三版。


三、进阶版(含唯一性验证)

基本代码和第二版一致,仅多了一个使用UUID生成的随机数,并把对应的随机数设置为分布式锁的value,当我们在释放锁的时候,取出对应的value,如果相同再删除,不同则说明过期,直接略过。

public class ClientController3 {private static StringRedisTemplate stringRedisTemplate = new StringRedisTemplate();public static void main(String[] args) {// 模拟前台接收到的idString keyId = "mobian_100";String clientId = UUID.randomUUID().toString();try {// 超时时间和设置key一步操作Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(keyId, clientId, 20, TimeUnit.SECONDS);if (!result) {System.out.println("获取锁失败,直接返回前台");}// 业务逻辑TimeUnit.SECONDS.sleep(5);} catch (InterruptedException e) {e.printStackTrace();} finally {if (clientId.equals(stringRedisTemplate.opsForValue().get(keyId))) {stringRedisTemplate.delete(keyId);}}System.out.printf("单次请求处理完成");}
}

问题:

其实上面的解决办法仅仅解决了释放锁的时候不会释放错的问题,并没有在本质上解决锁的并发安全问题。第一点,还是上面的例子,如果线程1在第20.3S的时候才操作了数据,而线程2在第3S的时候就操作了数据,即依然会出现线程安全问题。第二点,虽然我们的代码多了一个value值得唯一性校验,试想一下,线程1执行到finally的第一行equals方法时,判断为true,随即马上去执行delete操作,此时刚好线程1设置的锁信息过期,并且线程2刚好又加锁成功,此时依旧出现了释放错锁的现象。

上面的问题可以归结为两点,第一就是锁的过期时间不能时一个固定值,需要随着业务代码的变化而变化。第二点就是判断锁信息和释放锁信息需要使用一个原子操作。当然,你也可以理解为第一点才是问题的关键。

解决办法,第一点可以在后台能再启动一个线程去监控对应的锁过期时间和业务执行时间就能够达到目的。第二点,redis中可以利用lua脚本去复合执行很多操作,那么我们也就可以使用lua脚本编写判断和删除两个操作的脚本。巧在,我们能想到的别人早就已经想到,Redisson就是基于该原理实现了对应的分布式锁,即第四版代码。


四、单节点版(含Redisson)

引入Redisson,按照其指定的加锁方式即可完成对应的分布式锁的逻辑

<dependency><groupId>org.redisson</groupId><artifactId>redisson</artifactId><version>3.6.5</version>
</dependency>
public class ClientController4 {public static void main(String[] args) {Config config = new Config();config.useSingleServer().setAddress("redis://127.0.0.1:6379");RedissonClient redisson = Redisson.create(config);// 模拟前台接收到的idString keyId = "mobian_100";RLock lock = redisson.getLock(keyId);lock.lock(20, TimeUnit.SECONDS);try {// 业务逻辑TimeUnit.MILLISECONDS.sleep(5);} catch (InterruptedException e) {e.printStackTrace();} finally {lock.unlock();}}
}

观察上面的代码,你会发现和Java自带的AQS锁(ReentrantLock锁是AQS的其中一种实现)很相似,都是先lock,再unlock。在查看了Redisson的底层源码之后,你也就会发现很多东西就是AQS那一套,很多方法名字都是一样的如tryAcquire。不了解AQS底层实现的,小伙伴可以参考我之前的博客:浅谈AQS同步队列(含ReentrantLock加锁和解锁源码分析)

定时任务无限延长锁有效时间代码:

1)判断主线程的锁是否还存在,如果还存在,就延长internalLockLeaseTime(该值就是我们设置的锁的有效时间)时间

2)如果锁续命成功,则再次递归调用自己,再次完成锁续命

判断锁和释放锁原子化操作代码:

如果不是我们自己添加的锁,发布一个事件,然后直接返回nil。如果是我们添加的锁,就释放对应的锁,再返回 1

补充一张,Redisson底层加锁的大致流程图,如果你是从博客开头看到这里的,我不说,我相信你也能明白下图的逻辑。

问题:

使用Redisson是能够完成分布式场景下单Redis节点的数据安全问题。但是如果真实的业务为了数据安全,是采用Redis的多个对等单节点架构出现,我们还是会出现数据不一致问题。毕竟目前的逻辑是无法实现对Redis多节点架构同时加锁成功的,此时就引入了我们的RedLock


五、多节点版(含RedLock)

想要实现多个节点下的分布式锁,可以使用RedLock来实现,代码如下

public class ClientController5 {private static RedissonClient getRedisson(String url) {Config config = new Config();config.useSingleServer().setAddress(url);return Redisson.create(config);}public static void main(String[] args) {// 模拟前台接收到的idString keyId = "mobian_100";RLock lock1 = getRedisson("redis://127.0.0.1:6379").getLock(keyId);RLock lock2 = getRedisson("redis://127.0.0.1:6380").getLock(keyId);RLock lock3 = getRedisson("redis://127.0.0.1:6381").getLock(keyId);RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);try {boolean lockRes = redLock.tryLock(10, 30, TimeUnit.SECONDS);if (lockRes) {// 业务逻辑TimeUnit.MILLISECONDS.sleep(5);}} catch (InterruptedException e) {e.printStackTrace();} finally {redLock.unlock();}}
}

含有三个独立的Redis节点

通过Java API对Redis多节点进行加锁操作,只有当redis节点中半数以上的节点返回加锁成功,才能算此次加锁成功。即线程1中的key在redis1和redis2都加锁成功了,就算redis3因为意外宕机,那么我们也认为此次加锁成功。当线程2发送同样的key的时,此时redis3机器恢复正常,此时redis3能够加锁成功,但是redis1和redis2中已经存在对应的锁信息,即线程2的加锁不成功。

问题:

既然要保证半数节点都能加锁成功,那如果redis挂了两台怎么办?此时就会出现永远加锁不成功的现象,这是我们不愿意看到的。也许你会想到多扩展几台,不可能同时那么多Redis机器一起宕机。确实也有道理,可过多的Redis节点,一来增加了项目的复杂度,二来失去了Redis本来的高可用意义。

含有三个独立的节点,并且每个节点都含有主从关系

基于此,常规操作就是给每台Redis的master节点配置对应的slave节点,具体流程参考上面的流程图。当master节点接收到对应的加锁信息后,会在master节点完成加锁逻辑,然后返回加锁成功的信息,于此同时会将加锁信息同步给对应的slave节点。以此达到了高可用效果。

问题:

试想一下,线程1进行加锁操作,redis1主节点加锁成功,并且从节点数据同步成功,redis2主节点加锁成功,并且返回给client加锁成功提示,就在它将数据信息同步给slave节点的时候,主节点宕机,redis2的从节点就无法收到对应的线程1的加锁的数据,并且后期redis2的slave节点就被推举为master节点。紧接着线程2又进行加锁操作,此时就会出现redis3节点加锁成功,redis2节点也能够加锁成功,超过半数节点加锁成功,即线程2加锁成功。但此次加锁不应该成功,即还是出现了数据的不一致问题。

补充一个概念:

  • 主从:一份数据,一个redis库,为了数据安全,将数据由master节点备份到slave节点;
  • 集群:一份数据,多个redis库,将数据拆分为多个数据库存放(类似mysql的分库分表),每个redis数据库只存放一部分数据;
  • 对等的节点:多份数据,多个redis库,多个数据库的数据都是全量数据,且数据保持一致,每次操作都会进行三个数据库的操作。

以上的思想和zookeeper实现分布式锁思想相同,也就逐渐的由AP转向了CP(CAP理论: Consistency(一致性)、 Availability(可用性)、Partition tolerance(分区容错性),三者不可得兼),如果业务场景十分在意数据一致性,那为什么不直接使用zookeeper实现分布式锁呢?既然使用了Redis,势必会在数据一致性上做出让步。

浅谈如何使用Redis实现分布式锁相关推荐

  1. 得物技术浅谈深入浅出的Redis分布式锁

    一.什么是分布式锁 1.1 分布式锁介绍 分布式锁是控制不同系统之间访问共享资源的一种锁实现,如果不同的系统或同一个系统的不同主机之间共享了某个资源时,往往需要互斥来防止彼此干扰来保证一致性. 1.2 ...

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

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

  3. 积跬步以至千里,深入剖析Redis实战——分布式锁和延时队列

    前言 之前咱们简单介绍了一下Redis的简单结构,相信很多读者看着比较入门.的确,笔者在介绍任何技术时,都是由浅及深的路数,为的是刚入门不久的新人,毕竟相对于久经沙场的老将,新人更需要这方便的普及. ...

  4. 基于 Redis 实现分布式锁思考

    以下文章来源方志朋的博客,回复"666"获面试宝典 来源:blog.csdn.net/xuan_lu/article/details/111600302 分布式锁 基于redis实 ...

  5. Redis实现分布式锁的深入探究

    点击上方"方志朋",选择"设为星标" 回复"666"获取新整理的面试文章 一.分布式锁简介 锁 是一种用来解决多个执行线程 访问共享资源 错 ...

  6. nx set 怎么实现的原子性_基于Redis的分布式锁实现

    前言 本篇文章主要介绍基于Redis的分布式锁实现到底是怎么一回事,其中参考了许多大佬写的文章,算是对分布式锁做一个总结 分布式锁概览 在多线程的环境下,为了保证一个代码块在同一时间只能由一个线程访问 ...

  7. Zookeeper和Redis实现分布式锁,附我的可靠性分析

    作者:今天你敲代码了吗 链接:https://www.jianshu.com/p/b6953745e341 在分布式系统中,为保证同一时间只有一个客户端可以对共享资源进行操作,需要对共享资源加锁来实现 ...

  8. Redis——由分布式锁造成的重大事故

    作者:浪漫先生 原文:juejin.im/post/6854573212831842311 前言 基于Redis使用分布式锁在当今已经不是什么新鲜事了.本篇文章主要是基于我们实际项目中因为redis分 ...

  9. 基于Redis的分布式锁和Redlock算法

    来自:后端技术指南针 1 前言 今天开始来和大家一起学习一下Redis实际应用篇,会写几个Redis的常见应用. 在我看来Redis最为典型的应用就是作为分布式缓存系统,其他的一些应用本质上并不是杀手 ...

  10. 《Redis官方文档》用Redis构建分布式锁

    <Redis官方文档>用Redis构建分布式锁 用Redis构建分布式锁 在不同进程需要互斥地访问共享资源时,分布式锁是一种非常有用的技术手段. 有很多三方库和文章描述如何用Redis实现 ...

最新文章

  1. MySQL-锁表处理
  2. 怎么设置表格根据窗口自动调整_Word排版技巧之表格美化,你值得拥有!
  3. HTML5的未来 - HTML5 还能走多远?
  4. mysql存储过程详解[转]
  5. 如何解决tmux中Anaconda虚拟环境下的python版本不正确的问题
  6. C++模板类注意事项
  7. QT的QRegularExpressionValidator类的使用
  8. 基于J2EE+JBPM3.x/JBPM4.3+Flex流程设计器+Jquery+授权认证)企业普及版贝斯OA与工作流系统...
  9. 学习knex过程中好的参考资料整理
  10. Node.js: 如何继承 events 自定义事件及触发函数
  11. 末日前的唠叨:SEO之四大要不得
  12. 一米优店宝-淘宝店铺优化软件v1.0官方
  13. macOS Command - xcode-select
  14. stc15流水灯c语言,STC89C52单片机流水灯
  15. ionic3 使用QR Scaner 扫描
  16. NLB(网络负载平衡)+ADFS场高可用性安装
  17. 如何成为一名全栈开发人员
  18. JavaScript获取元素
  19. efi格式linux启动u盘启动不了,Grub2 EFI U盘启动
  20. Linux软件包管理-rpm、yum

热门文章

  1. Luogu1306 斐波那契公约数
  2. [2018.10.11 T3] 欠钱
  3. js正则表达式--个人常用
  4. Fiddler中常用的方法
  5. MySQL8.0.19解压安装教程
  6. python shell运行_Python 执行 Shell 命令
  7. 事务方法调用事务方法_实现系统调用的几种方法
  8. 计算机网络管理的应用,计算机网络管理技术及应用
  9. java法尔克2017款测评_史上最强买菜车!2017款奥迪RS4 Avant试驾测评
  10. 算法java人工智能_人工智能用的编程语言是哪些?