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

Redis实现分布式锁+执行lua脚本

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

Redis实现分布式锁+执行lua脚本Lock->uuid控制锁误删->lua脚本控制锁删除的原子分布式锁,即分布式系统中的锁。在单体应用中,我们使用锁来解决共享资源的访问控制问题,分布式锁解决分布式系统中共享资源的访问控制问题。与单体应用不同,分布式系统中竞争共享资源的最小粒度从线程升级为进程。假设现在redis中有5000个iphone14产品,我们通过扣除5000个iphone案例,逐步改进分布式实现,这样可以更好的理解这些改进的原因。setiphone1450001无分布式锁控制无锁控制时,多个线程会同时获取stock,然后扣除,会造成并发问题Stringstock=stringRedisTemplate.opsForValue().get("iphone14");if(stock!=null){//3.比较并扣除股票多头stockCount=Long.parseLong(stock);if(stockCount>0){//4.设置库存stringRedisTemplate.opsForValue().set("iphone14",String.valueOf(--stockCount));}}ab测试发现严重超卖问题ab-c100-n5000http://127.0.0.1:10010/deduct2。使用setnx命令通过redis的setnx命令加锁。这个命令的意思是如果key不存在只有存在才设置,模拟别人不抢到锁就加锁的意思。下面是使用setnx命令的分布式实现,看看会出现什么问题?publicvoiddeduct(){//1.获取redis锁while(Boolean.FALSE.equals(stringRedisTemplate.opsForValue().setIfAbsent("lock","lockvalue"))){try{TimeUnit.毫秒。睡觉(50);}catch(InterruptedExceptione){e.打印堆栈跟踪();}}try{//2.获取库存Stringstock=stringRedisTemplate.opsForValue().get("iphone14");if(stock!=null){//3.比较并扣除库存longstockCount=Long.parseLong(stock);if(stockCount>0){//4.设置库存stringRedisTemplate.opsForValue().set("iphone14",String.valueOf(--stockCount));}}}finally{//5.释放锁stringRedisTemplate.delete("lock");}问题(没有设置过期时间)可以看到我们在finally中释放了锁,但是如果机器在执行finally之前崩溃了,锁会一直存在导致没有释放导致死锁,所以需要为锁添加到期时间改进while(Boolean.FALSE.equals(stringRedisTemplate.opsForValue().setIfAbsent("lock","lockvalue"))){try{TimeUnit.MILLISECONDS.睡觉(50);}catch(InterruptedExceptione){e.printStackTrace();}}//给锁加一个过期时间???问题:就在这行代码即将执行的时候,崩溃了,出现了死锁stringRedisTemplate.expire("锁定",30,TimeUnit.SECONDS);虽然在获取到锁之后就给锁设置了过期时间,但是如果刚刚获取到锁,机器就死机了,同样会造成死锁。我们会发现我们需要在设置锁的时候同时设置过期时间,这两个操作需要是原子的,那么setnx命令就不适合了,需要使用set命令3.改用set命令setnx(以确保原子性)。从2.6.12版本开始,redis在SET命令中增加了一系列选项:EXseconds设置key的过期时间,单位是秒PXmilliseconds设置key的过期时间,单位是毫秒NX只有当key不存在,可以设置key的值XX只有当key存在时才设置key的值。set命令支持NXXX判断。NX表示不存在则设置,同时支持设置redistemplate对应的过期时间。方法是setifAbsent是NXsetIfPresent对应XX//保证设置key和过期时间这两条命令的原子性while(Boolean.FALSE.equals(stringRedisTemplate.opsForValue().setIfAbsent("lock","lockvalue",3,TimeUnit.SECONDS))){尝试{TimeUnit.MILLISECONDS.sleep(50);}catch(InterruptedExceptione){e.printStackTrace();}}try{//2.获取股票Stringstock=stringRedisTemplate.opsForValue().get("iphone14");如果(股票!=null){//3.比较扣除库存longstockCount=Long.parseLong(stock);if(stockCount>0){//4.设置库存stringRedisTemplate.opsForValue().set("iphone14",String.valueOf(--stockCount));}}}finally{//5.释放锁stringRedisTemplate.delete("lock");}问题(误删锁)从上面的代码可以发现,如果某个线程获取到锁并且由于某些原因导致业务执行缓慢,导致锁在过期时间后自动释放,然后其他线程会获取锁,然后第一个线程执行完成后会删除锁,此时的锁已经是其他的了4.添加UUID防止误删锁这里简单演示一下添加uuid防止误删其他线程锁Stringuuid=UUID.randomUUID().toString();while(Boolean.FALSE.equals(stringRedisTemplate.opsForValue().setIfAbsent("lock",uuid,3,TimeUnit.SECONDS))){try{TimeUnit.MILLISECONDS.sleep(50);}catch(InterruptedExceptione){e.printStackTrace();}}......finally{//5.释放锁,判断锁的uuid,判断是否是自己的锁StringlockValue=stringRedisTemplate.opsForValue().get("lock");如果(lockValue.equals(uuid)){stringRedisTemplate.delete("锁");}}问题(误删锁的原子性问题)可以发现还是有问题,虽然我们已经比较了这个锁是不是自己的,但是还是有可能刚刚比较完equals之后,这个锁在它失效并被阻塞的情况下,其他线程又加了新的锁,这时候还是有误删的可能。5、lua脚本控制删除锁的原子性。Redis为lua脚本留了一个洞。通过eval命令运行lua脚本,以原子方式执行lua脚本中的lua逻辑。脚本很多就不一一介绍了。你只需要知道和理解下面的代码。也可以通过redis.call('set','name','johnny')调用redis命令好吧,我们来看看如何通过lua脚本来控制删除锁的原子性5.1lua脚本删除锁逻辑ifredis。call('get','lock')==uuidthenredis.call('del','lock')return1elsereturn0end如果此时redis中锁的uuid=91dbc829-d44e-4f03-96d8-95a06f3ff975转换为可执行文件ifredis.call('get',KEYS[1])==ARGV[1]thenredis.call('del',KEYS[1])return1elsereturn0end1lock91dbc829-d44e-4f03-96d8-95a06f3ff975**5.2redistemplate执行lua脚本实现privatefinalStringdeleteLockLua="ifredis.call('get',KEYS[1])==ARGV[1]thenredis.call('del',KEYS[1])return1elsereturn0end";publicvoiddeduct(){//1.获取redis锁Stringuuid=UUID.randomUUID().toString();while(Boolean.FALSE.equals(stringRedisTemplate.opsForValue().setIfAbsent("lock",uuid,3,TimeUnit.SECONDS))){try{TimeUnit.MILLISECONDS.sleep(50);}catch(InterruptedExceptione){e.printStackTrace();}}try{//2.获取股票Stringstock=stringRedisTemplate.opsForValue().get("iphone14");if(stock!=null){//3.比较扣除股票多头stockCount=Long.parseLong(stock);if(stockCount>0){//4.设置库存stringRedisTemplate.opsForValue().set("iphone14",String.valueOf(--stockCount));}}}finally{//通过DefaultRedisScript执行lua脚本DefaultRedisScriptredisScript=newDefaultRedisScript<>();//boolean对应lua脚本返回的01redisScript.setResultType(Boolean.class);//指定要执行的lua脚本redisScript.setScriptText(deleteLockLua);//5.释放锁//注意需要提供List键,Object...args代表键和ARGV字符串RedisTemplate.execute(redisScript,Collections.singletonList("lock"),uuid);}}压测发现库存正常控制总结本文主要是一步步演化手写redis分布式锁的实现,包括setnx->设置->过期时间->意外锁删除->uuid控制锁删除->lua脚本控制锁删除的原子性等,其实还是有问题的,包括锁更新和redis的可重入锁的问题有机会改善。在redis中需要注意如何使用lua脚本,因为有些原子操作需要lua脚本来控制,包括redission框架也是通过lua脚本实现的。1.分布式锁独占性:setnx防止死锁,需要设置过期时间过期时间和设置key不是原子的,所以需要缓存setkeyvex20nx命令,防止误删。添加uuid删除不是原子的,所以引入lua脚本解决原子删除问题ifredis.call('get','lock')==uuidthenredis.call('del','lock')return1elsereturn0endreentrantunlockifredis.call('hexists','lock','uuid')==0然后返回nilelseifredis.call('hincrby','lock','uuid',-1)==0thenreturnredis.call('del','lock')elsereturn0如果redis.call('exists','lock')==0或者redis.call('hexists','lock','uuid')==1thenredis.call('hincrby','uuid',1)redis.call('expire','lock',30)return1elsereturn0end欢迎大家访问我的个人博客JohnnyHut欢迎关注我公众号