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

Posted lonelyxmas

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了3.Redis系列Redis的高级应用-分布式锁相关的知识,希望对你有一定的参考价值。

原文: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"));
 }

}

以上是关于3.Redis系列Redis的高级应用-分布式锁的主要内容,如果未能解决你的问题,请参考以下文章

分布式缓存技术redis学习系列——redis高级应用(集群搭建集群分区原理集群操作)

分布式缓存技术redis学习系列——redis高级应用(主从事务与锁持久化)

8.Redis系列Redis的高级应用-简单限流

死磕 java同步系列之redis分布式锁进化史

死磕 java同步系列之redis分布式锁进化史

分布式缓存Redis高级应用实战:为什么要用缓存机制