当前位置: 首页 > 后端技术 > Java

面试官:如何实现Redis分布式锁

时间:2023-04-01 17:17:35 Java

本文已收录在github/gitee仓库。欢迎关注和stargithub仓库:https://github.com/Tyson0314/...如果不能访问github,可以访问gitee仓库。gitee仓库:https://gitee.com/tysondai/Ja...在单机环境下,当有多个线程可以同时改变一个变量(变量共享变量)时,就会出现线程安全问题。JAVA提供的并发并发下的volatile、ReentrantLock、synchronized和一些线程安全的类可以避免这个问题。在多机部署环境下,需要保证多进程下的线程安全。Java提供的这些API只能保证单个JVM进程内多线程访问共享资源的线程安全,已经不满足要求。这时候就需要使用分布式锁来保证线程安全。通过分布式锁,可以保证在分布式应用集群中,同一个方法同一时刻只能由一台机器上的一个线程执行。分布式锁需要满足四个条件:互斥。在任何时候,只有一个客户端可以持有锁。不会有僵局。即使一个client在持有锁的时候crash了,没有主动解锁,也需要保证后面其他client可以加锁。加锁和解锁必须是同一个客户端。客户端a无法解开客户端b的锁,即不能被误解锁。容错。只要大多数Redis节点启动并运行,客户端就能够获取和释放锁。Redis分布式锁常见的分布式锁实现方式有:数据库、Redis、Zookeeper。下面主要介绍使用Redis实现分布式锁。Redis2.6.12之前的版本使用setnx+expire实现分布式锁。Redis2.6.12之后,setnx增加了一个过期时间参数:SETlockKeyvalueNXPXexpire-time,所以Redis2.6.12之后,只需要使用setnx来实现分布式锁。加锁逻辑:setnx竞争key的锁。如果key已经存在,则不执行任何操作。过一会继续重试,确保只有一个客户端可以持有锁。value设置为requestId(可以用机器ip加入当前线程名),表示哪个request加了锁。解锁时需要判断当前请求是否持有锁,防止误解锁。比如客户端A加锁,在执行解锁之前,锁就过期了。此时客户端B尝试加锁成功,然后客户端A执行del()方法,客户端B的锁被释放。然后使用expire给锁加上一个过期时间,防止锁因为异常而被释放。解锁逻辑:先获取锁对应的值,检查是否与requestId相等,相等则删除锁。使用lua脚本实现原子操作,保证线程安全。下面我们使用Jedis(基于java语言的redis客户端)来演示分布式锁的实现。Jedis实现分布式锁,引入Jedisjar包,在pom.xml文件中添加代码:redis.clientsjedis2.9.0>/dependency>lock调用jedis的set()实现lock,lock代码如下:/***@description:*@author:程序员大斌*@time:2021-08-0117:13*/公共类RedisTest{privatestaticfinalStringLOCK_SUCCESS="OK";私有静态最终字符串SET_IF_NOT_EXIST="NX";privatestaticfinalStringSET_EXPIRE_TIME="PX";@Autowired私有JedisPooljedisPool;publicbooleanlocktryGetDistributedKey,StringexpireTime){Jedisjedis=jedisPool.getResource();字符串结果=jedis.set(lockKey,requestId,SET_IF_NOT_EXIST,SET_EXPIRE_TIME,expireTime);如果(LOCK_SUCCESS.equals(结果)){返回真;}返回假;}}each参数说明:lockKey:使用钥匙作为锁,需要保证钥匙的唯一性。您可以使用系统编号来连接自定义键。requestId:表示哪个请求加了锁,可以用机器ip加入当前线程名。解锁时需要判断当前请求是否持有锁,防止误解锁。比如客户端A加锁,在执行解锁之前,锁就过期了。此时客户端B尝试加锁成功,然后客户端A执行del()方法,客户端B的锁被释放。NX:表示SETIFNOTEXIST,确保如果存在key则不进行任何操作,过一会继续重试。NX参数确保只有一个客户端可以持有锁。PX:给key添加过期设置,具体时间由expireTime决定。expireTime:设置key的过期时间,防止锁因为异常而被释放。解锁首先需要获取锁对应的值,判断是否与requestId相等,相等则删除锁。这里使用lua脚本实现原子操作,保证线程安全。使用eval命令执行Lua脚本时,不会执行其他脚本或Redis命令,实现组合命令的原子操作。lua脚本如下://KEYS[1]是lockKey,ARGV[1]是requestIdStringscript="ifredis.call('get',KEYS[1])==ARGV[1]thenreturnredis.call('del',KEYS[1])elsereturn0end";Objectresult=jedis.eval(script,Collections.singletonList(lockKey),Collections.singletonList(requestId));Jedis的eval()方法源码如下:publicObjecteval(Stringscript,Listkeys,Listargs){returnthis.eval(script,keys.size(),getParams(keys,args));}Lua脚本的意思是:调用get获取锁(KEYS[1]),检查是否等于requestId(ARGV[1])对应的值,如果相等则调用del删除锁.否则返回0。完整解锁代码如下:publicclassRedisTest{privatestaticfinalLongRELEASE_SUCCESS=1L;@Autowired私有JedisPooljedisPool;publicbooleanreleaseDistributedLock(StringlockKey,StringrequestId){Jedisjedis=jedisPool.getResource();////KEYS[1]是lockKey,ARGV[1]是requestIdStringscript="ifredis.call('get',KEYS[1])==ARGV[1]thenreturnredis.call('del',KEYS[1])否则返回0end";对象结果=jedis.eval(script,Collections.singletonList(lockKey),Collections.singletonList(requestId));如果(RELEASE_SUCCESS.equals(结果)){返回真;}返回假;以上就是使用Redis实现分布式锁的全部内容,希望对大家有所帮助。码字不易,如果这篇文章写得好,可以点个赞,让我知道,支持我写出更好的文章!