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