Redis分布式锁

分布式锁背景分析

在学习一个新技术时,建议大家最好从以下两个方面出发,渐进式学习,可以更好地帮助我们滤清思路。拿学习分布式锁举例,请大家先记住这两个问题:

  • 为什么要使用分布式锁?
  • 分布式锁解决了什么问题?

在一些业务中,分布式应用进行逻辑处理时经常会遇到并发问题。比如一个操作要修改用户的状态,修改状态需要先读出用户的状态,在内存里进行修改,改完了再存回去。如果这样的操作同时进行了,就会出现并发问题,因为读取和保存状态这两个操作不是原子的。

这个时候就要使用到分布式锁来限制程序的并发执行。理解了这个场景,也就理解了上面两个问题了。

(那么原子是什么意思呢?
在 Wiki 中是这样解释的:所谓原子操作是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何 context switch 线程切换。)

分布式锁

分布式锁本质上要实现的目标就是在 Redis 里面占一个“位置”,当别的进程也要来占时,发现已经有人在这个位置了,

举个例子: 某天你的饭卡丢失了,这时候你要去学校里唯一的一台自助补卡机去补卡,你到了之后会有两种情况:

  1. 如果没有人在使用,那么你就可以直接使用,并且占了这个位置,其他人只能等你使用完。
  2. 如果人正在使用,那么你就只好放弃或者等等再试了。

实现思路一

最简易的,我们可以使用 setnx(set if not exists) 指令,在开始逻辑时设置一个特殊 key 执行完毕后删除释放 key ,执行任务时只需要判断这个特殊 key 是否已经被创造了,如果被创造了就代表有人正在执行逻辑,你就需要等一等。

//尝试创建特殊 key ,根据返回值判断是否创造失败,失败则已存在
Boolean flag = redisTemplate.opsForValue().setIfAbsent(key, value, 10, TimeUnit.SECONDS);
if (!flag) {
	//失败
    return "加锁失败";
} else {
	//成功,执行相关逻辑
	//并删除 key
}

但是这种思路有个很明显的缺点,如果逻辑执行到中间出现异常了,可能会导致删除 key 指令没有被调用,这样就会陷入死锁,锁永远得不到释放。

实现思路二

为了改进思路一的缺点,我们通常会选择 expire 指令给这个特殊 key 加上一个过期时间,如果逻辑出现异常无法调用删除指令,也能在超时后自动释放锁。

这里要注意,不要把 setnx 和 expire 分成两条指令写,因为这样如果系统在刚执行完 setnx 指令后挂掉,expire 得不到执行还是会出现无法释放问题。(在 Redis 2.8 版本之前为了解决此问题需要耗费很多精力)在 Redis 2.8 版本之后官方为了解决这个问题,允许 setnx 和 expire 合并为一条指令执行。

但是这种思路又面临新的问题,不能解决超时问题,如果在加锁和释放锁之间的逻辑执行的太长,以至于超出了锁的超时限制,就会出现问题。因为这时候第一个线程持有的锁过期了,临界区的逻辑还没有执行完,这个时候第二个线程就提前重新持有了这把锁,导致临界区代码不能得到严格的串行执行。

tag = random.nextint()  # 随机数
if redis.set(key, tag, nx=True, ex=5):
    do_something()
    redis.delifequals(key, tag)  # 假想的 delifequals 指令

有一个稍微安全一点的方案是为 set 指令的 value 参数设置为一个随机数,释放锁时先匹配随机数是否一致,然后再删除 key,这是为了确保当前线程占有的锁不会被其它线程释放,除非这个锁是过期了被服务器自动释放的。 但是匹配 value 和删除 key 不是一个原子操作,Redis 也没有提供类似于delifequals这样的指令,这就需要使用 Lua 脚本来处理了,因为 Lua 脚本可以保证连续多个指令的原子性执行。

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

实现思路三

上面说了这么多,大家看了肯定觉得:“这也太繁琐了吧?”,没错,上面只是简单的分布式锁实现,甚至还没涉及到可重入性设计,从头设计一个分布式锁相当麻烦的,所以我推荐大家用现成的轮子来实现分布式锁,使用 Redisson 来实现分布式锁是目前最好的选择。

Redisson 是什么?

Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务。
Redisson提供了使用Redis的最简单和最便捷的方法。Redisson的宗旨是促进使用者对Redis的关注分离(Separation of Concern),从而让使用者能够将精力更集中地放在处理业务逻辑上。

springboot 集成 Redisson

集成很简单,只需要两步

  1. 引入 pom 依赖
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
</dependency>
  1. application.yml增加redis配置
spring:
  application:
    name: test
  redis:
    host: 127.0.0.1
    port: 6379

使用起来也很简单,只需要注入RedissonClient即可

public class RedissonTest {

    @Resource
    RedissonClient redissonClient;

    public void test() {
        RLock rLock = redissonClient.getLock("anyKey");
        rLock.lock();
        try {
            // do something
        } catch (Exception e) {
            log.error("业务异常", e);
        } finally {
            rLock.unlock();
        }

    }
    
}

可能不了解redisson的小伙伴会不禁发出疑问。
what?加锁时不需要加过期时间吗?这样会不会导致死锁啊。解锁不需要判断是不是自己持有吗?别担心,这些已经都被 Redisson 巧妙地设计给解决了,我们只需要使用就可以了。想深入了解 Redisson的话我之后会在出一篇文章来把源码哦!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值