美文网首页
千帆竞发 —— 分布式锁

千帆竞发 —— 分布式锁

作者: DreamsonMa | 来源:发表于2019-03-21 17:25 被阅读0次

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

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

    相关文章

      网友评论

          本文标题:千帆竞发 —— 分布式锁

          本文链接:https://www.haomeiwen.com/subject/zoccvqtx.html