3.2.1Redisson介绍

Redisson是架设在Redis基础上的一个Java驻内存数据网格(In-Memory Data Grid)。充分的利用了Redis键值数据库提供的一系列优势,基于Java实用工具包中常用接口,为使用者提供了一系列具有分布式特性的常用工具类。使得原本作为协调单机多线程并发程序的工具包获得了协调分布式多机多线程并发系统的能力,大大降低了设计和研发大规模分布式系统的难度。同时结合各富特色的分布式服务,更进一步简化了分布式环境中程序相互之间的协作。

3.2.2Redisson简单使用

Config config = new Config();
 config.useClusterServers()
 .addNodeAddress(“redis://192.168.31.101:7001”)
 .addNodeAddress(“redis://192.168.31.101:7002”)
 .addNodeAddress(“redis://192.168.31.101:7003”)
 .addNodeAddress(“redis://192.168.31.102:7001”)
 .addNodeAddress(“redis://192.168.31.102:7002”)
 .addNodeAddress(“redis://192.168.31.102:7003”);RedissonClient redisson = Redisson.create(config);
RLock lock = redisson.getLock(“anyLock”);
lock.lock();
lock.unlock();

只需要通过它的 API 中的 Lock 和 Unlock 即可完成分布式锁,而且考虑了很多细节:

l Redisson 所有指令都通过 Lua 脚本执行,Redis 支持 Lua 脚本原子性执行

l Redisson 设置一个 Key 的默认过期时间为 30s,但是如果获取锁之后,会有一个WatchDog每隔10s将key的超时时间设置为30s。

另外,Redisson 还提供了对 Redlock 算法的支持,它的用法也很简单:

RedissonClient redisson = Redisson.create(config);
 RLock lock1 = redisson.getFairLock(“lock1”);
 RLock lock2 = redisson.getFairLock(“lock2”);
 RLock lock3 = redisson.getFairLock(“lock3”);
 RedissonRedLock multiLock = new RedissonRedLock(lock1, lock2, lock3);
 multiLock.lock();multiLock.unlock();

3.2.3Redisson原理分析

java redis订单加锁 redis如何加锁_数据库

(1) 加锁机制

线程去获取锁,获取成功: 执行lua脚本,保存数据到redis数据库。

线程去获取锁,获取失败: 一直通过while循环尝试获取锁,获取成功后,执行lua脚本,保存数据到redis数据库。

(2) WatchDog自动延期机制

在一个分布式环境下,假如一个线程获得锁后,突然服务器宕机了,那么这个时候在一定时间后这个锁会自动释放,也可以设置锁的有效时间(不设置默认30秒),这样的目的主要是防止死锁的发生。但是在实际情况中会有一种情况,业务处理的时间可能会大于锁过期的时间,这样就可能**导致解锁和加锁不是同一个线程。**所以WatchDog作用就是Redisson实例关闭前,不断延长锁的有效期。

如果程序调用加锁方法显式地给了有效期,是不会开启后台线程(也就是watch dog)进行延期的,如果没有给有效期或者给的是-1,redisson会默认设置30s有效期并且会开启后台线程(watch dog)进行延期

多久进行一次延期:(默认有效期/3),默认有效期可以设置修改的,即默认情况下每隔10s设置有效期为30s

(3) 可重入加锁机制

Redisson可以实现可重入加锁机制的原因:

l Redis存储锁的数据类型是Hash类型

l Hash数据类型的key值包含了当前线程的信息

下面是redis存储的数据

java redis订单加锁 redis如何加锁_java redis订单加锁_02

这里表面数据类型是Hash类型,Hash类型相当于我们java的 <key,<key1,value>> 类型,这里key是指 ‘redisson’

它的有效期还有9秒,我们再来看里们的key1值为078e44a3-5f95-4e24-b6aa-80684655a15a:45它的组成是:

guid + 当前线程的ID。后面的value是就和可重入加锁有关。value代表同一客户端调用lock方法的次数,即可重入计数统计。

举图说明

java redis订单加锁 redis如何加锁_数据库_03

上面这图的意思就是可重入锁的机制,它最大的优点就是相同线程不需要在等待锁,而是可以直接进行相应操作。

3.2.4 获取锁的流程

java redis订单加锁 redis如何加锁_java redis订单加锁_04

其中的指定字段也就是hash结构中的field值(构成是uuid+线程id),即判断锁是否是当前线程

3.2.5 加锁的流程

java redis订单加锁 redis如何加锁_java redis订单加锁_05

3.2.6 释放锁的流程

java redis订单加锁 redis如何加锁_redis_06

4. 使用Redis做分布式锁的缺点

Redis有三种部署方式

l 单机模式

l Master-Slave+Sentienl选举模式

l Redis Cluster模式

如果采用单机部署模式,会存在单点问题,只要 Redis 故障了。加锁就不行了

采用 Master-Slave 模式,加锁的时候只对一个节点加锁,即便通过 Sentinel 做了高可用,但是如果 Master 节点故障了,发生主从切换,此时就会有可能出现锁丢失的问题。

基于以上的考虑,Redis 的作者也考虑到这个问题,他提出了一个 RedLock 的算法。

这个算法的意思大概是这样的:假设 Redis 的部署模式是 Redis Cluster,总共有 5 个 Master 节点。

通过以下步骤获取一把锁:

  • 获取当前时间戳,单位是毫秒。
  • 轮流尝试在每个 Master 节点上创建锁,过期时间设置较短,一般就几十毫秒。
  • 尝试在大多数节点上建立一个锁,比如 5 个节点就要求是 3 个节点(n / 2 +1)。
  • 客户端计算建立好锁的时间,如果建立锁的时间小于超时时间,就算建立成功了。
  • 要是锁建立失败了,那么就依次删除这个锁。
  • 只要别人建立了一把分布式锁,你就得不断轮询去尝试获取锁。

但是这样的这种算法,可能会出现节点崩溃重启,多个客户端持有锁等其他问题,无法保证加锁的过程一定正确。例如:

假设一共有5个Redis节点:A, B, C, D, E。设想发生了如下的事件序列:

(1)客户端1成功锁住了A, B, C,获取锁成功(但D和E没有锁住)。

(2)节点C崩溃重启了,但客户端1在C上加的锁没有持久化下来,丢失了。

(3)节点C重启后,客户端2锁住了C, D, E,获取锁成功。

这样,客户端1和客户端2同时获得了锁(针对同一资源)。

5.在Springboot中集成redisson

5.1 引入依赖

org.redisson redisson-spring-boot-starter 3.13.3

如果引入以上的依赖,会丧失一定的配置灵活性

5.2 application.yml

java redis订单加锁 redis如何加锁_分布式_07

简单配置一下,此时启动应用就已经将redisson集成到了springboot中,此时的配置就是集群环境下的。如果需要单机和集群的切换,则需要引入redisson的依赖,自己编写配置类,这里为了简单,采用starter的方式集成

5.3测试

import lombok.extern.slf4j.Slf4j;
 import org.redisson.api.RLock;
 import org.redisson.api.RedissonClient;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.RestController;import java.util.concurrent.TimeUnit;
/**
• @Author Song
• @Date 2020/8/31 14:49
• @Version 1.0
• @Description
 */
 @Slf4j
 @RestController
 public class TestController {
 private static final String KEY = “mylock”;@Autowired
 private RedissonClient redissonClient;@GetMapping(“/lock1”)
 public String testLock1() {
 log.info(“lock1 正在获取锁。。。。”);
 RLock lock = redissonClient.getLock(KEY);
 lock.lock();
 log.info(Thread.currentThread().getName() + “:” + Thread.currentThread().getId() + " lock1 已经获取到锁");
 try {
 //模拟业务处理20s
 log.info(“正在进行业务处理”);
 TimeUnit.SECONDS.sleep(20);
 } catch (InterruptedException e) {
 e.printStackTrace();
 }
 lock.unlock();
 log.info(Thread.currentThread().getName() + “:” + Thread.currentThread().getId() + " lock1 已解锁");
 return “lock1”;
 }@GetMapping(“/lock2”)
 public String testLoc2() {
 log.info(“lock2 正在获取锁。。。。”);
 RLock lock = redissonClient.getLock(KEY);
 lock.lock();
 log.info(Thread.currentThread().getName() + “:” + Thread.currentThread().getId() + " lock2 已经获取到锁");
 try {
 //模拟业务处理20s
 log.info(“正在进行业务处理”);
 TimeUnit.SECONDS.sleep(20);
 } catch (InterruptedException e) {
 e.printStackTrace();
 }
 lock.unlock();
 log.info(Thread.currentThread().getName() + “:” + Thread.currentThread().getId() + " lock2 已解锁");
 return “lock2”;
 }
 }

然后平行地启动两个实例,分别访问localhost:8080/lock1和localhost:8081/lock2