Redis 分布式锁

当服务采用集群方式部署的时候,本地锁无法发挥作用,所以需要分布式锁来实现加锁。

实现

Redis主要运用setnx命令进行锁操作

  • 加锁: SETINX key value, 当锁不存在的时候,成功设置锁并返回
  • 解锁: DEL key, 通过删除键值对释放锁,以便其他线程可以通过SETINX来获取锁
  • 锁超时:EXPIRE key timeout, 设置超时时间,以便即使锁没有被及时释放,也可以在一定时间内自动释放,避免资源被永远锁住

Redis使用lua脚本的好处

  1. 减少网络开销。可以将多个请求通过脚本形式一次发送,减少网络时延。
  2. 原子操作。Redis会将整个脚本作为整体执行,中间不会被其他命令插入。
  3. 复用。客户端发送的脚本会永久存储在redis中,这样其他客户端就可以复用而不需要使用代码完成相同的逻辑

如何使用lua

EVAL
eval "return {KEYS[1], KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second

-- 第一个参数是lua程序脚本
-- 第二个参数是lua脚本后面的那个参数,表示KEYS参数的个数
-- 第三个参数是Redis键名。lua脚本可以访问有KEYS全局变量组成的一维数据参数
-- 第四个参数是相应KEYS所对应的值,并且lua脚本可以通过ARGV访问其值
redis.call

可以使用redis.call(),redis.pcall()从lua脚本调用Redis命令。

redis.call()与redis.pcall()唯一的区别在于Redis命令调用错误时,redis.call抛出Lua类型的错误,再强制EVAL将错误返回给命令的调用者,而redis.pcall将捕获错误并返回表示错误的Lua表类型。

问题

SETINX和EXPIRE的非原子性

在设置锁成功后,而设置超时时间时因为服务器挂掉、重启、网络等问题而没有执行成功

解决方法:

public boolean tryLock_with_lua(String key, String UniqueId, int seconds) {
    String lua_scripts = "if redis.call('setnx',KEYS[1],ARGV[1]) == 1 then" +
            "redis.call('expire',KEYS[1],ARGV[2]) return 1 else return 0 end";
    List<String> keys = new ArrayList<>();
    List<String> values = new ArrayList<>();
    keys.add(key);
    values.add(UniqueId);
    values.add(String.valueOf(seconds));
    Object result = jedis.eval(lua_scripts, keys, values);
    //判断是否成功
    return result.equals(1L);
}

锁误解除

如果线程A获取到锁并设置过期时间为30s,然后过期时间到了线程B获取到了锁,随后线程A执行完毕,就会使用DEL误释放B的锁。

解决方法:

通过在value中设置当前锁的标识,并在删除之前判断是否为当前线程所持有。

// 设置锁
public boolean tryLock_with_set(String key, String uniqueId, int seconds){
    // u    niqueId具有唯一性
    // N X
    return "Ok".equals(jedis.set(key,uniqueId,"NX","EX",seconds));
}

// 释放锁
public boolean releaseLock_with_lua(String key, String value){
    // 使用lua脚本尽量保持原子性
    String luaScript= "if redis.call('get', KEYS[1])==ARGV[1] then "+
        "return redis.call('del',KEYS[1]) else return 0 end";
    return jedis.eval(luaScript, Collections.singletonList(key), Collections.singletonList(value)).equals(1L);
}

超时解锁导致并发

如果线程A获取到了锁并设置了过期时间30s,但线程A执行时间超过了30s,这个时候线程B获取到了锁,就会导致A,B并发执行。

为了解决这个问题,我们可以

  1. 将过期时间设置的足够长,保证代码逻辑在锁释放前执行完成。但是这很难设置,要么设置时间过长影响性能,要么比较短还是会导致并发执行
  2. 为获取锁的线程添加守护线程,为将要过期但并未释放的锁增加有效时间。

不可重入

通过本地记录重入次数在分布式锁保证可重入性,由于考虑到过期时间、本地以及Redis一致性的问题,会增加代码的复杂性。

解决方法:

// 如果lock_key不存在
if (redis.call('exists', KEYS[1]==0))
    then
	// 设置lock_key线程标识1进行加锁
	redis.call('hset',KEYS[1],ARGV[2],1);
	// 设置过期时间
    redis.call('pexpire',KEYS[1],ARGV[1]);
    
// 如果lock_key存在且线程标识是当前欲加锁线程的标识
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1)
	// 重入次数自增
	then
    redis.call('hincrby', KEYS[1], ARGV[2], 1);
	// 重置过期时间
    redis.call('pexpire', KEYS[1], ARGV[1]);
    return nil;
        end;

// 如果加锁失败,返回锁剩余时间
return redis.call('pttl', KEYS[1]);

无法等待锁释放

我们可以通过两种方式来等待锁的释放,第一种是比较传统的客户端轮询的方式,当未获取到锁的时候会等待一段时间后重新去获取锁,直到成功获取锁或等待超时。可是这种方式比较消耗服务器资源

另一种是使用redis的发布订阅功能,当获取锁失败的时候,订阅锁释放消息

Ref

  1. https://www.jianshu.com/p/366d1b4f0d13

  2. https://www.cnblogs.com/PatrickLiu/p/8656675.html

  3. https://xiaomi-info.github.io/2019/12/17/redis-distributed-lock/

  4. https://juejin.cn/post/6844903830442737671

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值