一个操作要修改用户的状态,修改状态需要先读出用户的状态,在内存里进行修改,改完了再存回去。如果这样的操作同时进行了,就会出现并发问题,因为读取和保存状态这两个操作不是原子的。这个时候就要使用到分布式锁来限制程序的并发执行。
同时操作一个context,存在并发问题分布式锁
一般是使用 setnx(set if not exists) 指令占坑, 用完再调用 del 指令释放茅坑。如果逻辑执行到中间出现异常了,可能会导致 del 指令没有被调用,这样就会陷入死锁,锁永远得不到释放。于是我们在拿到锁之后,再给锁加上一个过期时间,比如 5s,这样即使中间出现异常也可以保证 5 秒之后锁会自动释放。
/**
* @Auther: majx2
* @Date: 2019-3-21 16:02
* @Description:
*/
public class DistributedLockTest {
Jedis jedis = RedisDS.create().getJedis();
final static String KEY = "KEY";
@Test
public void testLock() throws InterruptedException {
new Thread(new Runnable() {
@Override
public void run() {
Assert.assertTrue(exec());
}
}).start();
Thread.sleep(1000);
Assert.assertFalse(exec());
Thread.sleep(3000);
}
public boolean exec(){
return new RedLock().trylock(KEY, new LockWrap() {
@Override
public boolean invoke() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {}
return true;
}
});
}
public class RedLock{
public boolean trylock(String key,LockWrap wrap){
Long result = jedis.setnx(key, KEY);// 占坑
if(result == 1L){
jedis.expire(key,5000); // 避免没有删除
boolean invoke = wrap.invoke();
jedis.del(key);
return invoke;
}
return false;
}
}
public interface LockWrap{
boolean invoke();
}
}
但是以上逻辑还有问题。如果在 setnx 和 expire 之间服务器进程突然挂掉了,可能是因为机器掉电或者是被人为杀掉的,就会导致 expire 得不到执行,也会造成死锁。这种问题的根源就在于 setnx 和 expire 是两条指令而不是原子指令。 解决这些问题,可以使用开源分布式组建redission。
超时问题
Redis 的分布式锁不能解决超时问题,如果在加锁和释放锁之间的逻辑执行的太长,以至于超出了锁的超时限制,就会出现问题。因为这时候锁过期了,第二个线程重新持有了这把锁,第二个线程就会在第一个线程逻辑执行完之间拿到了锁;紧接着第一个线程执行完了业务逻辑,就把锁给释放了,第三个线程就会在第二个线程逻辑执行完之间拿到了锁。为了避免这个问题,Redis 分布式锁不要用于较长时间的任务。如果真的偶尔出现了,数据出现的小波错乱可能需要人工介入解决。
集群问题
在 Sentinel 集群中,主节点挂掉时,从节点会取而代之,客户端上却并没有明显感知。原先第一个客户端在主节点中申请成功了一把锁,但是这把锁还没有来得及同步到从节点,主节点突然挂掉了。然后从节点变成了主节点,这个新的节点内部没有这个锁,所以当另一个客户端过来请求加锁时,立即就批准了。这样就会导致系统中同样一把锁被两个客户端同时持有,不安全性由此产生。
集群环境下,分布式锁存在问题如果你很在乎高可用性,希望挂了一台 redis 完全不受影响,那就应该考虑 redlock算法
。不过代价也是有的,需要更多的 redis 实例,性能也下降了。
注: Redlock算法,需要提供多个 Redis 实例,这些实例之前相互独立没有主从关系。加锁时,它会向过半节点发送 set(key, value, nx=True, ex=xxx) 指令,只要过半节点 set 成功,那就认为加锁成功。释放锁时,需要向所有节点发送 del 指令。不过, Redlock 算法还需要考虑出错重试、时钟漂移等很多细节问题,同时因为 Redlock 需要向多个节点进行读写,意味着相比单实例 Redis 性能会下降一些。
本文基于《Redis深度历险:核心原理和应用实践》一文的JAVA实践。更多文章请参考:高性能缓存中间件Redis应用实战(JAVA)
网友评论