一分钟了解分布式锁

 

目录

介绍

基于数据库(mysql)实现分布式锁

基于redis分布式锁

基于redission分布式锁


介绍

最近在拆微服务的过程中遇到了自己刚入职公司时接的一个需求。

需求:用户领取优惠券

一个用户课堂下,可以创建N个课程,用户可以基于课程创建优惠券(可以针对一节课创建多个优惠券,每个优惠券可以最多领取一次),优惠券创建时会指定一定的数量

拿到这个需求要做技术设计的时候,我们很容易的想到以下几个关键点:

  1. 优惠券的数量是一定的,不能超额发放
  2. 用户有且只能领取一张

这个时候作为一名职业的码农,我们能很快的想到一个关键的问题就是,高并发的情况下去领取优惠券,如何能保证上面两个关键点?

我们假设创建了a课程的一种优惠券总数是s,那么sum就是一个并发中的资源共享变量,那么我们的逻辑简单就是

int sum = 0;  // 共享变量

if(用户已经领取了优惠券 || sum <= 0) {
    
    return;

} else {
   
    【sum--】

}

 

【sum--】当发生并发时这个怎么处理?无非不过加【锁】 呗,加锁的目的是让本应该并发的操作,线下执行,这样就不会有共享资源变量竞争问题,在分布式系统性有以下几种加锁方式

一分钟了解分布式锁_第1张图片

【注意】单机不考虑(synchronized),这里主要讨论分布式

redis分布式锁(本篇重点讨论)

zookper分布式锁(本篇不讨论)

基于数据库(mysql)实现分布式锁

通过乐观锁和悲观锁的方式实现

乐观锁:不做详细讲解,不明白的可以去看一下,点击此处

悲观锁:mysql使用innodb存储引擎是支持行锁的

select  ... from table where ... for update;

update table set ....

缺点:上面两种方式因为依赖于数据库,明显存在性能问题,生产环境中像领取优惠券类似的高并发场景不推荐使用

优点:依赖mysql,编写简单,维护容易,不需要引入其他技术

基于redis分布式锁

使用redis实现分布式锁,redis本身提供了setnx这个命令,但是让我们自己去实现会有很多坑在里面,下面我们就来看看这些坑。

下图代码是简单的实现了一个领取优惠券的代码:

一分钟了解分布式锁_第2张图片

大家看看上面代码有什么问题?有的小伙伴说,你这个效率太低,第一个请求进来后程序执行到

Boolean isExist = redisTemplate.opsForValue().
        setIfAbsent(key, String.valueOf(System.currentTimeMillis()));

后,其他线程都没办进来了,本来优惠券还有很多,直接就返回了,不能领取,这里可以使用一个while循环,去抢锁,或者可以利用分段锁的思想去做,提高并发。

分段锁:把一张优惠券分成多张去领取,比如:优惠券A,一共100张,我们的key就可以设计成,A_1,  A_2,  A_3,  A_4 每个25张

除了上述问题,还有一个很关键的问题就是:

Boolean isExist = redisTemplate.opsForValue().
        setIfAbsent(key, String.valueOf(System.currentTimeMillis())); // 第一步
redisTemplate.expire(key, 10, TimeUnit.SECONDS);// 第二步

这两行代码不是原子的,假如我系统直接宕机在执行了第一步后,这个时候我们的锁就没办法释放,解决办法也很简单

Boolean isExist = redisTemplate.opsForValue().
        setIfAbsent(key, String.valueOf(System.currentTimeMillis()), 10, TimeUnit.SECONDS);

API提供给我们一个原子的操作,这样不就可以了吗?现在我们的代码换成了下面,这个时候大家看看还有问题吗?

一分钟了解分布式锁_第3张图片

我们看下面一种情况,假如我们的【其他业务操作】执行出于某种原因(慢sql或者服务器卡顿等等)执行时间太长,超过我们预定的可以过期时间10s,那么就会出现下图的情况,当前线程释放了本应属于其他线程的key。

一分钟了解分布式锁_第4张图片

聪明的伙伴一下子就知道如何处理当前情况,在处理删除的时候判断一下是不是当前线程生成的value不就可以了,所以代码又跟新为下面。

一分钟了解分布式锁_第5张图片

但是这样子并没有解决我们的key超时过期问题啊,还是会有其他线程进来,你是不是在考虑我们把10s设置成20s,30s,100s,但是这样并没有解决问题的本质,就算100s真的可以,那么服务宕机后其他线程要等好久key才会过期,其实这里的解决办法是:锁续命

锁续命】:当线程开始执行时,我们在fork一个子线程去监控当前线程,比如我们的过期时间设置的是10s,我们每过5s,去给当前key,续命成10s,直到线程执行结束。

这样是不是很麻烦啊,写好多代码去维护,其实redission帮助我们解决了上面的问题,所以我们直接使用就行了,别自己去实现,还有的小伙伴脑子里现在考虑,你这太啰嗦了,我写个lua脚本不就行了,lua脚本确实能保证原子的去执行,还是老问题,~麻烦~,我们接下来使用redisson看看这里怎么实现。

基于redission分布式锁

我们先来看redis官方对于分布式锁的描述:官网也推荐我们,当使用java开发时,使用redisson。

一分钟了解分布式锁_第6张图片

一分钟了解分布式锁_第7张图片

官方针对各种语言都提供了对应的分布式锁实现,下面提供了java implementation + spring boot 的整合链接,有兴趣的可以去看一下。

redisson-spring-boot-starter Redisson Spring Boot 脚手架

【注意】:当利用redis实现分布式锁时,推荐使用Redisson,框架帮我们避免了大多数坑,可以通过阅读其底层实现学习。

下面使用redisson来改善一下我们的既有代码,如下图:

redisson:是Redisson的实例;

一分钟了解分布式锁_第8张图片

看完代码,是不是觉得很简单,大家会特别疑惑,就这几行代码就能处理掉我们上面说的那么多问题吗,其实redisson框架本身不简单,其中实现分布式锁一块,底层也是使用lua脚本来实现的。

下面是redission实现分布式锁的原理图:基本上就是我们上面分析的,其实有兴趣的小伙伴去读源码

一分钟了解分布式锁_第9张图片

第一次写博客有什么不到位的地方或者问题,大家多多见谅~,私底下可以联系我。

你可能感兴趣的:(项目构建,redis专栏,java,spring)