千帆竞发 —— 分布式锁

2019/04/19 22:42
阅读数 81

一个操作要修改用户的状态,修改状态需要先读出用户的状态,在内存里进行修改,改完了再存回去。如果这样的操作同时进行了,就会出现并发问题,因为读取和保存状态这两个操作不是原子的。这个时候就要使用到分布式锁来限制程序的并发执行。

同时操作一个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)

展开阅读全文
打赏
0
0 收藏
分享
加载中
更多评论
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部