Redis怎样实现分布式锁?

前言

       我们知道,在Java单进程中,多线程的环境下,如果我们要操作一个共享变量,需要使用synchronized或者是JUC同步工具类才能保证线程安全。那么,多进程环境下,我们要怎样保证线程安全?

为什么需要分布式锁?

       我们知道,synchronized或者是JUC同步工具类只能在同一进程中保证线程安全,他们的影响范围没办法超出本Java进程。但是随着分布式成为主流,多进程共享数据的情况越来越常见。

       如上图,两个进程同时对存储在MySQL、Redis或者是zookeeper中的共享数据进行读写,即有可能出现线程安全问题,这种情况下,我们就需要一个可以在分布式环境下也可以使用的锁,来保证线程安全,这即是分布式锁。

分布式锁可以使用什么组件实现?

       因为要在分布式环境下生效,因此实现分布式锁使用的组件也必须是每个进程都可以连接到的,目前比较常见的是使用Redis和Zookeeper来实现。

Redis分布式锁实现逻辑

       Redis中,我们使用String数据结构来实现分布式锁。

加锁

       Redis中,set的语法如下:
       SET key value [EX seconds] [PX milliseconds] [NX|XX]

  • EX second :设置键的过期时间为 second 秒。 SET key value EX second 效果等同于 SETEX key second value 。
  • PX millisecond :设置键的过期时间为 millisecond 毫秒。 SET key value PX millisecond 效果等同于 PSETEX key millisecond value 。
  • NX :只在键不存在时,才对键进行设置操作。 SET key value NX 效果等同于 SETNX key value 。
  • XX :只在键已经存在时,才对键进行设置操作。

       上述参数中,我们可以将key作为锁标识,然后设置NX参数。如果key写入成功,表示当前Redis中不存在这个key,可以加锁;如果key写入失败,表示当前Redis中已存在这个key,已经有其它线程获取到锁了。命令如下:

SET lockKey requestId NX
lockKey为锁标识,最好带上使用共享变量唯一标识,可以使用订单编号、用户编号等。 requestId为本次锁请求编号,释放锁时使用。

       上述命令中,可以实现加锁,但是如果在加锁后,应用挂了,或者出现了其它问题,导致没有及时解锁,就有可能出现死锁,因此,需要再给加上一个过期时间,让锁可以自动消失。命令如下:

SET lockKey requestId EX seconds NX

释放锁

       释放锁的时候,我们不能直接使用del命令去删除Redis键值,否则会出现A获取的锁,B也可以释放的情况。因此,我们在释放锁的时候,需要判断当前锁的请求ID是否是加锁时是否一致,如果一致,才能释放锁。
       由于没有现成的命令可以实现上述的释放锁的逻辑,所以我们需要使用Redis的script来实现,script脚本如下:

if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end
KEYS[1]为锁标识,即加锁时的lockKey
ARGV[1]为锁请求编号,即加锁时的requestId

Redis分布式锁代码

public class RedisDistributedLock implements Lock {

    // 锁键值
    private String              lockKey;
    // 锁请求编号
    private String              requestId;
    // redis集群客户端
    private JedisCluster        jedisCluster;

    public RedisDistributedLock(String lockKey, JedisCluster jedisCluster){
        this.lockKey = lockKey;
        // 使用UUID作为锁唯一标识
        requestId = UUID.randomUUID().toString();
        this.jedisCluster = jedisCluster;
    }
    
    /**
     * 尝试获取锁
     */
    @Override
    public boolean tryLock() {
        // 获取锁
        String result = jedisCluster.set(lockKey, requestId, "NX", "EX", 2);
        return StringUtils.isNotBlank(result) && LOCK_SUCCESS.equals(result);
    }
    
    /**
     * 释放锁
     */
    @Override
    public void unlock() {
        // 若redis中存在lockKey,则删除lockKey,否则返回0
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        Object result = jedisCluster.eval(script, Collections.singletonList(lockKey),
                                          Collections.singletonList(requestId));
        // 成功
        if (result != null && "OK".equals(result.toString())) {
            logger.debug("释放锁成功,lockKey:{}, requestId:{}", lockKey, requestId);
        } else {// 失败
            logger.debug("释放锁失败,lockKey:{}, requestId:{}", lockKey, requestId);
        }
    }
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值