3.【Redis系列】Redis的高级应用-分布式锁

醉酒当歌 提交于 2020-03-18 02:28:15
原文:3.【Redis系列】Redis的高级应用-分布式锁

在进行分布式应用逻辑开发时,经常会遇到并发问题。
比如我们在修改一个用户的信息,首先需要获取用户信息,再内存中修改后,再存回去。这个过程如果有其他线程同时操作,着就会产生并发问题,因为读取和存储都不是原子性的。我们需要通过分布式锁限制程序的并发执行。

1.分布式锁

分布式锁本质上就是在Redis里面占一个车位,当有新的车辆过来时,发现已经有一辆车停在车位上,只能是放弃或者稍后再来。

我们用命令模拟下:

> setnx lock:qqsir true
OK
...do something ... 
>del lock:qqsir 

但是这样有一个问题,如果中间的逻辑出现问题,导致没有执行del,这样就会陷入死锁,锁永远不能被释放。

改进1: 我们可以对key设置一个过期时间,这样即使后面逻辑报错,时间到期后也可以将锁是释放掉

> setnx lock:qqsir true
OK
>expire lock:qqsir 5
...do something ... 
>del lock:qqsir 

以上的逻辑其实还是存在问题,假设setnx和expire之间突然断电,没有执行,这样锁还是无法释放。这个问题的根本原因就是setnx和expire的操作不是原子性的。

为了解决这个问题,开源社区出现了一堆分布式锁的library。专门解决这个问题,实现逻辑非常复杂。

为了治理这个乱象,Redis2.8版本加入了set的扩展参数,是的setnx和expire指令可以一起执行,彻底解决了分布式锁的乱象。

> setnx lock:qqsir true ex 5 nx
OK
>expire lock:qqsir 5
...do something ... 
>del lock:qqsir 

以上就是setnx和expire的原子指令。

超时问题

上面加了个过期时间是5s,如果中间的逻辑执行大于5s,后面的逻辑将得不到执行,为了避免这个问题,Redis分布式锁的逻辑不要过长,如果真的偶尔出现了就需要人工介入了。

可重入性

可重入性就是在持有锁的情况下,再次请求加锁,如果一个锁支持同一个线程的多次加锁,那这个线程就是可重入性的。比如java语言中的ReentrantLock就是一种可重入的锁。Redis的可重入锁,需要对set方法进行封装,使用线程的ThreadLocal变量存储当前只有持有锁的计数。

以下是java版本的可重入锁实现

public class RedisWithReentrantLock {

 private ThreadLocal<Map<String, Integer>> lockers = new ThreadLocal<>();

 private Jedis jedis;

 public RedisWithReentrantLock(Jedis jedis) {
   this.jedis = jedis;
 }

 private boolean _lock(String key) {
   return jedis.set(key, "", "nx", "ex", 5L) != null;
 }

 private void _unlock(String key) {
   jedis.del(key);
 }

 private Map<String, Integer> currentLockers() {
   Map<String, Integer> refs = lockers.get();
   if (refs != null) {
     return refs;
   }
   lockers.set(new HashMap<>());
   return lockers.get();
 }

 public boolean lock(String key) {
   Map<String, Integer> refs = currentLockers();
   Integer refCnt = refs.get(key);
   if (refCnt != null) {
     refs.put(key, refCnt + 1);
     return true;
   }
   boolean ok = this._lock(key);
   if (!ok) {
     return false;
   }
   refs.put(key, 1);
   return true;
 }

 public boolean unlock(String key) {
   Map<String, Integer> refs = currentLockers();
   Integer refCnt = refs.get(key);
   if (refCnt == null) {
     return false;
   }
   refCnt -= 1;
   if (refCnt > 0) {
     refs.put(key, refCnt);
   } else {
     refs.remove(key);
     this._unlock(key);
   }
   return true;
 }

 public static void main(String[] args) {
   Jedis jedis = new Jedis();
   RedisWithReentrantLock redis = new RedisWithReentrantLock(jedis);
   System.out.println(redis.lock("codehole"));
   System.out.println(redis.lock("codehole"));
   System.out.println(redis.unlock("codehole"));
   System.out.println(redis.unlock("codehole"));
 }

}
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!