关注微信公众号(文强的技术小屋),学习更多技术知识,一起遨游知识海洋~

快速导航:

Redis入门总结(一):redis配置文件,五种数据结构,线程模型和持久化方式

Redis入门总结(二):主从复制,事务和发布订阅

Redis入门总结(三):redis实现分布式锁的正确姿势

目录

前言:

可靠性:

代码实现

组件依赖

加锁代码

正确示例:

错误示例1:

错误示例2

解锁代码

正确姿势

错误示例1

错误示例2

总结


前言:

分布式锁一般有三种实现方式:

  • 数据库乐观锁
  • 基于Redis的分布式锁
  • 基于ZooKeeper的分布式锁。

本篇博客将介绍第二种方式,基于Redis实现分布式锁。虽然网上已经有各种介绍Redis分布式锁实现的博客,然而他们的实现却有着各种各样的问题,为了寻找redis实现分布式锁的正确姿势,本篇博客将详细介绍如何正确地实现Redis分布式锁。

可靠性:

首先,为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件:

  1. 互斥性。在任意时刻,只有一个客户端能持有锁。
  2. 不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
  3. 具有容错性。只要大部分的Redis节点正常运行,客户端就可以加锁和解锁。
  4. 解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。

代码实现

组件依赖

首先我们要通过Maven引入Jedis开源组件,在pom.xml文件加入下面的代码:

<dependency><groupId>redis.clients</groupId><artifactId>jedis</artifactId><version>2.9.0</version>
</dependency>

加锁代码

正确示例:

Talk is cheap, show me the code。先展示代码,再带大家慢慢解释为什么这样实现:

public class RedisTool {private static final String LOCK_SUCCESS = "OK";private static final String SET_IF_NOT_EXIST = "NX";private static final String SET_WITH_EXPIRE_TIME = "PX";/*** 尝试获取分布式锁* @param jedis Redis客户端* @param lockKey 锁* @param requestId 请求标识* @param expireTime 超期时间* @return 是否获取成功*/public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);if (LOCK_SUCCESS.equals(result)) {return true;}return false;}}

可以看到,我们加锁就一行代码:jedis.set(String key, String value, String nxxx, String expx, int time)这个set()方法一共有五个形参:

  • 第一个为key,我们使用key来当锁,因为key是唯一的。

  • 第二个为value,我们传的是requestId,很多童鞋可能不明白,有key作为锁不就够了吗,为什么还要用到value?原因就是我们在上面讲到可靠性时,分布式锁要满足第四个条件解铃还须系铃人,通过给value赋值为requestId,我们就知道这把锁是哪个请求加的了,在解锁的时候就可以有依据。requestId可以使用UUID.randomUUID().toString()方法生成。

  • 第三个为nxxx,这个参数我们填的是NX,意思是SET IF NOT EXIST,即当key不存在时,我们进行set操作;若key已经存在,则不做任何操作;

  • 第四个为expx,这个参数我们传的是PX,意思是我们要给这个key加一个过期的设置,具体时间由第五个参数决定。

  • 第五个为time,与第四个参数相呼应,代表key的过期时间。

总的来说,执行上面的set()方法就只会导致两种结果:1. 当前没有锁(key不存在),那么就进行加锁操作,并对锁设置个有效期,同时value表示加锁的客户端。2. 已有锁存在,不做任何操作。

我们的加锁代码满足我们可靠性里描述的三个条件。首先,set()加入了NX参数,可以保证如果已有key存在,则函数不会调用成功,也就是只有一个客户端能持有锁,满足互斥性。其次,由于我们对锁设置了过期时间,即使锁的持有者后续发生崩溃而没有解锁,锁也会因为到了过期时间而自动解锁(即key被删除),不会发生死锁。最后,因为我们将value赋值为requestId,代表加锁的客户端请求标识,那么在客户端在解锁的时候就可以进行校验是否是同一个客户端。由于我们只考虑Redis单机部署的场景,所以容错性我们暂不考虑。

错误示例1:

比较常见的错误示例就是使用jedis.setnx()jedis.expire()组合实现加锁,代码如下:

public static void wrongGetLock1(Jedis jedis, String lockKey, String requestId, int expireTime) {Long result = jedis.setnx(lockKey, requestId);if (result == 1) {// 若在这里程序突然崩溃,则无法设置过期时间,将发生死锁jedis.expire(lockKey, expireTime);}}

setnx()方法作用就是SET IF NOT EXISTexpire()方法就是给锁加一个过期时间。乍一看好像和前面的set()方法结果一样,然而由于这是两条Redis命令,不具有原子性,如果程序在执行完setnx()之后突然崩溃,导致锁没有设置过期时间。那么将会发生死锁。网上之所以有人这样实现,是因为低版本的jedis并不支持多参数的set()方法。

错误示例2

public static boolean wrongGetLock2(Jedis jedis, String lockKey, int expireTime) {long expires = System.currentTimeMillis() + expireTime;String expiresStr = String.valueOf(expires);// 如果当前锁不存在,返回加锁成功if (jedis.setnx(lockKey, expiresStr) == 1) {return true;}// 如果锁存在,获取锁的过期时间String currentValueStr = jedis.get(lockKey);if (currentValueStr != null && Long.parseLong(currentValueStr) < System.currentTimeMillis()) {// 锁已过期,获取上一个锁的过期时间,并设置现在锁的过期时间String oldValueStr = jedis.getSet(lockKey, expiresStr);if (oldValueStr != null && oldValueStr.equals(currentValueStr)) {// 考虑多线程并发的情况,只有一个线程的设置值和当前值相同,它才有权利加锁return true;}}// 其他情况,一律返回加锁失败return false;}

这一种错误示例就比较难以发现问题,而且实现也比较复杂。

实现思路:使用jedis.setnx()命令实现加锁,其中key是锁,value是锁的过期时间。

执行过程:

  • 通过setnx()方法尝试加锁,如果当前锁不存在,返回加锁成功。
  • 如果锁已经存在则获取锁的过期时间,和当前时间比较,如果锁已经过期,则设置新的过期时间,返回加锁成功。

那么这段代码问题在哪里?

  • 由于是客户端自己生成过期时间,所以需要强制要求分布式下每个客户端的时间必须同步。
  • 当锁过期的时候,如果多个客户端同时执行jedis.getSet()方法,那么虽然最终只有一个客户端可以加锁,但是这个客户端的锁的过期时间可能被其他客户端覆盖。
  • 锁不具备拥有者标识,即任何客户端都可以解锁。

解锁代码

正确姿势

还是先展示代码,再带大家慢慢解释为什么这样实现:

public class RedisTool {private static final Long RELEASE_SUCCESS = 1L;/*** 释放分布式锁* @param jedis Redis客户端* @param lockKey 锁* @param requestId 请求标识* @return 是否释放成功*/public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));if (RELEASE_SUCCESS.equals(result)) {return true;}return false;}}

可以看到,我们解锁只需要两行代码就搞定了!第一行代码,我们写了一个简单的Lua脚本代码,上一次见到这个编程语言还是在《黑客与画家》里,没想到这次居然用上了。第二行代码,我们将Lua代码传到jedis.eval()方法里,并使参数KEYS[1]赋值为lockKey,ARGV[1]赋值为requestId。eval()方法是将Lua代码交给Redis服务端执行。

那么这段Lua代码的功能是什么呢?

其实很简单,首先获取锁对应的value值,检查是否与requestId相等,如果相等则删除锁(解锁)。

那么为什么要使用Lua语言来实现呢?

因为要确保上述操作是原子性的。关于非原子性会带来什么问题,可以阅读【解锁代码-错误示例2】 。那么为什么执行eval()方法可以确保原子性,源于Redis的特性,下面是官网对eval命令的部分解释:简单来说,就是在eval命令执行Lua代码的时候,Lua代码将被当成一个命令去执行,并且直到eval命令执行完成,Redis才会执行其他命令。

错误示例1

最常见的解锁代码就是直接使用jedis.del()方法删除锁,这种不先判断锁的拥有者而直接解锁的方式,会导致任何客户端都可以随时进行解锁,即使这把锁不是它的。

public static void wrongReleaseLock1(Jedis jedis, String lockKey) {jedis.del(lockKey);
}

错误示例2

这种解锁代码乍一看也是没问题,甚至我之前也差点这样实现,与正确姿势差不多,唯一区别的是分成两条命令去执行,代码如下:

public static void wrongReleaseLock2(Jedis jedis, String lockKey, String requestId) {// 判断加锁与解锁是不是同一个客户端if (requestId.equals(jedis.get(lockKey))) {// 若在此时,这把锁突然不是这个客户端的,则会误解锁jedis.del(lockKey);}}

如代码注释,问题在于如果调用jedis.del()方法的时候,这把锁已经不属于当前客户端的时候会解除他人加的锁。

那么是否真的有这种场景?答案是肯定的,比如客户端A加锁,一段时间之后客户端A解锁,在执行jedis.del()之前,锁突然过期了,此时客户端B尝试加锁成功,然后客户端A再执行del()方法,则将客户端B的锁给解除了。


总结

本文主要介绍了如何使用Java代码正确实现Redis分布式锁,对于加锁和解锁也分别给出了两个比较经典的错误示例。其实想要通过Redis实现分布式锁并不难,只要保证能满足可靠性里的四个条件。

本文转载于网络,间接转载于https://blog.csdn.net/Happy_wu/article/details/80083158 ,原始文章未知。在我对redis的初步学习中,感觉这篇文章确实分析比较详细一点,网络上其余博主的文章大多存在一定的原子性等问题。

如果对你有帮助,记得点赞哦~欢迎大家关注我的博客,可以进群366533258一起交流学习哦~

本群给大家提供一个学习交流的平台,内设菜鸟Java管理员一枚、精通算法的金牌讲师一枚、Android管理员一枚、蓝牙BlueTooth管理员一枚、Web前端管理一枚以及C#管理一枚。欢迎大家进来交流技术。

关注微信公众号(文强的技术小屋),学习更多技术知识,一起遨游知识海洋~

Redis入门总结(三):redis实现分布式锁的正确姿势相关推荐

  1. 【分布式缓存系列】Redis实现分布式锁的正确姿势

    一.前言 在我们日常工作中,除了Spring和Mybatis外,用到最多无外乎分布式缓存框架--Redis.但是很多工作很多年的朋友对Redis还处于一个最基础的使用和认识.所以我就像把自己对分布式缓 ...

  2. 掌握Redis分布式锁的正确姿势

    本文中案例都会在上传到git上,请放心浏览 git地址:https://github.com/muxiaonong/Spring-Cloud/tree/master/order-lock 本文会使用到 ...

  3. Redis实现分布式锁的正确姿势 | Spring Cloud 36

    一.分布式锁 1.1 什么是分布式锁 分布式锁,即分布式系统中的锁.在单体应用中我们通过锁解决的是控制共享资源访问的问题,而分布式锁,就是解决了分布式系统中控制共享资源访问的问题.与单体应用不同的是, ...

  4. 程序员如何 Get 分布式锁的正确姿势?| 技术头条

    作者 | 刘春龙 责编 | 郭芮 在很多互联网产品应用中,有些场景需要加锁处理,比如秒杀.全局递增ID.楼层生成等等,大部分的解决方案是基于DB实现的,Redis也是较为常见的方案之一. Redis为 ...

  5. 这才是实现分布式锁的正确姿势!

    都9102年了,你还在手写分布式锁吗? 经常被问到"如何实现分布式锁",看来这是大家的一个痛点. 其实Java世界的"半壁江山"--Spring早就提供了分布式 ...

  6. 【最全】Spring Boot 实现分布式锁——这才是实现分布式锁的正确姿势!

    ava世界的"半壁江山"--Spring早就提供了分布式锁的实现.早期,分布式锁的相关代码存在于Spring Cloud的子项目Spring Cloud Cluster中,后来被迁 ...

  7. 七种方案!探讨Redis分布式锁的正确使用姿势

    前言 日常开发中,秒杀下单.抢红包等等业务场景,都需要用到分布式锁.而Redis非常适合作为分布式锁使用.本文将分七个方案展开,跟大家探讨Redis分布式锁的正确使用方式.如果有不正确的地方,欢迎大家 ...

  8. Redis 作者 Antirez 讲如何实现分布式锁?Redis 实现分布式锁天然的缺陷分析Redis分布式锁的正确使用姿势!...

    Redis分布式锁基本原理 采用 redis 实现分布式锁,主要是利用其单线程命令执行的特性,一般是 setnx, 只会有一个线程会执行成功,也就是只有一个线程能成功获取锁:看着很完美. 然而-- 看 ...

  9. 视频教程- 19年录制Redis实战教程 高可用秒杀分布式锁布隆过滤器实战 SpringBoot教程整合-Java

    19年录制Redis实战教程 高可用秒杀分布式锁布隆过滤器实战 SpringBoot教程整合 7年的开发架构经验,曾就职于国内一线互联网公司,开发工程师,现在是某创业公司技术负责人, 擅长语言有nod ...

  10. 【Redis Lua 脚本 可重入分布式锁】

    文章目录 前言 一.最简单的版本:setnx key value 获取锁成功 获取锁失败 释放锁 缺点 二.升级版本:set key value [ex seconds] [nx] 获取锁成功 获取锁 ...

最新文章

  1. Word VSTO Error:Interop type 'Microsoft.Office.Interop.OneNote.Application' cannot be embedded...
  2. python Pil byteio转换
  3. ThinkPHP 框架培训资料
  4. linux自动对齐命令,linux查看硬盘4K对齐方法
  5. 最小二乘法矩阵微分偏导法证明
  6. 数据库:MySQL大批量SQL插入性能优化
  7. 使用Spring JUnit规则进行参数化集成测试
  8. Android官方开发文档Training系列课程中文版:电池续航时间优化之按需开启广播接收器
  9. ORA-39070:无法打开日志文件
  10. 新手学习Linux——搭建个人论坛
  11. Linux之Inode详解 作者:羽飞博客 http://www.opsers.org/
  12. 【51nod 1439】互斥对【容斥原理】
  13. 8种Python文本处理工具集
  14. 手把手教你在 Vue 中使用 JSX,不怕学不会!【建议收藏】
  15. 品味之旅见行见心 ——香港科大EMBA郎酒庄园深度体验之旅
  16. 新版微信文件夹路径FileStorage变成了MsgAttach
  17. windows下利用注册表regedit手动删除文件
  18. UE4-(蓝图)第四十七课过场动画之主序列新建镜头、镜头剪辑、部分功能简介
  19. Windows Server 2008 R2中Windows Server Backup功能之备份、恢复
  20. 卡内基梅隆大学计算机专业介绍,卡内基梅隆大学计算机专业介绍 全美大学计算机专业榜首...

热门文章

  1. Linux文件归档与压缩命令
  2. 焊接好的CH340G芯片不工作
  3. rails 杂记 - erb 中的 form_helper
  4. ipsec-tools之racoon搭建
  5. 【View基础知识】TouchSlop、VelocityTracker、GestureDetector、Scroller
  6. 应急响应之windows进程排查
  7. Python 爬虫——爬取文章自动发送QQ群
  8. 系统盘修复计算机命令,U盘启动盘修复系统的详细步骤
  9. 面积二次矩second moment of area 极惯性矩polar moment of area 转动惯量moment of inertia面积一次矩first moment of area
  10. 面试官:请你讲讲Thread.sleep(0) 的作用?