孤尽班第26天 -- 分布式锁

分布式锁使用场景

在同一个JVM内部,大家往往采用synchronized或者Lock的方式来解决多线程间的安全问题。但在分布式集群工作的开发场景中,在分布式架构下,在JVM之间,那么就需要一种更加高级的锁机制,来处理种跨JVM进程之间的线程安全问题,解决方案就是:使用分布式锁。

分布式锁组成要素

setnx: set key if not exist

expire: set expiration for a key

两者结合就可以组成一个分布式锁

原理解析

 1. key不存在时创建,并设置value和过期时间,返回值为1;成功获取锁;

2. 如key存在时直接返回0,抢锁失败;

3. 持有锁的线程释放锁时,手动删除锁;或者过期时间到,可以自动删除,锁释放。

 

如果某线程加锁后没有释放锁,锁就会被永久占用。解决方案有两种:

1. 使用set的命令时,同时设置过期时间,命令:set lock "1234" ex 100 nx

2. 使用lua脚本,将加锁的命令放在lua脚本中执行,lua脚本中的命令会被一起执行,所以有原子性。

eval命令

EVAL script numkeys key [key ...] arg [arg ...]
> eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second
1) "key1"
2) "key2"
3) "first"
4) "second

其中 "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 是被求值的 Lua 脚本,数字 2 指定了键名参数的数量, key1 和 key2 是键名参数,分别使用 KEYS[1] 和 KEYS[2] 访问,而最后的 first 和 second 则是附加参数,可以通过 ARGV[1] 和 ARGV[2] 访问它们。

call命令

> eval "return redis.call('set',KEYS[1],ARGV[1])" 1 foo bar

Redis实现分布式锁

引入Jedis包

    <dependency>
        <groupId>redis.clients</groupId>
        <artifactId>jedis</artifactId>
        <version>2.9.0</version>
    </dependency>

 用Jedis加锁

@Slf4j
@AllArgsConstructor
public class JedisCommandLock {

    private  RedisTemplate redisTemplate;

    private static final String LOCK_SUCCESS = "OK";
    private static final String SET_IF_NOT_EXIST = "NX";
    private static final String SET_WITH_EXPIRE_TIME = "PX";

    /**
     * 尝试获取分布式锁
     * @param jedis Redis客户端
     * @param lockKey 锁
     * @param requestId 请求标识
     * @param expireTime 超期时间
     * @return 是否获取成功
     */
    public static   boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {

        String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);

        if (LOCK_SUCCESS.equals(result)) {
            return true;
        }
        return false;
    }
}

在Jedis的set函数中:

  • key:加锁的key
  • value:可以使用UUID.randomUUID().toString(),代表加锁的客户端请求标识
  • nxxx:NX,表示SET IF NOT EXIST
  • expx:PX,表示毫秒
  • time:key的过期时间。

set函数在满足设置lock的key值和ttl的同时,在value中设置requestID,可以识别加锁的客户端,防止其他客户端删锁。

用Jedis解锁

错误示例1:

public static void wrongReleaseLock1(Jedis jedis, String lockKey) {
    jedis.del(lockKey);
}

这个错得很明显,不管谁加的锁,上来就删。

错误示例2:

public static void wrongReleaseLock2(Jedis jedis, String lockKey, String requestId) {
        
    // 判断加锁与解锁是不是同一个客户端
    if (requestId.equals(jedis.get(lockKey))) {
        //在这两行代码的时间差,可以已经被其他代码解锁,而引发错误解锁
        jedis.del(lockKey);
    }
}

这个例子的缺陷在于判断是否是自己加的锁和解锁不是原子操作,从原理上来说,存在解错锁的风险,即当判断时还是我的锁,但等到解锁时可能已经不是我的锁了,可是我还要解,结果就把别人的锁给解了。这种错误非常微妙,一旦发生很难debug。

正确的解锁姿势:

@Slf4j
@AllArgsConstructor
public class RedisCommandLock {

    private static final Long RELEASE_SUCCESS = 1L;

    /**
     * 释放分布式锁
     * @param jedis Redis客户端
     * @param lockKey 锁
     * @param requestId 请求标识
     * @return 是否释放成功
     */
    public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {

        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));

        if (RELEASE_SUCCESS.equals(result)) {
            return true;
        }
        return false;
    }
}

上面的例子用lua脚本执行的原子性来解锁。

锁过期问题

预估业务操作10秒,锁设置20秒,但出于各种原因,比如STW,业务操作执行超过了20秒,业务会在无锁状态下运行,就会发生数据紊乱。

解决方案

1. 使用乐观锁。模拟CAS乐观锁的方式,增加版本号。

 

这个方法的缺点在于需要修改代码才能在写请求中增加版本号。

2. 使用Watch Dog自动延期机制。客户端1加锁的key默认生存时间为30秒。一旦加锁成功,就会启动一个watch dog的后台线程,在客户端1持有锁超过30秒后,watch dog每隔10秒就检查一下客户端1(这里有点像心跳机制),如果客户端1依然持有锁,那么就会延长锁的生存时间,直到客户端1完成操作释放锁为止。

Credit:刘雪松老师的讲义。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值