参考文章:

Redis分布式锁的正确实现方式

分布式锁看这篇就够了

在这两篇文章的指引下亲测 Redis分布式锁

引言

分布式系统一定会存在CAP权衡问题,所以才会出现分布式锁

什么是CAP理论?

为了更好的理解文章,建议阅读:分布式系统的CAP理论

什么是锁?

  • 在单进程的系统中,当存在多个线程可以同时改变某个变量(可变共享变量)时,就需要对变量或代码块做同步,使其在修改这种变量时能够线性执行消除并发修改变量。
  • 而同步的本质是通过锁来实现的。为了实现多个线程在一个时刻同一个代码块只能有一个线程可执行,那么需要在某个地方做个标记,这个标记必须每个线程都能看到,当标记不存在时可以设置该标记,其余后续线程发现已经有标记了则等待拥有标记的线程结束同步代码块取消标记后再去尝试设置标记。这个标记可以理解为锁。
  • 不同地方实现锁的方式也不一样,只要能满足所有线程都能看得到标记即可。如 Java 中 synchronize 是在对象头设置标记,Lock 接口的实现类基本上都只是某一个 volitile 修饰的 int 型变量其保证每个线程都能拥有对该 int 的可见性和原子修改,linux 内核中也是利用互斥量或信号量等内存数据做标记。
  • 除了利用内存数据做锁其实任何互斥的都能做锁(只考虑互斥情况),如流水表中流水号与时间结合做幂等校验可以看作是一个不会释放的锁,或者使用某个文件是否存在作为锁等。只需要满足在对标记进行修改能保证原子性和内存可见性即可。

分布式场景

此处主要指集群模式下,多个相同服务同时开启.

在许多的场景中,我们为了保证数据的最终一致性,需要很多的技术方案来支持,比如分布式事务分布式锁等。很多时候我们需要保证一个方法在同一时间内只能被同一个线程执行。在单机环境中,通过 Java 提供的并发 API 我们可以解决,但是在分布式环境下,就没有那么简单啦。

  • 分布式与单机情况下最大的不同在于其不是多线程而是多进程
  • 多线程由于可以共享堆内存,因此可以简单的采取内存作为标记存储位置。而进程之间甚至可能都不在同一台物理机上,因此需要将标记存储在一个所有进程都能看到的地方。

什么是分布式锁?

  • 当在分布式模型下,数据只有一份(或有限制),此时需要利用锁的技术控制某一时刻修改数据的进程数。
  • 与单机模式下的锁不仅需要保证进程可见,还需要考虑进程与锁之间的网络问题。(我觉得分布式情况下之所以问题变得复杂,主要就是需要考虑到网络的延时和不可靠。。。一个大坑)
  • 分布式锁还是可以将标记存在内存,只是该内存不是某个进程分配的内存而是公共内存如 Redis、Memcache。至于利用数据库、文件等做锁与单机的实现是一样的,只要保证标记能互斥就行。

我们需要怎样的分布式锁?

  • 可以保证在分布式部署的应用集群中,同一个方法在同一时间只能被一台机器上的一个线程执行。
  • 这把锁要是一把可重入锁(避免死锁)
  • 这把锁最好是一把阻塞锁(根据业务需求考虑要不要这条)
  • 这把锁最好是一把公平锁(根据业务需求考虑要不要这条)
  • 有高可用的获取锁和释放锁功能
  • 获取锁和释放锁的性能要好

可靠性

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

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

一般实现方式

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

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

本文将介绍第二种方式,基于Redis实现分布式锁。


测试代码实现

注意: Redis 从2.6.12版本开始 set 命令支持 NX 、 PX 这些参数来达到 setnx 、 setex 、 psetex 命令的效果,文档参见: http://doc.redisfans.com/string/set.html

Spring Boot 下的 RedisTemplate 并不支持 NX 同时设置过期时间这种 set 操作(具有原子性)

所以这里我们需要 Maven 引入支持这种 set 操作的 Jedis 依赖

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

并发下单,库存锁测试:

创建10个线程,同时启动下单操作,对库存操作加入分布式锁

测试代码:

package com.elise.userinfocenter;import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import redis.clients.jedis.Jedis;import java.util.Collections;@RunWith(SpringRunner.class)
@SpringBootTest
public class UserInfoCenterApplicationTests {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";private static final Long RELEASE_SUCCESS = 1L;private int splitPoint = 500;@Autowiredprivate RedisProperties redisConfig;@Testpublic void redisLock() {ThreadTest[] threadTests = new ThreadTest[10];for (int i=0; i<10; i++) {threadTests[i] = new ThreadTest();}for (int i=0; i<10; i++) {threadTests[i].start();}}private class ThreadTest extends Thread {@Overridepublic void run() {Jedis jedis = new Jedis(redisConfig.getHost(),redisConfig.getPort(),redisConfig.getTimeout());String requestId = this.getId()+"";int i=0;while (true){i = ++i;try {if(tryGetDistributedLock(jedis,"lock-test",requestId,2000)) {System.out.println("线程:"+requestId+" 成功获得分布式锁!!!");System.out.println("当前库存:"+splitPoint);splitPoint = --splitPoint;System.out.println("线程:"+requestId+"下单成功后库存:"+splitPoint);if(releaseDistributedLock(jedis,"lock-test",requestId)) {System.out.println("线程:"+requestId+"  成功释放分布式锁!!!");}break;} else {System.out.println("线程:"+requestId+" 第"+i+"次无法获得分布式锁,继续抢锁!!!");}}catch (Exception e) {e.printStackTrace();}}}}/*** 尝试获取分布式锁* @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;}/*** 释放分布式锁* @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;}}

效果图:

通过上面的代码+效果图可以知道这十个线程启动之后都开始抢占 redis分布式锁,没有获得锁继续抢锁,蹭蹭蹭几下每个线程都准确无误滴执行了下单,减少库存操作,下面具体分析一下加锁,解锁代码

加锁代码

   /*** 尝试获取分布式锁* @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()方法生成。(本测试用例使用的是当前线程ID)

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

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

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

总的来说,执行上面的set()方法就只会导致两种结果:

  • 当前没有锁(key不存在),那么就进行加锁操作,并对锁设置个有效期,同时value表示加锁的客户端。
  • 已有锁存在,不做任何操作。

心细的童鞋就会发现了,我们的加锁代码满足我们可靠性里描述的三个条件。

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

解锁代码

 /*** 释放分布式锁* @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语言来实现呢?因为要确保上述操作是原子性的。

简单来说,就是在eval命令执行Lua代码的时候,Lua代码将被当成一个命令去执行,并且直到eval命令执行完成,Redis才会执行其他命令。

关注公众号,分享干货,讨论技术

转载于:https://www.cnblogs.com/molashaonian/p/9581038.html

分布式锁--Redis小试牛刀相关推荐

  1. 聊聊分布式锁——Redis和Redisson的方式

    聊聊分布式锁--Redis和Redisson的方式 一.什么是分布式锁 分布式~~锁,要这么念,首先得是『分布式』,然后才是『锁』 分布式:这里的分布式指的是分布式系统,涉及到好多技术和理论,包括CA ...

  2. 分布式锁-Redis解决方案和Redisson解决方案

    文章目录 1:分布式锁的概念 1:概念 2:锁/分布式锁/事务区别 2:本文使用的案例场景 1:需求 2:controller层代码 3:锁控制层代码(使用synchronized 不成功) 4:调用 ...

  3. 分布式锁-Redis红锁解决方案

    文章目录 1:分布式锁的概念 1:概念 2:锁/分布式锁/事务区别 2:本文使用的案例场景 1:需求 2:controller层代码 3:锁控制层代码(使用synchronized 不成功) 4:调用 ...

  4. 分布式锁--Redis实现

    为什么80%的码农都做不了架构师?>>>    实现原理:Redis为单进程单线程模式,采用队列模式将并发访问变成串行访问,且多客户端对Redis的连接并不存在竞争关系.redis的 ...

  5. 第六章 商品详情进阶 + redis分布式锁 + redis问题解决 + redisson + 布隆过滤器

    一.商品详情页面优化 1.1 思路 虽然咱们实现了页面需要的功能,但是考虑到该页面是被用户高频访问的,所以性能需要优化. 一般一个系统最大的性能瓶颈,就是数据库的io操作.从数据库入手也是调优性价比最 ...

  6. 分布式锁-redis、zookeeper优缺点

    redis分布式锁优缺点 缺点: 获取锁的方式简单粗暴,获取不到锁直接不断尝试获取锁,比较消耗性能: redis的设计定位决定了它的数据并不是强一致性的,在某些极端情况下,可能会出现问题.锁的模型不够 ...

  7. python并发锁,分布式锁,redis分布式锁

    关于toollib 当我们高并发时,为了确保一个方法(或代码块)同一时间只能被同一个线程执行,这时我们就需要锁来保证一致性 toollib实现了redis分布式锁.(pip install tooll ...

  8. Redis分布式锁/Redis的setnx命令如何设置key的失效时间(同时操作setnx和expire)

    Redis的setnx命令是当key不存在时设置key,但setnx不能同时完成expire设置失效时长,不能保证setnx和expire的原子性.我们可以使用set命令完成setnx和expire的 ...

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

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

最新文章

  1. 更换XP SN的vbs
  2. 一个考察函数基础知识的题
  3. ALFNet行人检测
  4. 一个不错的git学习网站
  5. Sap权限相关设置、控制及传输
  6. poj 2078(搜索+剪枝)
  7. (Redis设计与实现-6) 频道的订阅与退订
  8. linux删除之前的文件日志
  9. url 函数 php,php中url处理函数总结
  10. 【BZOJ3172】单词,AC自动机练习
  11. python服务端语言_使用Python实现简单的服务器功能
  12. 操作系统笔记(三)进程管理之管理
  13. 电脑操作精典密笈60式
  14. java 程序打包成jar_把Java程序打包成jar文件包并执行的方法
  15. itest听力答案2020_2020英语一真题和答案
  16. ubuntu美化之conky美化
  17. 为什么国外程序员爱用Mac?
  18. 腾讯全民wifi驱动无法安装
  19. Redis爬坑记(一):incr命令和expire命令的误区
  20. GEAP 遗传算法/遗传编程 genetic programming + python(deap库)实现

热门文章

  1. linux查看CPU信息
  2. DEBUGnbsp;设置监视点:判断变量值
  3. 去掉ILDasm的SuppressIldasmAttribute限制
  4. 转载:配置nginx支持pathinfo功能
  5. [转载]dorado学习笔记(二)
  6. PHP iconv()函数转字符编码的问题(转)
  7. 计算机和打印机的耗材管理,打印设备耗材余量知多少?多种途径实时掌握
  8. python中datetime模块常用方法_Python中datetime的使用和常用时间处理
  9. python unicodeencodeerror_解决 Python UnicodeEncodeError 错误
  10. git 在拉取代码的时候connect 谷歌报错_工具 | 手把手教你在VSCode中使用Git