redis学习总结
为啥要加分布式锁?为了多个进程对共享资源并发访问时,能够保证数据的一致性和系统的稳定性。在库存扣减场景中使用分布式锁,主要为了高并发下防止超卖。
为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件:
- 互斥性。在任意时刻,只有一个客户端能持有锁。
- 不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
- 解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。
- 加锁和解锁必须具有原子性。
redis实现分布式锁:
- redis是基于单线程模型,这个特性可以让调用方的请求排队,对于并发请求,只能有一个调用方能够获得锁。
- setNx(),redis的API,向redis保存一个key-value,特性是只有key不存在时才会设置成功,否则返回0。体现分布式锁互斥性。
- expire(),给分布式锁设置超时时间,避免客户端获得锁后宕机造成死锁。
- 防止误删:给分布式锁的value设置一个requestId,可以是uuid。A线程超时时间内没有完成业务,锁过期删除了,此时B线程获得锁,A线程这时才完成业务并删除锁,但此时删除的是B线程的锁。造成误删。
- 锁续期:看门狗,开一个线程不断给锁续期,续期三次没有完成业务,报错且回滚。现在能防止误删了,但是业务执行时间超过了锁超时时间,意味着A线程还没处理完,B线程已经获取锁进行处理了,可能造成并发问题。而且没等B线程也没有处理完,锁又过期了,C线程又获得了锁,连锁的并发问题,需要给锁续期。
我觉得,不用使用锁续期。因为比较复杂,而且谁也不会在分布式锁里面加很多操作,少量的操作,这些操作的接口调用超时时间加在一起设置为锁的超时时间就行了。
加锁:并且使用requestId防止误删
public boolean tryLock(String lockKey, String requestId){
String threadName = Thread.currentThread().getName();
Jedis jedis = this.jedisPool.getResource();
Long start = System.currentTimeMillis();
try{
for (;;){
String lockResult = jedis.set(lockKey, requestId, setParams);
if ("OK".equals(lockResult)){
System.out.println(threadName+": 获取锁成功");
return true;
}
//否则循环等待,在timeout时间内仍未获取到锁,则获取失败
System.out.println(threadName+": 获取锁失败,等待中");
long l = System.currentTimeMillis() ‐ start;
if (l>=timeout) {
return false;
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}finally {
jedis.close();
}
}
解锁:使用lua脚本,保证误删比较操作和删除lockKey操作的原子性。
public boolean releaseLock(String lockKey,String requestId){
String threadName = Thread.currentThread().getName();
System.out.println(threadName+":释放锁");
Jedis jedis = this.jedisPool.getResource();
String lua = "if redis.call('get',KEYS[1]) == ARGV[1] then" + " return redis.call('del',KEYS[1]) " + "else" + " return 0 " + "end";
try {
Object result = jedis.eval(lua, Collections.singletonList(lockKey),Collections.singletonList(requestId));
if("1".equals(result.toString())){
return true;
}
return false;
}finally {
jedis.close();
}
}

浙公网安备 33010602011771号