Redis分布式锁
- 👏作者简介:大家好,我是爱吃芝士的土豆倪,24届校招生Java选手,很高兴认识大家
- 📕系列专栏:Spring源码、JUC源码、Kafka原理、分布式技术原理、数据库技术
- 🔥如果感觉博主的文章还不错的话,请👍三连支持👍一下博主哦
- 🍂博主正在努力完成2023计划中:源码溯源,一探究竟
- 📝联系方式:nhs19990716,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬👀
文章目录
Redis分布式锁
经典面试题
Redis除了拿来做缓存,还可以用来做什么?:
- 分布式session
- 分布式锁
- 全局ID
- 点赞
- 位统计
- 差集交集并集,用户关注,可能认识的人,推荐模型
- 热点新闻,热搜排行榜
Redis做分布式锁的时候有需要注意的问题吗?
你们公司自己实现的分布式锁是否用setnx实现?这个是最合适的吗?你如何考虑分布式锁的可重入性?
Redis分布式锁如何续期?看门狗知道吗?
锁的种类
单机版同一个JVM虚拟机内,synchronized或者Lock接口
分布式多个不同JVM虚拟机,单机的线程锁机制不再起作用,资源类在不同的服务器之间共享了
靠谱的分布式锁需要具备的条件和刚需
独占性
OnlyOne,任何时刻只能有且仅有一个线程持有
高可用
若Redis集群环境下,不能因为某一个节点挂了而出现获取锁和释放锁失败的情况,高并发请求下,依旧性能OK好使
防死锁
杜绝死锁,必须有超时控制或者撤销操作,有个兜底终止跳出方案
不乱抢
防止张冠李戴,不能私下unlock别人的锁,只能自己加锁自己释放,自己约的锁自己含泪也要自己解
重入性
同一个节点的同一个线程如果获得锁之后,它也可以再次获取这个锁
分布式锁
setnx key value
差评,setnx+expire不安全,两条命令非原子性的
set key value [EX seconds] [PX millseconds] [NX|XX]
重点
JUC中的AQS锁的规范落地参考 + 可重入锁考虑 + Lua脚本 + Redis命令一步步实现分布式锁
Base案例(boot + redis)
使用场景:多个服务间保证同一时刻同一时间段内同一用户只能有一个请求(防止业务出现并发攻击)
InventoryService
@Service
@Slf4j
public class InventoryService
{
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Value("${server.port}")
private String port;
private Lock lock = new ReentrantLock();
public String sale()
{
String retMessage = "";
lock.lock();
try
{
//1 查询库存信息
String result = stringRedisTemplate.opsForValue().get("inventory001");
//2 判断库存是否足够
Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
//3 扣减库存
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;
}
}
InventoryController
@RestController
@Api(tags = "redis分布式锁测试")
public class InventoryController
{
@Autowired
private InventoryService inventoryService;
@ApiOperation("扣减库存,一次卖一个")
@GetMapping(value = "/inventory/sale")
public String sale()
{
return inventoryService.sale();
}
}
这段代码算是初始版本,加了synchronized或者lock
nginx分布式微服务架构
v2.0版本分布式部署后,单机锁害死出现超卖现象,需要分布式锁
修改nginx上的配置文件 /usr/local/nginx/conf 目录下修改配置文件nginx.conf新增反向代理和负载均衡
启动配置两个InventoryService 分别在7777 和 8888端口
通过Nginx访问,你的linux服务器地址ip,反向代理 + 负载均衡
采用jmeter来模拟高并发
共有100个商品
发现76号商品被卖出两次,出现超卖故障现象
但是为什么加了synchronized或者lock还是没有控制住呢?
在单机环境下,可以使用synchronized或Lock来实现。
但是在分布式系统中,因为竞争的线程可能不在同一个节点上(同一个jvm中),
所以需要一个让所有进程都能访问到的锁来实现(比如redis或者zookeeper来构建)
不同进程jvm层面的锁就不管用了,那么可以利用第三方的一个组件,来获取锁,未获取到锁,则阻塞当前想要运行的线程
分布式锁的出现,能够跨进程+跨服务、解决超卖、防止缓存击穿
解决
redis分布式锁
@Service
@Slf4j
public class InventoryService
{
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Value("${server.port}")
private String port;
private Lock lock = new ReentrantLock();
public String sale()
{
String retMessage = "";
String key = "zzyyRedisLock";
String uuidValue = IdUtil.simpleUUID()+":"+Thread.currentThread().getId();
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, uuidValue);
if(!flag){
//暂停20毫秒后递归调用
try { TimeUnit.MILLISECONDS.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); }
sale();
}else{
try{
//1 查询库存信息
String result = stringRedisTemplate.opsForValue().get("inventory001");
//2 判断库存是否足够
Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
//3 扣减库存
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;
}
}
通过递归重试的方式,但是会有问题就是,测试手工OK,测试Jmeter压测5000OK
递归是一种思想没错,但是容易StackOverflowError,不太推荐,需要进一步完善
@Service
@Slf4j
public class InventoryService
{
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Value("${server.port}")
private String port;
private Lock lock = new ReentrantLock();
public String sale()
{
String retMessage = "";
String key = "zzyyRedisLock";
String uuidValue = IdUtil.simpleUUID()+":"+Thread.currentThread().getId();
while(!stringRedisTemplate.opsForValue().setIfAbsent(key, uuidValue)){
//暂停20毫秒,类似CAS自旋
try { TimeUnit.MILLISECONDS.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); }
}
try
{
//1 查询库存信息
String result = stringRedisTemplate.opsForValue().get("inventory001");
//2 判断库存是否足够
Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
//3 扣减库存
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;
}
}
可以使用自旋来替代递归重试
宕机与过期+防止死锁
部署了微服务的java程序机器挂了,代码层面根本没有走到finally这块,没办法保证解锁(无过期时间key一直存在)这个key没有被删除,需要加入一个过期时间限定key
初步这样设计
while(!stringRedisTemplate.opsForValue().setIfAbsent(key, uuidValue))
{
//暂停20毫秒,进行递归重试.....
try { TimeUnit.MILLISECONDS.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); }
}
stringRedisTemplate.expire(key,30L,TimeUnit.SECONDS);
// 请大家思考可以这么操作吗?
设置key + 过期时间分开了,必须要合并成一行具备原子性
解决
@Service
@Slf4j
public class InventoryService
{
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Value("${server.port}")
private String port;
private Lock lock = new ReentrantLock();
public String sale()
{
String retMessage = "";
String key = "zzyyRedisLock";
String uuidValue = IdUtil.simpleUUID()+":"+Thread.currentThread().getId();
while(!stringRedisTemplate.opsForValue().setIfAbsent(key, uuidValue,30L,TimeUnit.SECONDS))
{
//暂停毫秒
try { TimeUnit.MILLISECONDS.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); }
}
try
{
//1 查询库存信息
String result = stringRedisTemplate.opsForValue().get("inventory001");
//2 判断库存是否足够
Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
//3 扣减库存
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的问题
实际业务处理时间如果超过了默认设置key的过期时间?
那么就会出现张冠李戴,删除了别人的锁
那么就需要做成,自己删除自己的,不许动别人的
@Service
@Slf4j
public class InventoryService
{
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Value("${server.port}")
private String port;
private Lock lock = new ReentrantLock();
public String sale()
{
String retMessage = "";
String key = "zzyyRedisLock";
String uuidValue = IdUtil.simpleUUID()+":"+Thread.currentThread().getId();
while(!stringRedisTemplate.opsForValue().setIfAbsent(key, uuidValue,30L,TimeUnit.SECONDS))
{
//暂停毫秒
try { TimeUnit.MILLISECONDS.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); }
}
try
{
//1 查询库存信息
String result = stringRedisTemplate.opsForValue().get("inventory001");
//2 判断库存是否足够
Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
//3 扣减库存
if(inventoryNumber > 0) {
stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNumber));
retMessage = "成功卖出一个商品,库存剩余: "+inventoryNumber+"\t"+uuidValue;
System.out.println(retMessage);
}else{
retMessage = "商品卖完了,o(╥﹏╥)o";
}
}finally {
// v5.0判断加锁与解锁是不是同一个客户端,同一个才行,自己只能删除自己的锁,不误删他人的
if(stringRedisTemplate.opsForValue().get(key).equalsIgnoreCase(uuidValue)){
stringRedisTemplate.delete(key);
}
}
return retMessage+"\t"+"服务端口号:"+port;
}
}
Lua保证原子性
前面有一个问题就是 finally块的判断 + del删除操作不是原子性的
为了保证原子性,需要启用lua脚本编写redis分布式锁判断 + 删除判断代码
Redis调用Lua脚本通过eval命令保证代码执行的原子性,直接用return返回脚本执行后的结果值
解决
@Service
@Slf4j
public class InventoryService
{
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Value("${server.port}")
private String port;
private Lock lock = new ReentrantLock();
public String sale()
{
String retMessage = "";
String key = "zzyyRedisLock";
String uuidValue = IdUtil.simpleUUID()+":"+Thread.currentThread().getId();
while(!stringRedisTemplate.opsForValue().setIfAbsent(key, uuidValue,30L,TimeUnit.SECONDS))
{
//暂停毫秒
try { TimeUnit.MILLISECONDS.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); }
}
try
{
//1 查询库存信息
String result = stringRedisTemplate.opsForValue().get("inventory001");
//2 判断库存是否足够
Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
//3 扣减库存
if(inventoryNumber > 0) {
stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNumber));
retMessage = "成功卖出一个商品,库存剩余: "+inventoryNumber+"\t"+uuidValue;
System.out.println(retMessage);
}else{
retMessage = "商品卖完了,o(╥﹏╥)o";
}
}finally {
//V6.0 将判断+删除自己的合并为lua脚本保证原子性
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), Arrays.asList(key), uuidValue);
}
return retMessage+"\t"+"服务端口号:"+port;
}
}
可重入锁 + 设计模型
如何兼顾锁的可重入性问题呢?
可重入锁
可重入锁又名递归锁
是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提,锁对象得是同一个对象),不会因为之前已经获取过还没释放而阻塞。
如果是1个有 synchronized 修饰的递归调用方法,程序第2次进入被自己阻塞了岂不是天大的笑话,出现了作茧自缚。
所以Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。
可重入锁的种类
隐式锁(即synchronized关键字使用的锁)默认是可重入锁
指的是可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁,这样的锁就叫做可重入锁。
简单的来说就是:在一个synchronized修饰的方法或代码块的内部调用本类的其他synchronized修饰的方法或代码块时,是永远可以得到锁的
与可重入锁相反,不可重入锁不可递归调用,递归调用就发生死锁。
public class ReEntryLockDemo
{
public static void main(String[] args)
{
final Object objectLockA = new Object();
new Thread(() -> {
synchronized (objectLockA)
{
System.out.println("-----外层调用");
synchronized (objectLockA)
{
System.out.println("-----中层调用");
synchronized (objectLockA)
{
System.out.println("-----内层调用");
}
}
}
},"a").start();
}
}
public class ReEntryLockDemo
{
public synchronized void m1()
{
System.out.println("-----m1");
m2();
}
public synchronized void m2()
{
System.out.println("-----m2");
m3();
}
public synchronized void m3()
{
System.out.println("-----m3");
}
public static void main(String[] args)
{
ReEntryLockDemo reEntryLockDemo = new ReEntryLockDemo();
reEntryLockDemo.m1();
}
}
Synchronized重入的实现机理
每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针。
当执行monitorenter时,如果目标锁对象的计数器为零,那么说明它没有被其他线程所持有,Java虚拟机会将该锁对象的持有线程设置为当前线程,并且将其计数器加1。
在目标锁对象的计数器不为零的情况下,如果锁对象的持有线程是当前线程,那么 Java 虚拟机可以将其计数器加1,否则需要等待,直至持有线程释放该锁。
**当执行monitorexit时,Java虚拟机则需将锁对象的计数器减1。**计数器为零代表锁已被释放。
lock/unlock配合可重入锁进行AQS源码分析
如果还是当前线程,则nextc = c + acquires
可重入锁的计数问题,redis中那个数据类型可以代替
K,K,V
Map<String,Map<Object,Object>>
命令
hset key field value
hset redis锁名字(zzyyRedisLock) 某个请求线程的UUID+ThreadID 加锁的次数
总结
setnx,只能解决有无的问题,够用但是不完美
hset,不但解决有无,还解决可重入
思考+设计
目前有两条支线,目的是保证同一个时刻只能有一个线程持有锁进去redis做扣减库存动作
- 可重入
- 扣减原子性
Lua脚本
redis命令过程分析
加锁lua脚本lock
先判断redis分布式锁这个key是否存在
EXISTS key
返回零说明不存在,hset新建当前线程属于自己的锁 BY UUID :ThreadID
HSET zzyyRedisLock 0c90d37cb6ec42268861b3d739f8b3a8:1 1
命令 key value = UUID:ThreadID 次数
返回一说明已经有锁,需进一步判断是不是当前线程自己的
返回零说明不是自己的
返回一说明是自己的锁,自增一次表示重入
上述设计修改为Lua脚本
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命令
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
测试
解锁lua脚本unlock
设计思路:有锁且还是自己的锁
HEXISTS key uuid:ThreadID
返回零,说明根本没有锁,程序块返回nil
不是零,说明有锁且是自己的锁,直接调用HINCRBY -1,表示每次减个一,解锁一次。直到它变为零表示可以删除该锁key。
全套流程
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
测试全套流程
将上述Lua脚本整个进入微服务Java程序
通过实现JUC里面的Lock接口,实现Redis分布式锁RedisDistributedLock
RedisDistributedLock
public class RedisDistributedLock implements Lock
{
private StringRedisTemplate stringRedisTemplate;
private String lockName;//KEYS[1]
private String uuidValue;//ARGV[1]
private long expireTime;//ARGV[2]
public RedisDistributedLock(StringRedisTemplate stringRedisTemplate, String lockName)
{
this.stringRedisTemplate = stringRedisTemplate;
this.lockName = lockName;
this.uuidValue = IdUtil.simpleUUID()+":"+Thread.currentThread().getId();//UUID:ThreadID
this.expireTime = 30L;
}
@Override
public void lock()
{
tryLock();
}
@Override
public boolean tryLock()
{
try {tryLock(-1L,TimeUnit.SECONDS);} catch (InterruptedException e) {e.printStackTrace();}
return false;
}
/**
* 干活的,实现加锁功能,实现这一个干活的就OK,全盘通用
* @param time
* @param unit
* @return
* @throws InterruptedException
*/
@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 (!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";
// nil = false 1 = true 0 = false
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");
}
}
//===下面的redis分布式锁暂时用不到=======================================
//===下面的redis分布式锁暂时用不到=======================================
//===下面的redis分布式锁暂时用不到=======================================
@Override
public void lockInterruptibly() throws InterruptedException
{
}
@Override
public Condition newCondition()
{
return null;
}
}
这样设计的问题?
考虑扩展,本次是redis实现分布式锁,以后是zookeeper、mysql呢?
引入工厂模式改造
DistributedLockFactory
@Component
public class DistributedLockFactory
{
@Autowired
private StringRedisTemplate stringRedisTemplate;
private String lockName;
public Lock getDistributedLock(String lockType)
{
if(lockType == null) return null;
if(lockType.equalsIgnoreCase("REDIS")){
lockName = "zzyyRedisLock";
return new RedisDistributedLock(stringRedisTemplate,lockName);
} else if(lockType.equalsIgnoreCase("ZOOKEEPER")){
//TODO zookeeper版本的分布式锁实现
return new ZookeeperDistributedLock();
} else if(lockType.equalsIgnoreCase("MYSQL")){
//TODO mysql版本的分布式锁实现
return null;
}
return null;
}
}
RedisDistributedLock
public class RedisDistributedLock implements Lock
{
private StringRedisTemplate stringRedisTemplate;
private String lockName;//KEYS[1]
private String uuidValue;//ARGV[1]
private long expireTime;//ARGV[2]
public RedisDistributedLock(StringRedisTemplate stringRedisTemplate, String lockName){
this.stringRedisTemplate = stringRedisTemplate;
this.lockName = lockName;
this.uuidValue = IdUtil.simpleUUID()+":"+Thread.currentThread().getId();//UUID:ThreadID
this.expireTime = 30L;
}
@Override
public void lock(){
tryLock();
}
@Override
public boolean tryLock(){
try {tryLock(-1L,TimeUnit.SECONDS);} catch (InterruptedException e) {e.printStackTrace();}
return false;
}
/**
* 干活的,实现加锁功能,实现这一个干活的就OK,全盘通用
* @param time
* @param unit
* @return
* @throws InterruptedException
*/
@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 (!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";
// nil = false 1 = true 0 = false
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");
}
}
//===下面的redis分布式锁暂时用不到=======================================
//===下面的redis分布式锁暂时用不到=======================================
//===下面的redis分布式锁暂时用不到=======================================
@Override
public void lockInterruptibly() throws InterruptedException
{
}
@Override
public Condition newCondition()
{
return null;
}
}
InventoryService
@Service
@Slf4j
public class InventoryService
{
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Value("${server.port}")
private String port;
@Autowired
private DistributedLockFactory distributedLockFactory;
public String sale()
{
String retMessage = "";
Lock redisLock = distributedLockFactory.getDistributedLock("redis");
redisLock.lock();
try
{
//1 查询库存信息
String result = stringRedisTemplate.opsForValue().get("inventory001");
//2 判断库存是否足够
Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
//3 扣减库存
if(inventoryNumber > 0)
{
inventoryNumber = inventoryNumber - 1;
stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(inventoryNumber));
retMessage = "成功卖出一个商品,库存剩余: "+inventoryNumber+"\t服务端口:" +port;
System.out.println(retMessage);
return retMessage;
}
retMessage = "商品卖完了,o(╥﹏╥)o"+"\t服务端口:" +port;
}catch (Exception e){
e.printStackTrace();
}finally {
redisLock.unlock();
}
return retMessage;
}
}
进行可重入测试发现出现问题
DistributedLockFactory
@Component
public class DistributedLockFactory
{
@Autowired
private StringRedisTemplate stringRedisTemplate;
private String lockName;
private String uuidValue;
public DistributedLockFactory()
{
this.uuidValue = IdUtil.simpleUUID();//UUID
}
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")){
//TODO zookeeper版本的分布式锁实现
return new ZookeeperDistributedLock();
} else if(lockType.equalsIgnoreCase("MYSQL")){
//TODO mysql版本的分布式锁实现
return null;
}
return null;
}
}
RedisDistributedLock
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;
}
}
自动续期
Redis分布式锁还存在一个问题就是续期问题。
//==============自动续期
if redis.call('HEXISTS',KEYS[1],ARGV[1]) == 1 then
return redis.call('expire',KEYS[1],ARGV[2])
else
return 0
end
RedisDistributedLock
public class RedisDistributedLock implements Lock
{
private StringRedisTemplate stringRedisTemplate;
private String lockName;//KEYS[1]
private String uuidValue;//ARGV[1]
private long expireTime;//ARGV[2]
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()
{
tryLock();
}
@Override
public boolean tryLock()
{
try {tryLock(-1L,TimeUnit.SECONDS);} catch (InterruptedException e) {e.printStackTrace();}
return false;
}
/**
* 干活的,实现加锁功能,实现这一个干活的就OK,全盘通用
* @param time
* @param unit
* @return
* @throws InterruptedException
*/
@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 (!stringRedisTemplate.execute(new DefaultRedisScript<>(script,Boolean.class), Arrays.asList(lockName),uuidValue,String.valueOf(expireTime))) {
TimeUnit.MILLISECONDS.sleep(50);
}
this.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";
// nil = false 1 = true 0 = false
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");
}
}
// 上锁的情况
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 (stringRedisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Arrays.asList(lockName),uuidValue,String.valueOf(expireTime))) {
renewExpire();
}
}
},(this.expireTime * 1000)/3);
}
//===下面的redis分布式锁暂时用不到=======================================
//===下面的redis分布式锁暂时用不到=======================================
//===下面的redis分布式锁暂时用不到=======================================
@Override
public void lockInterruptibly() throws InterruptedException
{
}
@Override
public Condition newCondition()
{
return null;
}
}
总结
synchronized单机版可以,但是上分布式死翘翘
nginx分布式微服务单机锁不行
如果取消宕机所,可以考虑上redis分布式锁setnx
但是只是加了锁,没有释放锁,出现异常的话,可能无法释放锁,必须要在代码层面finally释放锁
宕机了,部署了微服务代码层面根本没有走到finally这块,没办法保证解锁,这个key没有被删除,需要有lockkey的过期时间设定
为redis的分布式锁key,增加过期时间,此外,还必须要setnx+过期时间在同一行,保证原子性
且必须规定只能自己删除自己的锁,你不能把别人的锁删除了,防止张冠李戴
同时unlock变为Lua脚本保证
最后还需要考虑锁重入问题,使用hset替代setnx+lock变为lua脚本办证,以及自动续期问题
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!