redis分布式锁 目的:解决多个服务原子性的问题
在很多场景中,我们为了保证数据的最终一致性,需要很多的技术方案来支持,比如分布式事务、分布式锁等。那具体什么是分布式锁,分布式锁应用在哪些业务场景、如何来实现分布式锁呢?
锁的种类
单机版同一个jvm虚拟机内,synchronized或者Lock接口
分布式多个不同虚拟机,单机的线程锁机制不再起作用,资源类在不同的服务器之间共享了
一个靠谱分布式锁需要具备的条件和刚需
独占性
OnlyOne,任何时刻只能有且仅有一个线程持有
高可用
若redis集群环境下,不能因为某一个节点挂了而出现获取锁和释放锁失败的情况
高并发请求下,依旧性能ok好使
防死锁
杜绝死锁,必须有超时控制机制或者撤销操作,有个兜底终止跳出方案
不乱抢
不能私下unlock(解)别人的锁,只能自己加锁自己释放
重入性
同一个节点的同一个线程如何获得锁之后,它也可以再次获得这个锁。
分布式锁
案例1 使用场景:多个服务间保证同一时刻同一时间段内同一用户只能有一个请求(防止关键业务出现并发攻击)
两个服务,分别是redis_distributed_lock2和redis_distributed_lock3代码一样端口号不一样
pom
<dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-web</artifactId > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-data-redis</artifactId > </dependency > <dependency > <groupId > org.apache.commons</groupId > <artifactId > commons-pool2</artifactId > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-test</artifactId > <scope > test</scope > </dependency > <dependency > <groupId > org.projectlombok</groupId > <artifactId > lombok</artifactId > <version > 1.18.36</version > <optional > true</optional > </dependency > <dependency > <groupId > cn.hutool</groupId > <artifactId > hutool-all</artifactId > <version > 5.8.8</version > </dependency >
yaml
server: port: 7777 spring: application: name: redis_distributed_lock2 data: redis: host: 192.168 .0 .134 port: 6379 password: redis lettuce: pool: max-idle: 8 max-wait: -1ms max-active: 8 min-idle: 0 database: 0
config
package com.lazy.config;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;import org.springframework.data.redis.core.RedisTemplate;import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;import org.springframework.data.redis.serializer.StringRedisSerializer;@Configuration public class RedisConfig { @Bean public RedisTemplate<String, Object> redisTemplate (LettuceConnectionFactory lettuceConnectionFactory) { RedisTemplate<String, Object> redisTemplate = new RedisTemplate <>(); redisTemplate.setConnectionFactory(lettuceConnectionFactory); redisTemplate.setKeySerializer(new StringRedisSerializer ()); redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer ()); redisTemplate.setHashKeySerializer(new StringRedisSerializer ()); redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer ()); redisTemplate.afterPropertiesSet(); return redisTemplate; } }
service
package com.lazy.service;import jakarta.annotation.Resource;import org.springframework.beans.factory.annotation.Value;import org.springframework.data.redis.core.StringRedisTemplate;import org.springframework.stereotype.Service;import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReentrantLock;@Service public class InventoryService { @Resource private StringRedisTemplate stringRedisTemplate; @Value("${server.port}") private String port; private final Lock lock = new ReentrantLock (); public String sale () { String retMessage = "" ; lock.lock(); try { String result = stringRedisTemplate.opsForValue().get("inventory001" ); Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result); if (inventoryNumber > 0 ) { stringRedisTemplate.opsForValue().set("inventory001" ,String.valueOf(--inventoryNumber)); retMessage = "成功卖出一个商品,库存剩余: " +inventoryNumber; System.out.println(retMessage); }else { retMessage = "商品卖完了,o(╥﹏╥)o" ; } }finally { lock.unlock(); } return retMessage+"\t" +"服务端口号:" +port; } }
controller
package com.lazy.controller;import com.lazy.service.InventoryService;import jakarta.annotation.Resource;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RestController;@RestController public class InventoryController { @Resource private InventoryService inventoryService; @GetMapping("/inventory/sale") public String sale () { return inventoryService.sale(); } }
nginx配置
使用start nginx启动即可
测试:
没问题!使用压测工具同时开启100个线程访问
加了lock还是出现超卖的现象,在分布式下毫无意义,因为lock只适用于单服务!
第一次改造(递归重试) 接下来改造我们的service
package com.lazy.service;import cn.hutool.core.util.IdUtil;import jakarta.annotation.Resource;import org.springframework.beans.factory.annotation.Value;import org.springframework.data.redis.core.StringRedisTemplate;import org.springframework.stereotype.Service;import java.util.concurrent.TimeUnit;@Service public class InventoryService { @Resource private StringRedisTemplate stringRedisTemplate; @Value("${server.port}") private String port; public String sale () { String retMessage = "" ; String key = "lock" ; String value = IdUtil.simpleUUID()+":" +Thread.currentThread().getId(); Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, value); if (Boolean.FALSE.equals(flag)){ try { TimeUnit.MILLISECONDS.sleep(20 ); } catch (InterruptedException e) { throw new RuntimeException (e); } this .sale(); }else { try { String result = stringRedisTemplate.opsForValue().get("inventory001" ); Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result); if (inventoryNumber > 0 ) { stringRedisTemplate.opsForValue().set("inventory001" , String.valueOf(--inventoryNumber)); retMessage = "成功卖出一个商品,库存剩余: " + inventoryNumber; System.out.println(retMessage); } else { retMessage = "商品卖完了,o(╥﹏╥)o" ; } } finally { stringRedisTemplate.delete(key); } } return retMessage + "\t" + "服务端口号:" + port; } }
启动测试,我们要把值修改为5000,我们要使用1秒2w线程数进行压测
这是我们的锁
递归是一种思想没错,但是容易导致StackOverflowError,不太推荐,需要进一步完善,另外,高并发唤醒后推荐用white判断而不是if
第二次改造(while替代) 继续改造service
package com.lazy.service;import cn.hutool.core.util.IdUtil;import jakarta.annotation.Resource;import org.springframework.beans.factory.annotation.Value;import org.springframework.data.redis.core.StringRedisTemplate;import org.springframework.stereotype.Service;import java.util.concurrent.TimeUnit;@Service public class InventoryService { @Resource private StringRedisTemplate stringRedisTemplate; @Value("${server.port}") private String port; public String sale () { String retMessage = "" ; String key = "lock" ; String value = IdUtil.simpleUUID()+":" +Thread.currentThread().getId(); while (Boolean.FALSE.equals(stringRedisTemplate.opsForValue().setIfAbsent(key, value))) { try { TimeUnit.MILLISECONDS.sleep(20 ); } catch (InterruptedException e) { throw new RuntimeException (e); } } try { String result = stringRedisTemplate.opsForValue().get("inventory001" ); Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result); if (inventoryNumber > 0 ) { stringRedisTemplate.opsForValue().set("inventory001" , String.valueOf(--inventoryNumber)); retMessage = "成功卖出一个商品,库存剩余: " + inventoryNumber; System.out.println(retMessage); } else { retMessage = "商品卖完了,o(╥﹏╥)o" ; } } finally { stringRedisTemplate.delete(key); } return retMessage + "\t" + "服务端口号:" + port; } }
继续进行压测,压测1轮3000
问题:
部署了微服务的java程序机器挂了,代码层面根本没有走到finally这块,没办法保证解锁(无过期时间改key一直存在),这个key没有被删除,需要加入一个过期时间限定key
第三次改造(宕机与过期+防止死锁) 设置key的过期时间,设置过期时间为30秒
package com.lazy.service;import cn.hutool.core.util.IdUtil;import jakarta.annotation.Resource;import org.springframework.beans.factory.annotation.Value;import org.springframework.data.redis.core.StringRedisTemplate;import org.springframework.stereotype.Service;import java.util.concurrent.TimeUnit;@Service public class InventoryService { @Resource private StringRedisTemplate stringRedisTemplate; @Value("${server.port}") private String port; public String sale () { String retMessage = "" ; String key = "lock" ; String value = IdUtil.simpleUUID()+":" +Thread.currentThread().getId(); while (Boolean.FALSE.equals(stringRedisTemplate.opsForValue().setIfAbsent(key, value,30L , TimeUnit.SECONDS))) { try { TimeUnit.MILLISECONDS.sleep(20 ); } catch (InterruptedException e) { throw new RuntimeException (e); } } try { String result = stringRedisTemplate.opsForValue().get("inventory001" ); Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result); if (inventoryNumber > 0 ) { stringRedisTemplate.opsForValue().set("inventory001" , String.valueOf(--inventoryNumber)); retMessage = "成功卖出一个商品,库存剩余: " + inventoryNumber; System.out.println(retMessage); } else { retMessage = "商品卖完了,o(╥﹏╥)o" ; } } finally { stringRedisTemplate.delete(key); } return retMessage + "\t" + "服务端口号:" + port; } }
在进行压测
结论:
第四次改造(防止误删key的问题)
只能删除自己的,不许删除别人的锁
package com.lazy.service;import cn.hutool.core.util.IdUtil;import jakarta.annotation.Resource;import org.springframework.beans.factory.annotation.Value;import org.springframework.data.redis.core.StringRedisTemplate;import org.springframework.stereotype.Service;import java.util.Objects;import java.util.concurrent.TimeUnit;@Service public class InventoryService { @Resource private StringRedisTemplate stringRedisTemplate; @Value("${server.port}") private String port; public String sale () { String retMessage = "" ; String key = "lock" ; String value = IdUtil.simpleUUID()+":" +Thread.currentThread().getId(); while (Boolean.FALSE.equals(stringRedisTemplate.opsForValue().setIfAbsent(key, value,30L , TimeUnit.SECONDS))) { try { TimeUnit.MILLISECONDS.sleep(20 ); } catch (InterruptedException e) { throw new RuntimeException (e); } } try { String result = stringRedisTemplate.opsForValue().get("inventory001" ); Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result); if (inventoryNumber > 0 ) { stringRedisTemplate.opsForValue().set("inventory001" , String.valueOf(--inventoryNumber)); retMessage = "成功卖出一个商品,库存剩余: " + inventoryNumber; System.out.println(retMessage); } else { retMessage = "商品卖完了,o(╥﹏╥)o" ; } } finally { if (Objects.requireNonNull(stringRedisTemplate.opsForValue().get(key)).equalsIgnoreCase(value)) { stringRedisTemplate.delete(key); } } return retMessage + "\t" + "服务端口号:" + port; } }
存在问题,最后判断和删除不是原子性操作,需要使用lua脚本确保一致性操作
第五次改造(保证原子性,使用lua脚本)
Lua脚本 redis调用Lua脚本通过eval命令保证代码执行的原子性,直接用return返回脚本执行后的结果值
eval命令
EVAL script numkeys key [key ...] arg [arg ...] #numkeys 代表参数个数 #key 是键 #arg 是具体key的值
hello world入门
hello lua
127 .0 .0 .1 :6379 > EVAL "return 'hello lua'" 0 "hello lua"
set k1 v1 get k1
127 .0 .0 .1 :6379 > EVAL "redis.call ('set ','k1','v1') return redis.call ('get','k1')" 0 "v1"
mset
127 .0 .0 .1 :6379 > EVAL "return redis.call ('mset',KEYS[1 ],ARGV[1 ],KEYS[2 ],ARGV[2 ]) " 2 k1 k2 v1 v2OK 127 .0 .0 .1 :6379 > get k1"v1" 127 .0 .0 .1 :6379 > get k2"v2"
Lua脚本进一步
条件判断语法
条件判断案例
127 .0 .0 .1 :6379 > get keys"1 " 127 .0 .0 .1 :6379 > EVAL "if redis.call ('get',KEYS[1 ])==ARGV[1 ] then return redis.call ('del ',KEYS[1 ]) else return 0 end" 1 keys 2 (integer) 0 127 .0 .0 .1 :6379 > EVAL "if redis.call ('get',KEYS[1 ])==ARGV[1 ] then return redis.call ('del ',KEYS[1 ]) else return 0 end" 1 keys 1 (integer) 1
改造
package com.lazy.service;import cn.hutool.core.util.IdUtil;import jakarta.annotation.Resource;import org.springframework.beans.factory.annotation.Value;import org.springframework.data.redis.core.StringRedisTemplate;import org.springframework.data.redis.core.script.DefaultRedisScript;import org.springframework.stereotype.Service;import java.util.List;import java.util.concurrent.TimeUnit;@Service public class InventoryService { @Resource private StringRedisTemplate stringRedisTemplate; @Value("${server.port}") private String port; public String sale () { String retMessage = "" ; String key = "lock" ; String value = IdUtil.simpleUUID()+":" +Thread.currentThread().getId(); while (Boolean.FALSE.equals(stringRedisTemplate.opsForValue().setIfAbsent(key, value,30L , TimeUnit.SECONDS))) { try { TimeUnit.MILLISECONDS.sleep(20 ); } catch (InterruptedException e) { throw new RuntimeException (e); } } try { String result = stringRedisTemplate.opsForValue().get("inventory001" ); Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result); if (inventoryNumber > 0 ) { stringRedisTemplate.opsForValue().set("inventory001" , String.valueOf(--inventoryNumber)); retMessage = "成功卖出一个商品,库存剩余: " + inventoryNumber; System.out.println(retMessage); } else { retMessage = "商品卖完了,o(╥﹏╥)o" ; } } finally { String luaScript = "if redis.call('get',KEYS[1]) == ARGV[1] then " + "return redis.call('del',KEYS[1]) " + "else return 0 end" ; stringRedisTemplate.execute(new DefaultRedisScript <>(luaScript,Boolean.class), List.of(key),value); } return retMessage + "\t" + "服务端口号:" + port; } }
结果
127 .0 .0 .1 :6379 > get lock"7 f5195a7954d468aa32830d9ebedcdd6:244 " 127 .0 .0 .1 :6379 > get lock"4721213967994 ef28ce98ee6ed3a68bc:76 " 127 .0 .0 .1 :6379 > get lock"e9d20717da254f5b930fc04b6e983974:47 "
保证了程序的原子性!
第六次改造 while判断并自旋重试获得锁+setnx含自然过期时间+Lua脚本删除锁命令
问题
一个靠谱分布式锁需要具备的条件和刚需
可重入锁(又名递归锁) 是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提,锁对象得是同一个对象),不会因为之前已经获取过还没释放而阻塞。
如果是1个有 synchronized 修饰的递归调用方法,程序第2次进入被自己阻塞了岂不是天大的笑话,出现了作茧自缚。
所以Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。
可重入锁种类
隐式锁(即synchronized关键字使用的锁)默认是可重入锁
指的是可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁,这样的锁就叫做可重入锁。
简单的来说就是:在一个synchronized修饰的方法或代码块的内部调用本类的其他synchronized修饰的方法或代码块时,是永远可以得到锁的
与可重入锁相反,不可重入锁不可递归调用,递归调用就发生死锁。
同步块
package com.lazy;import org.junit.jupiter.api.Test;import org.springframework.boot.test.context.SpringBootTest;@SpringBootTest public class ReEntryLockDemo { final Object obj = new Object (); public void lockBlock () { new Thread (()->{ synchronized (obj){ System.out.println(Thread.currentThread().getName()+"---外层调用" ); synchronized (obj){ System.out.println(Thread.currentThread().getName()+"---中层调用" ); synchronized (obj){ System.out.println(Thread.currentThread().getName()+"---内层调用" ); } } } },"t1" ).start(); } @Test public void testLock () { ReEntryLockDemo reEntryLockDemo = new ReEntryLockDemo (); reEntryLockDemo.lockBlock(); } }
同步方法
package com.lazy;import org.junit.jupiter.api.Test;import org.springframework.boot.test.context.SpringBootTest;@SpringBootTest public class ReEntryLockDemo { public synchronized void lockMethod () { System.out.println(Thread.currentThread().getName()+"---外层调用" ); lockMethod2(); } public synchronized void lockMethod2 () { System.out.println(Thread.currentThread().getName()+"---中层调用" ); lockMethod3(); } public synchronized void lockMethod3 () { System.out.println(Thread.currentThread().getName()+"---内层调用" ); } @Test public void testLock () { ReEntryLockDemo reEntryLockDemo = new ReEntryLockDemo (); reEntryLockDemo.lockMethod(); } }
Synchronized的重入的实现原理
每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针。
当执行monitorenter时,如果目标锁对象的计数器为零,那么说明它没有被其他线程所持有,Java虚拟机会将该锁对象的持有线程设置为当前线程,并且将其计数器加1。
在目标锁对象的计数器不为零的情况下,如果锁对象的持有线程是当前线程,那么Java虚拟机可以将其计数器加1,否则需要等待,甚至持有线程释放该锁。
当执行monitorexit时,Java虚拟机则需将锁对象的计数器减1。计数器为零代表锁已被释放。
显示锁(即Lock)也有ReentrantLock这样的可重入锁。
public void lock () { new Thread (()->{ myLock.lock(); try { System.out.println(Thread.currentThread().getName()+"---外层调用" ); myLock.lock(); try { System.out.println(Thread.currentThread().getName()+"---中层调用" ); myLock.lock(); try { System.out.println(Thread.currentThread().getName()+"---内层调用" ); }finally { myLock.unlock(); } }finally { myLock.unlock(); } }finally { myLock.unlock(); } },"t1" ).start(); }
切记,一般而言,lock了几次就要unlock几次。
上述可重入锁问题,redis中那个数据类型可以代替?
思考+设计模式(一横一纵)
Lua脚本
redis命令过程分析
127 .0 .0 .1 :6379 > EXISTS lock(integer) 0 127 .0 .0 .1 :6379 > HSET lock 1 a2b3c 1 (integer) 1 127 .0 .0 .1 :6379 > HINCRBY lock 1 a2b3c 1 (integer) 2 127 .0 .0 .1 :6379 > HGET lock 1 a2b3c"2 "
加锁Lua脚本lock
先判断redis分布式锁这个key是否存在
上述设计修改为Lua脚本
v1
if redis.call ('exists','key') == 0 then redis.call ('hset','key','uuid:threadid',1 ) redis.call ('expire','key',30 ) return 1 elseif redis.call ('hexists','key','uuid:threadid') == 1 then redis.call ('hincrby','key','uuid:threadid',1 ) redis.call ('expire','key',30 ) return 1 else return 0 end
相同部分是否可以替换处理???
hincrby命令可否替代hset命令
v2
if redis.call ('exists','key') == 0 or redis.call ('hexists','key','uuid:threadid') == 1 then redis.call ('hincrby','key','uuid:threadid',1 ) redis.call ('expire','key',30 ) return 1 else return 0 end
v3
key
KEYS[1]
lock
value
ARGV[1]
2f586ae740a94736894ab9d51880ed9d:1
过期时间值
ARGV[2]
30 秒
if redis.call ('exists',KEYS[1 ]) == 0 or redis.call ('hexists',KEYS[1 ],ARGV[1 ]) == 1 then redis.call ('hincrby',KEYS[1 ],ARGV[1 ],1 ) redis.call ('expire',KEYS[1 ],ARGV[2 ]) return 1 else return 0 end
解锁Lua脚本unlock
第七次改造(确保可重入性和原子性) RedisDistributedLock
package com.lazy.locks;import cn.hutool.core.util.IdUtil;import org.springframework.data.redis.core.StringRedisTemplate;import org.springframework.data.redis.core.script.DefaultRedisScript;import java.util.Arrays;import java.util.concurrent.TimeUnit;import java.util.concurrent.locks.Condition;import java.util.concurrent.locks.Lock;public class RedisDistributedLock implements Lock { private StringRedisTemplate stringRedisTemplate; private String lockName; private String uuidValue; private long expireTime; public RedisDistributedLock (StringRedisTemplate stringRedisTemplate, String lockName) { this .stringRedisTemplate = stringRedisTemplate; this .lockName = lockName; this .uuidValue = IdUtil.simpleUUID()+":" +Thread.currentThread().getId(); this .expireTime = 30L ; } @Override public void lock () { tryLock(); } @Override public boolean tryLock () { try {tryLock(-1L ,TimeUnit.SECONDS);} catch (InterruptedException e) {e.printStackTrace();} return false ; } @Override public boolean tryLock (long time, TimeUnit unit) throws InterruptedException{ if (time != -1L ){ this .expireTime = unit.toSeconds(time); } String script = "if redis.call('exists',KEYS[1]) == 0 or redis.call('hexists',KEYS[1],ARGV[1]) == 1 then " + "redis.call('hincrby',KEYS[1],ARGV[1],1) " + "redis.call('expire',KEYS[1],ARGV[2]) " + "return 1 " + "else " + "return 0 " + "end" ; System.out.println("script: " +script); System.out.println("lockName: " +lockName); System.out.println("uuidValue: " +uuidValue); System.out.println("expireTime: " +expireTime); while (Boolean.FALSE.equals(stringRedisTemplate.execute(new DefaultRedisScript <>(script, Boolean.class), Arrays.asList(lockName), uuidValue, String.valueOf(expireTime)))) { TimeUnit.MILLISECONDS.sleep(50 ); } return true ; } @Override public void unlock () { String script = "if redis.call('HEXISTS',KEYS[1],ARGV[1]) == 0 then " + " return nil " + "elseif redis.call('HINCRBY',KEYS[1],ARGV[1],-1) == 0 then " + " return redis.call('del',KEYS[1]) " + "else " + " return 0 " + "end" ; System.out.println("lockName: " +lockName); System.out.println("uuidValue: " +uuidValue); System.out.println("expireTime: " +expireTime); Long flag = stringRedisTemplate.execute(new DefaultRedisScript <>(script, Long.class), Arrays.asList(lockName),uuidValue,String.valueOf(expireTime)); if (flag == null ) { throw new RuntimeException ("This lock doesn't EXIST" ); } } @Override public void lockInterruptibly () throws InterruptedException { } @Override public Condition newCondition () { return null ; } }
service
package com.lazy.service;import cn.hutool.core.util.IdUtil;import com.lazy.locks.RedisDistributedLock;import jakarta.annotation.Resource;import org.springframework.beans.factory.annotation.Value;import org.springframework.data.redis.core.StringRedisTemplate;import org.springframework.data.redis.core.script.DefaultRedisScript;import org.springframework.stereotype.Service;import java.util.List;import java.util.concurrent.locks.Lock;@Service public class InventoryService { @Resource private StringRedisTemplate stringRedisTemplate; @Value("${server.port}") private String port; private final Lock lock = new RedisDistributedLock (stringRedisTemplate,"lock" ); public String sale () { String retMessage = "" ; lock.lock(); try { String result = stringRedisTemplate.opsForValue().get("inventory001" ); Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result); if (inventoryNumber > 0 ) { stringRedisTemplate.opsForValue().set("inventory001" ,String.valueOf(--inventoryNumber)); retMessage = "成功卖出一个商品,库存剩余: " +inventoryNumber; System.out.println(retMessage); }else { retMessage = "商品卖完了,o(╥﹏╥)o" ; } }finally { lock.unlock(); } return retMessage+"服务端口号:" +port; } }
为了有更好的兼容性我们引入了工程设计模式
DistributedLockFactory
package com.lazy.factory;import com.lazy.locks.RedisDistributedLock;import jakarta.annotation.Resource;import org.springframework.data.redis.core.StringRedisTemplate;import org.springframework.stereotype.Component;import java.util.concurrent.locks.Lock;@Component public class DistributedFactory { @Resource private StringRedisTemplate stringRedisTemplate; private String lockName; public Lock getDistributedLock (String lockType) { if (lockType == null ) return null ; if (lockType.equalsIgnoreCase("redis" )){ this .lockName = "redisLock" ; return new RedisDistributedLock (stringRedisTemplate,lockName); }else if (lockType.equalsIgnoreCase("mysql" )){ return null ; }else if (lockType.equalsIgnoreCase("zookeeper" )){ return null ; } return null ; } }
service
package com.lazy.service;import com.lazy.factory.DistributedFactory;import jakarta.annotation.Resource;import org.springframework.beans.factory.annotation.Value;import org.springframework.data.redis.core.StringRedisTemplate;import org.springframework.stereotype.Service;import java.util.concurrent.locks.Lock;@Service public class InventoryService { @Resource private StringRedisTemplate stringRedisTemplate; @Value("${server.port}") private String port; @Resource private DistributedFactory distributedFactory; public String sale () { Lock redisLock = distributedFactory.getDistributedLock("redis" ); String retMessage = "" ; redisLock.lock(); try { String result = stringRedisTemplate.opsForValue().get("inventory001" ); Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result); if (inventoryNumber > 0 ) { stringRedisTemplate.opsForValue().set("inventory001" ,String.valueOf(--inventoryNumber)); retMessage = "成功卖出一个商品,QD库存剩余: " +inventoryNumber; System.out.println(retMessage); }else { retMessage = "商品卖完了,o(╥﹏╥)o" ; } }finally { redisLock.unlock(); } return retMessage+"服务端口号:" +port; } }
看着好像没问题
可重入测试重点 我们在service增加一个方法试试
package com.lazy.service;import com.lazy.factory.DistributedFactory;import jakarta.annotation.Resource;import org.springframework.beans.factory.annotation.Value;import org.springframework.data.redis.core.StringRedisTemplate;import org.springframework.stereotype.Service;import java.util.concurrent.locks.Lock;@Service public class InventoryService { @Resource private StringRedisTemplate stringRedisTemplate; @Value("${server.port}") private String port; @Resource private DistributedFactory distributedFactory; public String sale () { Lock redisLock = distributedFactory.getDistributedLock("redis" ); String retMessage = "" ; redisLock.lock(); try { String result = stringRedisTemplate.opsForValue().get("inventory001" ); Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result); if (inventoryNumber > 0 ) { stringRedisTemplate.opsForValue().set("inventory001" ,String.valueOf(--inventoryNumber)); retMessage = "成功卖出一个商品,QD库存剩余: " +inventoryNumber; testLock(); System.out.println(retMessage); }else { retMessage = "商品卖完了,o(╥﹏╥)o" ; } }finally { redisLock.unlock(); } return retMessage+"服务端口号:" +port; } public void testLock () { Lock redisLock = distributedFactory.getDistributedLock("redis" ); redisLock.lock(); try { System.out.println("测试可重入性方法" ); }finally { redisLock.unlock(); } } }
启动测试试试
DistributedLockFactory
package com.lazy.factory;import cn.hutool.core.util.IdUtil;import com.lazy.locks.RedisDistributedLock;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.data.redis.core.StringRedisTemplate;import org.springframework.stereotype.Component;import java.util.concurrent.locks.Lock;@Component public class DistributedLockFactory { @Autowired private StringRedisTemplate stringRedisTemplate; private String lockName; private String uuidValue; public DistributedLockFactory () { this .uuidValue = IdUtil.simpleUUID(); } public Lock getDistributedLock (String lockType) { if (lockType == null ) return null ; if (lockType.equalsIgnoreCase("REDIS" )){ lockName = "zzyyRedisLock" ; return new RedisDistributedLock (stringRedisTemplate,lockName,uuidValue); } else if (lockType.equalsIgnoreCase("ZOOKEEPER" )){ return null ; } else if (lockType.equalsIgnoreCase("MYSQL" )){ return null ; } return null ; } }
RedisDistributedLock
package com.lazy.locks;import org.springframework.data.redis.core.StringRedisTemplate;import org.springframework.data.redis.core.script.DefaultRedisScript;import java.util.Arrays;import java.util.concurrent.TimeUnit;import java.util.concurrent.locks.Condition;import java.util.concurrent.locks.Lock;public class RedisDistributedLock implements Lock { private StringRedisTemplate stringRedisTemplate; private String lockName; private String uuidValue; private long expireTime; public RedisDistributedLock (StringRedisTemplate stringRedisTemplate, String lockName,String uuidValue) { this .stringRedisTemplate = stringRedisTemplate; this .lockName = lockName; this .uuidValue = uuidValue+":" +Thread.currentThread().getId(); this .expireTime = 30L ; } @Override public void lock () { this .tryLock(); } @Override public boolean tryLock () { try { return this .tryLock(-1L ,TimeUnit.SECONDS); } catch (InterruptedException e) { e.printStackTrace(); } return false ; } @Override public boolean tryLock (long time, TimeUnit unit) throws InterruptedException { if (time != -1L ) { expireTime = unit.toSeconds(time); } String script = "if redis.call('exists',KEYS[1]) == 0 or redis.call('hexists',KEYS[1],ARGV[1]) == 1 then " + "redis.call('hincrby',KEYS[1],ARGV[1],1) " + "redis.call('expire',KEYS[1],ARGV[2]) " + "return 1 " + "else " + "return 0 " + "end" ; System.out.println("lockName: " +lockName+"\t" +"uuidValue: " +uuidValue); while (!stringRedisTemplate.execute(new DefaultRedisScript <>(script, Boolean.class), Arrays.asList(lockName), uuidValue, String.valueOf(expireTime))) { try { TimeUnit.MILLISECONDS.sleep(60 ); } catch (InterruptedException e) { e.printStackTrace(); } } return true ; } @Override public void unlock () { String script = "if redis.call('HEXISTS',KEYS[1],ARGV[1]) == 0 then " + "return nil " + "elseif redis.call('HINCRBY',KEYS[1],ARGV[1],-1) == 0 then " + "return redis.call('del',KEYS[1]) " + "else " + "return 0 " + "end" ; System.out.println("lockName: " +lockName+"\t" +"uuidValue: " +uuidValue); Long flag = stringRedisTemplate.execute(new DefaultRedisScript <>(script, Long.class), Arrays.asList(lockName), uuidValue, String.valueOf(expireTime)); if (flag == null ) { throw new RuntimeException ("没有这个锁,HEXISTS查询无" ); } } @Override public void lockInterruptibly () throws InterruptedException { } @Override public Condition newCondition () { return null ; } }
测试通过!
第八次改造(添加自动续期) 确保redisLock过期时间大于业务执行时间的问题,在第一次改造上修改
service
package com.lazy.service;import com.lazy.factory.DistributedLockFactory;import jakarta.annotation.Resource;import org.springframework.beans.factory.annotation.Value;import org.springframework.data.redis.core.StringRedisTemplate;import org.springframework.stereotype.Service;import java.util.concurrent.TimeUnit;import java.util.concurrent.locks.Lock;@Service public class InventoryService { @Resource private StringRedisTemplate stringRedisTemplate; @Value("${server.port}") private String port; @Resource private DistributedLockFactory distributedFactory; public String sale () { Lock redisLock = distributedFactory.getDistributedLock("redis" ); String retMessage = "" ; redisLock.lock(); try { String result = stringRedisTemplate.opsForValue().get("inventory001" ); Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result); if (inventoryNumber > 0 ) { stringRedisTemplate.opsForValue().set("inventory001" ,String.valueOf(--inventoryNumber)); retMessage = "成功卖出一个商品,QD库存剩余: " +inventoryNumber; try { TimeUnit.SECONDS.sleep(120 ); } catch (InterruptedException e) { throw new RuntimeException (e); } }else { retMessage = "商品卖完了,o(╥﹏╥)o" ; } }finally { redisLock.unlock(); } return retMessage+"服务端口号:" +port; } }
RedisDistributedLock
package com.lazy.locks;import org.springframework.data.redis.core.StringRedisTemplate;import org.springframework.data.redis.core.script.DefaultRedisScript;import java.util.*;import java.util.concurrent.TimeUnit;import java.util.concurrent.locks.Condition;import java.util.concurrent.locks.Lock;public class RedisDistributedLock implements Lock { private final StringRedisTemplate stringRedisTemplate; private final String lockName; private final String uuidValue; private long expireTime; public RedisDistributedLock (StringRedisTemplate stringRedisTemplate, String lockName, String uuidValue) { this .stringRedisTemplate = stringRedisTemplate; this .lockName = lockName; this .uuidValue = uuidValue + ":" + Thread.currentThread().getId(); this .expireTime = 30L ; } @Override public void lock () { this .tryLock(); } @Override public boolean tryLock () { try { return this .tryLock(-1L , TimeUnit.SECONDS); } catch (InterruptedException e) { e.printStackTrace(); } return false ; } @Override public boolean tryLock (long time, TimeUnit unit) throws InterruptedException { if (time != -1L ) { expireTime = unit.toSeconds(time); } String script = "if redis.call('exists',KEYS[1]) == 0 or redis.call('hexists',KEYS[1],ARGV[1]) == 1 then " + "redis.call('hincrby',KEYS[1],ARGV[1],1) " + "redis.call('expire',KEYS[1],ARGV[2]) " + "return 1 " + "else " + "return 0 " + "end" ; System.out.println("lockName: " + lockName + "\t" + "uuidValue: " + uuidValue); while (!stringRedisTemplate.execute(new DefaultRedisScript <>(script, Boolean.class), Collections.singletonList(lockName), uuidValue, String.valueOf(expireTime))) { try { TimeUnit.MILLISECONDS.sleep(60 ); } catch (InterruptedException e) { e.printStackTrace(); } } renewExpire(); return true ; } @Override public void unlock () { String script = "if redis.call('HEXISTS',KEYS[1],ARGV[1]) == 0 then " + "return nil " + "elseif redis.call('HINCRBY',KEYS[1],ARGV[1],-1) == 0 then " + "return redis.call('del',KEYS[1]) " + "else " + "return 0 " + "end" ; System.out.println("lockName: " + lockName + "\t" + "uuidValue: " + uuidValue); Long flag = stringRedisTemplate.execute(new DefaultRedisScript <>(script, Long.class), Collections.singletonList(lockName), uuidValue, String.valueOf(expireTime)); if (flag == null ) { throw new RuntimeException ("没有这个锁,HEXISTS查询无" ); } } private void renewExpire () { String script = "if redis.call('HEXISTS',KEYS[1],ARGV[1]) == 1 then " + "return redis.call('expire',KEYS[1],ARGV[2]) " + "else " + "return 0 " + "end" ; new Timer ().schedule(new TimerTask () { @Override public void run () { if (Boolean.TRUE.equals(stringRedisTemplate.execute(new DefaultRedisScript <>(script, Boolean.class), Arrays.asList(lockName), uuidValue, String.valueOf(expireTime)))) { renewExpire(); } } },(this .expireTime * 1000 )/3 ); } @Override public void lockInterruptibly () throws InterruptedException { } @Override public Condition newCondition () { return null ; } }
效果!
总结
第一版:synchronized单机版OK,上分布式死翘翘
第二版:nginx分布式微服务单机锁不行
第三版:取消单机锁改为上redis分布式锁setnx
只加了锁,没有释放锁,出异常的话,可能无法释放锁,必须要在代码层面finally释放锁
宕机了,部署了微服务代码层面根本没有走到finally这块,没办法保证解锁,这个key没有被删除,需要有lockKey的过期时间设定
第四版:为redis的分布式锁key,增加过期时间,此外,还必须要setnx+过期时间必须同一行
第五版:必须规定只能删除自己的锁,不能删除别人的锁
第六版:unlock变为lua脚本保证
第七版:锁重入,hset替代setnx+lock变为lua脚本保证
第八版:自动续期