在进行分布式应用逻辑开发时,经常会遇到并发问题。
比如我们在修改一个用户的信息,首先需要获取用户信息,再内存中修改后,再存回去。这个过程如果有其他线程同时操作,着就会产生并发问题,因为读取和存储都不是原子性的。我们需要通过分布式锁限制程序的并发执行。
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"));
}
}
来源:oschina
链接:https://my.oschina.net/u/4283481/blog/4133747