Redis分布式锁

文章介绍了分布式锁的概念,以及在MySQL、Redis和Zookeeper中实现分布式锁的方法。针对基于Redis的分布式锁,讨论了存在的问题,如线程安全和原子性问题,并提出了解决方案,包括使用lua脚本保证命令原子性。最后,文章提到了Redisson这一基于Redis的分布式工具,它提供了一种更完善的分布式锁实现,具备可重入、可重试和超时续约等特性。
摘要由CSDN通过智能技术生成

分布式锁

什么是分布式锁

满足分布式系统或集群模式下多进程可见并且互斥的锁。

  • 多进程可见
  • 互斥
  • 高可用
  • 高性能(并发)
  • 安全性
  • 一些功能特性

分布式锁的实现

MySQL

  • 互斥:
    事务机制。MySQL可以自动分配互斥的锁,执行完业务之后提交事务,MySQL释放锁,如果出错可以回滚。
  • 高可用:
    支持主从模式。可用性还好
  • 高性能:一般
  • 安全性:断开连接自动释放锁

Redis

  • 互斥:
    利用setnx这样的互斥命令,只有数据不存在的时候语句才能成功,否则会失败。删除这个key,就可以释放锁。
  • 高可用:
    不仅支持主从,还支持集群。可用性好
  • 高性能:好
  • 安全性:利用key的过期机制,即便服务器故障,锁到期之后也能自动释放。

Zookeeper

  • 互斥:
    利用内部节点机制。内部可以创建节点,节点具有唯一性,有序性,还可以创建临时节点。利用有序性实现互斥,比如id最小的那个获取锁成功。删除节点即可释放锁。
  • 高可用:
    也支持集群,可用性好。
  • 高性能:
    要求强一致性,主从之间同步需要一定时间。性能一般。
  • 安全性:
    由于创建的一般是临时节点,断开连接会自动释放。

分布式锁

基于Redis的分布式锁

实现分布式锁时需要实现两个基本方法:

  • 获取锁

    • 互斥:确保只能有一个线程获取锁,可以利用setnx的互斥特性 setnlock thread1 EX 10 NX,成功返回OK,不成功返回nil
  • 释放锁:

    • 手动释放: DEL key
    • 超时释放:获取锁时添加一个超时时间

初次方案

实现分布式锁
但是这种方案在一些极端情况下还有一些问题:

  • 线程1在获取锁之后,业务阻塞住了,直到锁自然释放也没有完成,此时线程2就可以拿到锁,即便此时线程1还没有完成业务。
  • 线程2在运行的某一时刻,线程1突然苏醒,并且完成了自己的业务,然后它将锁释放了。此时它释放的锁时线程2获得的锁,因为它自己的已经过期自动释放了。
  • 线程2还没有执行完业务,线程3又来了,并且拿到了锁。因为线程2的锁被线程1删除掉了。
  • 此时又出现了线程并行的情况,出现线程安全问题。

问题

  • 所以我们在释放锁的时候,要先做个判断,判断锁的标识是否一致。之前在获取锁的时候,存了一个线程标识进去。
  • 在释放锁的时候,先把标识取出来,判断和自己的是否一样,如果一样,说明这是自己的锁,可以删除。

删前判断解决误删问题

解决方案
分布式锁的原子性问题

  • 如果线程1在判断锁标识之后,发现这个锁是自己的,正要删除锁的时候,线程1阻塞了。一直阻塞到,锁过期自动删除,还没醒过来。
  • 线程2此时可以获得锁,并且开始执行自己的业务。即使线程1没有执行完业务,亲自释放锁,但是它的锁已经过期了,此时并没有锁。
  • 线程2还没执行完业务,线程1醒了。发现自己之前判断锁是自己的,直接把当前的锁删除掉。但是实际上,线程1自己的锁已经过期了,它删的是线程2的锁,即使现在线程2还没执行完。
  • 此时线程3来了,并且获得锁。现在又出现了线程并行的情况,可能出现线程安全问题。
  • 我们需要把判断锁和删除锁变成一个原子性的操作。

原子性问题

lua脚本解决多条命令原子性的问题

  • 提到原子性,可能想到事务。
  • 但是Redis的事务与MySQL有很大差别。
    • Redis事务能保证原子性,但不能保证一致性
    • Redis事务的多个操作其实是一个批处理,最终一次性执行。我们就无法先查询,再判断。因为查询拿不到值。我们只能通过乐观锁做判断,确保在释放的时候没有人修改。
    • 但是这是不推荐的。
  • 推荐使用lua脚本解决多条命令原子性的问题
Redis的Lua脚本

Redis提供了Lua脚本功能,在一个脚本中编写多条Redis命令,确保多条命令执行时的原子性。Lua是一种编程语言。
Redis提供的调用函数:

  • 执行redis命令
redis.call('命令名称','key','其他参数',...)
  • 例如要执行set name jack
redfis.call('set','name','jack')
  • 如果要先执行set name Rose,再执行get name,则脚本如下
redis.call('set','name','jack')
local name = redis.call('get','name')
return name

写好脚本以后,用Redis命令来调用脚本EVAL
EVAL

  • 例如我们要执行redfis.call(‘set’,‘name’,‘jack’)
EVAL "return redfis.call('set','name','jack')" 0

后面的0是传入参数的数量。
如果脚本中的key、value不想写死,可以作为参数传递。key类型参数会放入KEYS数组,其它参数会放入ARGV数组,再脚本中开业从KEYS和ARGV数组获取这些参数。
Lua脚本传参
释放锁的业务流程是这样的:

  1. 获取锁中的线程标识
  2. 判断是否与指定的标识(当前线程标识)一直
  3. 如果一直则释放锁(删除)
  4. 如果不一致则什么都不做
-- 比较线程标识与锁中的标识是否一致
if (redis.call('get',KEYS[1])== ARGV[1]) then
	-- 释放锁
	return redis.call('del',KEYS[1])
end
return 0

我们使用编写Lua脚本的方式再次改进Redis的分布式锁。
但是还存在一些问题。

  1. 不可重入:同一个线程无法多次获取同一把锁
  2. 不可重试:获取只尝试一次就返回false,没有重试机制
  3. 超时释放:锁超时释放熟练可以避免思索,单如果业务执行时间耗时较长,也会导致锁释放,存在安全隐患
  4. 主从一致性:如果Redis提供了主从集群,主从同步存在延迟,当主宕机时,如果从并未同步主中的锁数据,则可能出现安全问题。

Redisson

Redisson入门

Redisson是一个在Redis基础上实现的一个分布式工具的集合。

  1. 引入依赖
<dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.13.6</version>
        </dependency>
  1. 配置Redisson客户端
@Configuration
public class RedisConfig {
    @Bean
    public RedissonClient redissonClient() {
        // 配置类
        Config config = new Config();
        // 添加redis地址,这里添加了单点的地址,也可以使用config.userClusterSevers()添加集群地址
        config.useSingleServer().setAddress("redis://192.168.27.128:6379").setPassword("root");
        //创建客户端
        return Redisson.create(config);

    }
}
  1. 使用Redisson分布式锁
		//获取锁(可重入),指定锁的名称
        RLock lock = redissonClient.getLock("lockId“);
        //尝试获取锁,参数分别时:获取锁的最大等待时间(期间会重试),锁自动释放时间,时间单位
        boolean isLock = lock.tryLock();//无参代表失败不等待
        //判断是否获取成功
        if (!isLock) {
            //返回失败信息
            return fail;
        }
        try {
        	// 执行业务
        }finally {
            //释放锁
            lock.unlock();
        }
Redisson分布式锁原理
  • 可重入: 利用hash结构记录线程id和重入次数。重入的时候先判断锁是否已经存在,如果已经存在,也不代表获取锁失败,而是再判断锁的线程标识是不是自己,如果是自己,就将锁的重入次数加一,获取锁成功,不是自己获取失败。在释放锁的时候,每次释放将锁的重入次数减一,到减为零的时候,再真正去释放锁。
  • 可重试: 利用信号量和PubSub功能实现等待、唤醒,获取锁失败的重试机制。第一次尝试获取锁失败之后,不是立即返回失败,而是利用Redis的PubSub订阅释放锁的消息。占有锁的进程在释放锁的时候将会发布释放锁的消息。等待的线程得到消息之后,可以重新尝试获取锁。如果再次失败,可以继续等待,再次重试。当然这不是无限制的,当超过等待时间之后,就不再重试。这种等待重试的方式并不会过多占用CPU。
  • 超时续约:利用watchdog,每隔一段时间(releaseTime / 3,releaseTime默认为30s),重置超时时间。
    锁的获取重试与释放
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值