【Redis】之分布式锁

本文介绍了分布式锁的概念,包括其作用和必备条件,并重点讲解了基于Redis的分布式锁实现方式。通过Redis的原子性命令和Lua脚本确保锁的获取与释放的正确性。还提到了Redisson和RedLock作为更高级的解决方案,它们解决了锁的可重入性和续期问题,以及在多节点Redis下的安全性。
摘要由CSDN通过智能技术生成

目录


一、分布式锁介绍


1、什么是分布式锁

在多线程环境中,为了控制线程对资源并发访问和竞争,我们经常需要用到锁来进行控制,例如我们常用的 SynchronizedReentrantLock 等。但这些锁只能用于单机系统中,如果涉及到多机器、多节点的分布式环境的资源竞争,就需要使用分布式锁了。

说白了分布式锁和本地锁是一样的性质,区别在于只是本地锁控制的是单节点,而分布式锁控制的是多节点的分布式项目。本质上都是锁,都是为了限制并发访问。

所以,分布式锁的作用我们可以总结为:保证同一时间只有一个客户端可以对共享资源进行操作

分布式锁常用的业务场景:电商领域的秒杀扣减库存,防止超卖的场景;

2、分布式锁具备的条件

  • 互斥性:和本地锁互斥性一样,分布式锁需要保证在不同节点的不同线程的互斥,即同一时间只能有一个节点获得锁;
  • 可重入性:同一个节点的一个线程获取锁后,还可以再次获取这个锁,避免死锁发生;
  • 锁过期:如果一个节点拿到了锁,但执行过程中崩溃了,就会导致锁无法释放而变成死锁;所以为了防止死锁,需要给锁设置一个过期时间;
  • 高性能、高可用:加锁和解锁需要高效,同时也需要保证高可用防止分布式锁失效;
  • 非阻塞性:即没有获取到锁将直接返回获取锁失败;
  • 支持公平锁和非公平锁(可选):公平锁的意思是按照请求加锁的顺序获得锁,非公平锁就相反是无序的。


二、分布式锁的实现


1、常用的分布式锁的实现方式

  1. 基于数据库(例如 MySQL)实现分布式锁;
  2. 基于 ZooKeeper 实现分布式锁;
  3. 基于 Redis 实现分布式锁;

使用数据库实现分布式锁的原理是:设计一个用于分布式锁的表,将用来竞争的资源名称设置为表的唯一索引,获取锁的时候,就插入一条记录,插入成功就代表获取到锁,插入失败就代表获取锁失败,释放锁的时候,就删除这条记录。

ZooKeeper 是以 Paxos 算法为基础分布式应用程序协调服务,并且 ZooKeeper 的数据节点和文件目录类似,所以我们可以用此特性实现分布式锁。

由于数据库的并发性能跟不上,而 ZooKeeper 需要额外维护一套第三方组件,所以这两种我们并不常用,并且也不是这篇文章介绍的重点。下面着重介绍基于 Redis 实现分布式锁发方式。

2、基于 Redis 实现分布式锁的原理

Redis 能实现分布式锁的原因在于它的以下两个命令:

# 如果key不存在,则创建并赋值,成功加入缓存并且返回1;如果已存在,则返回0。
SETNX key value
# 设置key的生存时间,当key过期(生存时间为0),会自动删除
EXPIRE key seconds

但执行这两个命令过程不是原子性的,如果设置了 SETNX 后程序崩溃,EXPIRE 未成功执行则会出现死锁的情况。所以 redis 在 2.6.12 版本过后增加新的命令:

SET key value [EX seconds | PX milliseconds] [NX|XX]

执行这条命令的整个过程是原子性的,其中:

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

注意:value 值要保证唯一,这样才不会出现A线程把B线程的锁给释放的问题(这个问题出现在锁过期但程序未执行完的情况下)。

所以,如果这条命令执行成功,则客户端成功获取到了锁,接下来就可以访问共享资源了;而如果执行失败,则说明获取锁失败。

而释放锁时需要判断锁的持有者(即通过 value 值的唯一来判断是否是自己的锁),这样可以避免把其他线程持有的锁给释放掉了。但 Redis 释放锁的过程包含三步操作:获取、判断和删除,而 Redis 的 get 和 del 命令都不是原子操作,所以我们需要引入 Lua 脚本来保证保证这三步的原子性:

if redis.call("get",KEYS[1]) == ARGV[1] then
	return redis.call("del",KEYS[1])
else
	return 0
end

这段 Lua 脚本在执行的时候把 key 作为 KEYS[1] 的值传进去,把 value 值作为 ARGV[1] 的值传进去。Redis 在执行这断 Lua 脚本的时候会将获取、判断和删除这三步整合成一个完整的操作,从而保证了原子性。

下面是在 Java 中使用该原生的方法获取锁的例子:

public String getRedisLock() {
	// 1、占分布式锁,去redis占坑,设置过期时间必须和加锁是同步的,保证原子性(避免死锁)
    String uuid = UUID.randomUUID().toString();
    Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", uuid,300,TimeUnit.SECONDS);
    if (lock) {
        System.out.println("获取分布式锁成功...");
        try {
            // 加锁成功...执行业务
            return "finish!";
        } finally {
            // 执行 lua 脚本释放锁来保证释放锁是原子性的
            String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
            stringRedisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList("lock"), uuid);
        }
    } else {
        System.out.println("获取分布式锁失败...等待重试...");
        // 加锁失败...重试机制,休眠一百毫秒
        try { TimeUnit.MILLISECONDS.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); }
        // 自旋的方式
        return getRedisLock();     
    }
}

3、RedissonRedLock

在项目中一般不会使用上面例子中原生且复杂的方式进行分布式加锁,因为它处理锁的续期问题比较麻烦,需要自己维护一个线程来检查业务处理完之前锁是否过期。所以我们一般都是使用 Redisson 来实现分布式加锁。

1)Redisson

Redisson 是一个 Redis 客户端,并且 Redisson 功能强大,所以使用 Redisson 可以很方便实现 Redis 分布式锁。而且 Redisson 可以轻松解决以下问题:

  • 锁的可重入性:Redisson 会存储加锁的线程信息,加锁的次数信息, 加了几次就要释放几次,通过 hash 数据结构进行存储;
  • 锁的续期问题:加锁成功后会启动一个后台线程(watchdog 看门狗机制),每隔一定时间(可配置)检查,如果客户端还持有锁,那么就会延长锁的时间。

关于 Redisson 的详情知识可以参考我的另一篇博客:【Redis】之 Redisson 分布式锁

2)RedLock

在主从复制、哨兵模式的多节点 Redids 下,存在这样一个集群安全问题:

  • 客户端 A 将 Key 写入到 Master 节点成功获取到锁;
  • 此时 Master 节点发生故障,Key 没有来得及同步到 Slave 上(数据是后台通过异步同步的);
  • Slave 节点升级为 Master 节点;
  • 客户端 B 从新的 Master 节点获取到了对应同一个资源的锁。

此时,客户端A和客户端B同时持有了同一个资源的锁,锁的安全性被打破。为了解决 Redis 主从架构锁失效问题,Redis 官方提供了 RedLock 来解决这个问题。

RedLock 是 redis 官方提出的实现分布式锁管理器的算法,这个算法会比一般的普通方法更加安全可靠:超过半数redis节点(没有任何的依赖关系,就是单独节点)加锁成功才算加锁成功。

关于 RedLock 的详情知识可以参考我的另一篇博客:【Redis】之 RedLock 分布式锁

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值