目录
5.8 Redisson和Jedis、Lettuce有什么区别?
1、为什么需要分布式锁
在单机环境,我们使用最多的是juc包里的单机锁,但是随着微服务分布式项目的普及,juc里的锁是不能控制分布锁环境的线程安全的,因为单机锁只能控制同个进程里的线程安全,不能控制多节点的线程安全,所以就需要使用分布式锁。
我们来假设一个最简单的秒杀场景:数据库里有一张表,column分别是商品ID,和商品ID对应的库存量,秒杀成功就将此商品库存量-1。现在假设有1000个线程来秒杀两件商品,500个线程秒杀第一个商品,500个线程秒杀第二个商品。我们来根据这个简单的业务场景来解释一下分布式锁。
通常具有秒杀场景的业务系统都比较复杂,承载的业务量非常巨大,并发量也很高。这样的系统往往采用分布式的架构来均衡负载。那么这1000个并发就会是从不同的地方过来,商品库存就是共享的资源,也是这1000个并发争抢的资源,这个时候我们需要将并发互斥管理起来。这就是分布式锁的应用。
2、redis分布式锁原理
学习之前先了解redis的命令,setnx
和expire
setnx命令
SETNX是SET if not exists的简写,设置key的值,如果key值不存在,则可以设置,否则不可以设置,这个有点像juc中cas锁的原理
# setnx命令,相当于set和nx命令一起用
setnx tkey aaa
EX : 设置指定的到期时间(以秒为单位)。
PX : 设置指定的到期时间(以毫秒为单
NX : 仅在键不存在时设置键。
XX : 只有在键已存在时才设置。
expire命令
如果只使用setnx
不加上过期时间,手动释放锁时候出现异常,就会导致一直解不了锁,所以还是要加上expire
命令来设置过期时间。
- 保证原子性
但是又有一个问题,设置过期时间时候报错了,也同样会导致锁释放不了,所以为了保证原子性,需要这两个命令一起执行
expire命令
如果只使用setnx不加上过期时间,手动释放锁时候出现异常,就会导致一直解不了锁,所以还是要加上expire命令来设置过期时间。
保证原子性
但是又有一个问题,设置过期时间时候报错了,也同样会导致锁释放不了,所以为了保证原子性,需要这两个命令一起执行
分布式锁实现原则
- 互斥性,同一时刻,智能有一个客户端持有锁。
- 防止死锁发生,如果持有锁的客户端崩溃没有主动释放锁,也要保证锁可以正常释放及其他客户端可以正常加锁。
- 加锁和释放锁必须是同一个客户端。
- 容错性,只有redis还有节点存活,就可以进行正常的加锁解锁操作。
补充知识
Redis 2.6.12 之前的版本中采用 setnx + expire 方式实现分布式锁,在 Redis 2.6.12 版本后 setnx 增加了过期时间参数。
3,Jedis实现分布式锁
引入Jedis jar包,在pom.xml文件增加代码:
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
调用jedis的set()实现加锁,加锁代码如下:
/**
* @description:
* @author: 程序员大彬
* @time: 2021-08-01 17:13
*/
public class RedisTest {
private static final String LOCK_SUCCESS = "OK";
private static final String SET_IF_NOT_EXIST = "NX";
private static final String SET_EXPIRE_TIME = "PX";
@Autowired
private JedisPool jedisPool;
public boolean tryGetDistributedLock(String lockKey, String requestId, int expireTime) {
Jedis jedis = jedisPool.getResource();
String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_EXPIRE_TIME, expireTime);
if (LOCK_SUCCESS.equals(result)) {
return true;
}
return false;
}
}
各参数说明:
- lockKey:使用key来当锁,需要保证key是唯一的。可以使用系统号拼接自定义的key。
- requestId:表示这把锁是哪个请求加的,可以使用机器ip拼接当前线程名称。在解锁的时候需要判断当前请求是否持有锁,防止误解锁。比如客户端A加锁,在执行解锁之前,锁过期了,此时客户端B尝试加锁成功,然后客户端A再执行del()方法,则将客户端B的锁给解除了。
- NX:意思是SET IF NOT EXIST,保证如果已有key存在,则不作操作,过段时间继续重试。NX参数保证只有一个客户端能持有锁。
- PX:给key加一个过期的设置,具体时间由expireTime决定。
- expireTime:设置key的过期时间,防止异常导致锁没有释放。
解锁
首先需要获取锁对应的value值,检查是否与requestId相等,如果相等则删除锁。这里使用lua脚本实现原子操作,保证线程安全。
使用eval命令执行Lua脚本的时候,不会有其他脚本或 Redis 命令被执行,实现组合命令的原子操作。lua脚本如下:
//KEYS[1]是lockKey,ARGV[1]是requestId
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
Jedis的eval()方法源码如下:
public Object eval(String script, List<String> keys, List<String> args) {
return this.eval(script, keys.size(), getParams(keys, args));
}
lua脚本的意思是:调用get获取锁(KEYS[1])对应的value值,检查是否与requestId(ARGV[1])相等,如果相等则调用del删除锁。否则返回0。
完整的解锁代码如下:
public class RedisTest {
private static final Long RELEASE_SUCCESS = 1L;
@Autowired
private JedisPool jedisPool;
public boolean releaseDistributedLock(String lockKey, String requestId) {
Jedis jedis = jedisPool.getResource();
////KEYS[1]是lockKey,ARGV[1]是requestId
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
if (RELEASE_SUCCESS.equals(result)) {
return true;
}
return false;
}
}
完整的工具类如下:
package com.shuangyueliao.shuangcloud.redislock;
import redis.clients.jedis.Jedis;
import java.util.Collections;
public class RedisTool {
private static final String LOCK_SUCCESS = "OK";
private static final String SET_IF_NOT_EXIST = "NX";
private static final String SET_WITH_EXPIRE_TIME = "PX";
/**
* 尝试获取分布式锁
*
* @param jedis Redis客户端
* @param lockKey 锁
* @param requestId 请求标识
* @param expireTime 超期时间
* @return 是否获取成功
*/
public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {
String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
if (LOCK_SUCCESS.equals(result)) {
return true;
}
return false;
}
private static final Long RELEASE_SUCCESS = 1L;
/**
* 释放分布式锁
*
* @param jedis Redis客户端
* @param lockKey 锁
* @param requestId 请求标识
* @return 是否释放成功
*/
public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
if (RELEASE_SUCCESS.equals(result)) {
return true;
}
return false;
}
}
4,分布式锁的案例演进
4.1. 单机数据一致性
单机数据一致性架构如下图所示:多个可客户访问同一个服务器,连接同一个数据库。
场景描述:
客户端模拟购买商品过程,在Redis中设定库存总数剩100个,多个客户端同时并发购买。
设置商品库存:
编写后端代码:
实际测试结果如下:
测试结果出现多个用户购买同一商品,发生了数据不一致问题!
解决办法:单体应用的情况下,对并发的操作进行加锁操作,保证对数据的操作具有原子性
1,synchronized
2,ReentrantLock
测试结果如下:
4.2 分布式数据一致性
上面解决了单体应用的数据一致性问题,但如果是分布式架构部署呢,架构如下:
提供两个服务,端口分别为8001
、8002
,连接同一个Redis
服务,在服务前面有一台Nginx
作为负载均衡。
两台服务代码相同,只是端口不同
将8001
、8002
两个服务启动,每个服务依然用ReentrantLock
加锁,用Jmeter
做并发测试,发现会出现数据一致性问题!
解决方式一:
取消单机锁,下面使用redis
的set
命令来实现分布式加锁
SET KEY VALUE [EX seconds] [PX milliseconds] [NX|XX]
- EX seconds 设置指定的到期时间(以秒为单位)
- PX milliseconds 设置指定的到期时间(以毫秒为单位)
- NX 仅在键不存在时设置键
- XX 只有在键已存在时才设置
上面的代码,可以解决分布式架构中数据一致性问题。但再仔细想想,还是会有问题,下面进行改进。
方式二(改进方式一)
在上面的代码中,如果程序在运行期间,部署了微服务jar
包的机器突然挂了,代码层面根本就没有走到finally
代码块,也就是说在宕机前,锁并没有被删除掉,这样的话,就没办法保证解锁,所以,这里需要对这个key
加一个过期时间,Redis
中设置过期时间有两种方法:
template.expire(REDIS_LOCK,10, TimeUnit.SECONDS)
template.opsForValue().setIfAbsent(REDIS_LOCK, value,10L,TimeUnit.SECONDS)
第一种方法需要单独的一行代码,且并没有与加锁放在同一步操作,所以不具备原子性,也会出问题。
第二种方法在加锁的同时就进行了设置过期时间,所有没有问题,这里采用这种方式
调整下代码,在加锁的同时,设置过期时间:
这种方式解决了因服务突然宕机而无法释放锁的问题。但再仔细想想,还是会有问题,下面进行改进。
方式三(改进方式二)
方式二设置了key
的过期时间,解决了key
无法删除的问题,但问题又来了,上面设置了key
的过期时间为10
秒,如果业务逻辑比较复杂,需要调用其他微服务,处理时间需要15
秒(模拟场景,别较真),而当10
秒钟过去之后,这个key
就过期了,其他请求就又可以设置这个key
,此时如果耗时15
秒的请求处理完了,回来继续执行程序,就会把别人设置的key
给删除了,这是个很严重的问题!
所以,谁上的锁,谁才能删除
这种方式解决了因服务处理时间太长而释放了别人锁的问题。这样就没问题了吗?
方式四(改进方式三)
在上面方式三下,规定了谁上的锁,谁才能删除,但finally
快的判断和del
删除操作不是原子操作,并发的时候也会出问题,并发嘛,就是要保证数据的一致性,保证数据的一致性,最好要保证对数据的操作具有原子性。在Redis
的set
命令介绍中,最后推荐Lua
脚本进行锁的删除。
因为获取lock是一个TCP/IP请求,删除delete又是一个网络请求,两者之间存在时间差,如果网络异常等原因导致,不能保障原子操作。
方式五(改进方式四)
在方式四下,规定了谁上的锁,谁才能删除,并且解决了删除操作没有原子性问题。但还没有考虑缓存续命,以及Redis
集群部署下,异步复制造成的锁丢失:主节点没来得及把刚刚set
进来这条数据给从节点,就挂了。所以直接上RedLock
的Redisson
落地实现。
5,redisson分布式锁
5.1 什么是 Redisson
Redisson - 是一个高级的分布式协调Redis客服端,能帮助用户在分布式环境中轻松实现一些Java的对象,Redisson、Jedis、Lettuce 是三个不同的操作 Redis 的客户端。
Jedis、Lettuce 的 API 更侧重对 Reids 数据库的 CRUD(增删改查)。
Redisson API 侧重于分布式开发,redisson实现的分布式锁,底层是setnx、expire和lua脚本(保证原子性)。
相比于 Jedis、Lettuce 等基于 redis 命令封装的客户端,Redisson 提供的功能更加高端和抽象,逼格高!
底层原理:
Redisson底层Redis客户端和Lettuce一样基于Netty框架实现
// 在使用多个客户端的情况下可以共享同一个EventLoopGroup
EventLoopGroup group = new NioEventLoopGroup();
RedisClientConfig config = new RedisClientConfig();
config.setAddress("redis://localhost:6379") // 或者用rediss://使用加密连接
.setPassword("myPassword")
.setDatabase(0)
.setClientName("myClient")
.setGroup(group);
RedisClient client = RedisClient.create(config);
RedisConnection conn = client.connect();
// 或
RFuture<RedisConnection> connFuture = client.connectAsync();
conn.sync(StringCodec.INSTANCE, RedisCommands.SET, "test", 0);
// 或
conn.async(StringCodec.INSTANCE, RedisCommands.GET, "test");
conn.close()
// 或
conn.closeAsync()
client.shutdown();
// 或
client.shutdownAsync();
Redisson在底层采用了高性能异步非阻塞式Java客户端,它同时支持异步和同步两种通信模式。如果有哪些命令Redisson还没提供支持,也可以直接通过调用底层Redis客户端来实现。Redisson支持的命令在Redis命令和Redisson对象匹配列表里做了详细对比参照。
5.2 添加Redisson
依赖包
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.15.0</version>
</dependency>
单机环境下,简单样例如下!
public class RedissonMain {
public static void main(String[] args) {
Config config = new Config();
config.useSingleServer()
.setAddress("redis://127.0.0.1:6379")
.setPassword("123456")
.setDatabase(0);
//获取客户端
RedissonClient redissonClient = Redisson.create(config);
//获取所有的key
redissonClient.getKeys().getKeys().forEach(key -> System.out.println(key));
//关闭客户端
redissonClient.shutdown();
}
}
5.3 支持Redis多种连接模式
5.3.1 单例模式
RedissonClient redisson = Redisson.create();
Config config = new Config();
config.useSingleServer().setAddress("myRedisServer:6379");
RedissonClient redisson = Redisson.create(config);
5.3.2 集群模式
Config config = new Config();
config.useClusterServers()
.setScanInterval(2000) // 集群状态扫描间隔时间,单位是毫秒
//可以用"rediss://"来启用SSL连接
.addNodeAddress("redis://127.0.0.1:7000", "redis://127.0.0.1:7001")
.addNodeAddress("redis://127.0.0.1:7002");
RedissonClient redisson = Redisson.create(config);
5.3.3 哨兵模式
Config config = new Config();
config.useSentinelServers()
.setMasterName("mymaster")
.addSentinelAddress("redis://127.0.0.1:26389", "redis://127.0.0.1:26379")
.addSentinelAddress("redis://127.0.0.1:26319");
RedissonClient redisson = Redisson.create(config);
5.3.4 主从模式
Config config = new Config();
config.useMasterSlaveServers()
.setMasterAddress("redis://127.0.0.1:6379")
.addSlaveAddress("redis://127.0.0.1:6389", "redis://127.0.0.1:6332", "redis://127.0.0.1:6419")
.addSlaveAddress("redis://127.0.0.1:6399");
RedissonClient redisson = Redisson.create(config);
5.4 Redisson操作Redis基本对象
使用 Redisson 操作 Redis 中的字符串、哈希、列表、集合、有序集合,以及布隆过滤器和分布式锁等功能。
5.4.1 字符串操作
Redisson 支持通过RBucket
对象来操作字符串数据结构,通过RBucket
实例可以设置value
或设置value
和有效期,简单样例如下!
//字符串操作
RBucket<String> rBucket = redissonClient.getBucket("strKey");
// 设置value和key的有效期
rBucket.set("张三", 30, TimeUnit.SECONDS);
// 通过key获取value
System.out.println(redissonClient.getBucket("strKey").get());
5.4.2 对象操作
Redisson 支持将对象作为value
存入redis
,被存储的对象事先必须要实现序列化接口Serializable
,否则会报错,简单样例如下!
public class Student implements Serializable {
private Long id;
private String name;
private Integer age;
//set、get...
@Override
public String toString() {
return "Student{" +
"id=" + id +
", name='" + name + '\'' +
", age=" + age +
'}';
}
}
//Student对象
Student student = new Student();
student.setId(1L);
student.setName("张三");
student.setAge(18);
//对象操作
RBucket<Student> rBucket = redissonClient.getBucket("objKey");
// 设置value和key的有效期
rBucket.set(student, 30, TimeUnit.SECONDS);
// 通过key获取value
System.out.println(redissonClient.getBucket("objKey").get());
5.4.3 哈希操作
Redisson 支持通过RMap
对象来操作哈希数据结构,简单样例如下!
//哈希操作
RMap<String, String> rMap = redissonClient.getMap("mapkey");
// 设置map中key-value
rMap.put("id", "123");
rMap.put("name", "赵四");
rMap.put("age", "50");
//设置过期时间
rMap.expire(30, TimeUnit.SECONDS);
// 通过key获取value
System.out.println(redissonClient.getMap("mapkey").get("name"));
5.4.4 列表操作
Redisson 支持通过RList
对象来操作列表数据结构,简单样例如下!
//字符串操作
RList<Student> rList = redissonClient.getList("listkey");
Student student1 = new Student();
student1.setId(1L);
student1.setName("张三");
student1.setAge(18);
rList.add(student1);
Student student2 = new Student();
student2.setId(2L);
student2.setName("李四");
student2.setAge(19);
rList.add(student2);
//设置过期时间
rList.expire(30, TimeUnit.SECONDS);
// 通过key获取value
System.out.println(redissonClient.getList("listkey"));
5.4.5 Set集合操作
Redisson 支持通过RSet
对象来操作集合数据结构,简单样例如下!
//字符串操作
RSet<Student> rSet = redissonClient.getSet("setkey");
Student student1 = new Student();
student1.setId(1L);
student1.setName("张三");
student1.setAge(18);
rSet.add(student1);
Student student2 = new Student();
student2.setId(2L);
student2.setName("李四");
student2.setAge(19);
rSet.add(student2);
//设置过期时间
rSet.expire(30, TimeUnit.SECONDS);
// 通过key获取value
System.out.println(redissonClient.getSet("setkey"));
5.4.6 有序Set集合操作
Redisson 支持通过RSortedSet
对象来操作有序集合数据结构,在使用对象来存储之前,实体对象必须先实现Comparable
接口,并重写比较逻辑,否则会报错,简单样例如下!
public class Student implements Serializable, Comparable<Student> {
private Long id;
private String name;
private Integer age;
//get、set.....
@Override
public String toString() {
return "Student{" +
"id=" + id +
", name='" + name + '\'' +
", age=" + age +
'}';
}
@Override
public int compareTo(Student obj) {
return this.getId().compareTo(obj.getId());
}
}
//有序集合操作
RSortedSet<Student> sortSetkey = redissonClient.getSortedSet("sortSetkey");
Student student1 = new Student();
student1.setId(1L);
student1.setName("张三");
student1.setAge(18);
sortSetkey.add(student1);
Student student2 = new Student();
student2.setId(2L);
student2.setName("李四");
student2.setAge(19);
sortSetkey.add(student2);
// 通过key获取value
System.out.println(redissonClient.getSortedSet("sortSetkey"));
5.4.7 BitSet集合
RBitSet set = redisson.getBitSet("simpleBitset");
set.set(0, true);
set.set(1812, false);
set.clear(0);
set.addAsync("e");
set.xor("anotherBitset");
5.4.8 Blocking Queue
还支持Queue, Deque, Blocking Deque
RBlockingQueue<SomeObject> queue = redisson.getBlockingQueue("anyQueue");
queue.offer(new SomeObject());
SomeObject obj = queue.peek();
SomeObject someObj = queue.poll();
SomeObject ob = queue.poll(10, TimeUnit.MINUTES);
5.4.9 布隆过滤器
布隆过滤器(Bloom Filter)是 1970 年由布隆提出的。它实际上是一个很长的二进制向量和一系列随机映射函数。
布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都比一般的算法要好的多,缺点是有一定的误识别率和删除困难。
Redisson 支持通过RBloomFilter
对象来操作布隆过滤器,简单样例如下!
RBloomFilter rBloomFilter = redissonClient.getBloomFilter("seqId");
// 初始化预期插入的数据量为10000和期望误差率为0.01
rBloomFilter.tryInit(10000, 0.01);
// 插入部分数据
rBloomFilter.add("100");
rBloomFilter.add("200");
rBloomFilter.add("300");
//设置过期时间
rBloomFilter.expire(30, TimeUnit.SECONDS);
// 判断是否存在
System.out.println(rBloomFilter.contains("300"));
System.out.println(rBloomFilter.contains("200"));
System.out.println(rBloomFilter.contains("999"));
5.4.10 分布式自增ID
ID 是数据的唯一标识,传统的做法是利用 UUID 和数据库的自增 ID。
但由于 UUID 是无序的,不能附带一些其他信息,因此实际作用有限。
随着业务的发展,数据量会越来越大,需要对数据进行分表,甚至分库。分表后每个表的数据会按自己的节奏来自增,这样会造成 ID 冲突,因此这时就需要一个单独的机制来负责生成唯一 ID,redis 原生支持生成全局唯一的 ID。
简单样例如下!
final String lockKey = "aaaa";
//通过redis的自增获取序号
RAtomicLong atomicLong = redissonClient.getAtomicLong(lockKey);
//设置过期时间
atomicLong.expire(30, TimeUnit.SECONDS);
// 获取值
System.out.println(atomicLong.incrementAndGet());
5.5 Redisson分布式锁
Redisson 最大的亮点,也是使用最多的功能,就是提供了强大的分布式锁实现,特点是:使用简单、安全!
Config config = new Config();
config.useSingleServer()
.setAddress("redis://127.0.0.1:6379")
.setPassword("123456")
.setDatabase(0);
RedissonClient redissonClient = Redisson.create(config);
//获取锁对象实例
final String lockKey = "abc";
RLock rLock = redissonClient.getLock(lockKey);
try {
//尝试5秒内获取锁,如果获取到了,最长60秒自动释放
boolean res = rLock.tryLock(5L, 60L, TimeUnit.SECONDS);
if (res) {
//成功获得锁,在这里处理业务
System.out.println("获取锁成功");
}
} catch (Exception e) {
System.out.println("获取锁失败,失败原因:" + e.getMessage());
} finally {
//无论如何, 最后都要解锁
rLock.unlock();
}
//关闭客户端
redissonClient.shutdown();
5.5.1 可重入锁Reentrant Lock可重入锁
基于Redis的Redisson分布式可重入锁RLock Java对象实现了java.util.concurrent.locks.Lock接口。同时还提供了异步(Async)、反射式(Reactive)和RxJava2标准的接口。
RLock lock = redisson.getLock("anyLock");
// 最常见的使用方法
lock.lock();
大家都知道,如果负责储存这个分布式锁的Redisson节点宕机以后,而且这个锁正好处于锁住的状态时,这个锁会出现锁死的状态。为了避免这种情况的发生,Redisson内部提供了一个监控锁的看门狗,它的作用是在Redisson实例被关闭前,不断的延长锁的有效期。默认情况下,看门狗的检查锁的超时时间是30秒钟,也可以通过修改Config.lockWatchdogTimeout来另行指定。
另外Redisson还通过加锁的方法提供了leaseTime的参数来指定加锁的时间。超过这个时间后锁便自动解开了。
// 加锁以后10秒钟自动解锁
// 无需调用unlock方法手动解锁
lock.lock(10, TimeUnit.SECONDS);
// 尝试加锁,最多等待100秒,上锁以后10秒自动解锁
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
if (res) {
try {
...
} finally {
lock.unlock();
}
}
Redisson同时还为分布式锁提供了异步执行的相关方法:
RLock lock = redisson.getLock("anyLock");
lock.lockAsync();
lock.lockAsync(10, TimeUnit.SECONDS);
Future<Boolean> res = lock.tryLockAsync(100, 10, TimeUnit.SECONDS);
RLock对象完全符合Java的Lock规范。也就是说只有拥有锁的进程才能解锁,其他进程解锁则会抛出IllegalMonitorStateException错误。但是如果遇到需要其他进程也能解锁的情况,请使用分布式信号量Semaphore 对象.
@ResponseBody
@GetMapping(value = "/hello")
public String hello() {
//1、获取一把锁,只要锁的名字一样,就是同一把锁
RLock myLock = redisson.getLock("my-lock");
//2、加锁
myLock.lock(); //阻塞式等待。默认加的锁都是30s
//1)、锁的自动续期,如果业务超长,运行期间自动锁上新的30s。不用担心业务时间长,锁自动过期被删掉
//2)、加锁的业务只要运行完成,就不会给当前锁续期,即使不手动解锁,锁默认会在30s内自动过期,不会产生死锁问题
// myLock.lock(10,TimeUnit.SECONDS); //10秒钟自动解锁,自动解锁时间一定要大于业务执行时间
//问题:在锁时间到了以后,不会自动续期(不会启动看门狗机制)
//1、如果我们传递了锁的超时时间,就发送给redis执行脚本,进行占锁,默认超时就是 我们指定的时间
//2、如果我们未指定锁的超时时间,就使用 lockWatchdogTimeout = 30 * 1000 【看门狗默认时间】
//只要占锁成功,就会启动一个定时任务【重新给锁设置过期时间,新的过期时间就是看门狗的默认时间】,每隔10秒都会自动的再次续期,续成30秒
// internalLockLeaseTime 【看门狗时间】 / 3, 10s
try {
System.out.println("加锁成功,执行业务..." + Thread.currentThread().getId());
try { TimeUnit.SECONDS.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); }
} catch (Exception ex) {
ex.printStackTrace();
} finally {
//3、解锁 假设解锁代码没有运行,Redisson会不会出现死锁
System.out.println("释放锁..." + Thread.currentThread().getId());
myLock.unlock();
}
return "hello";
}
5.5.2 公平锁(Fair Lock)
基于Redis的Redisson分布式可重入公平锁也是实现了java.util.concurrent.locks.Lock接口的一种RLock对象。同时还提供了异步(Async)、反射式(Reactive)和RxJava2标准的接口。它保证了当多个Redisson客户端线程同时请求加锁时,优先分配给先发出请求的线程。所有请求线程会在一个队列中排队,当某个线程出现宕机时,Redisson会等待5秒后继续下一个线程,也就是说如果前面有5个线程都处于等待状态,那么后面的线程会等待至少25秒。
RLock fairLock = redisson.getFairLock("anyLock");
// 最常见的使用方法
fairLock.lock();
大家都知道,如果负责储存这个分布式锁的Redis节点宕机以后,而且这个锁正好处于锁住的状态时,这个锁会出现锁死的状态。为了避免这种情况的发生,Redisson内部提供了一个监控锁的看门狗,它的作用是在Redisson实例被关闭前,不断的延长锁的有效期。默认情况下,看门狗的检查锁的超时时间是30秒钟,也可以通过修改Config.lockWatchdogTimeout来另行指定。
另外Redisson还通过加锁的方法提供了leaseTime的参数来指定加锁的时间。超过这个时间后锁便自动解开了。
// 10秒钟以后自动解锁
// 无需调用unlock方法手动解锁
fairLock.lock(10, TimeUnit.SECONDS);
// 尝试加锁,最多等待100秒,上锁以后10秒自动解锁
boolean res = fairLock.tryLock(100, 10, TimeUnit.SECONDS);
...
fairLock.unlock();
Redisson同时还为分布式可重入公平锁提供了异步执行的相关方法:
RLock fairLock = redisson.getFairLock("anyLock");
fairLock.lockAsync();
fairLock.lockAsync(10, TimeUnit.SECONDS);
Future<Boolean> res = fairLock.tryLockAsync(100, 10, TimeUnit.SECONDS);
5.5.3 联锁(MultiLock)
基于Redis的Redisson分布式联锁RedissonMultiLock对象可以将多个RLock对象关联为一个联锁,每个RLock对象实例可以来自于不同的Redisson实例。
RLock lock1 = redissonInstance1.getLock("lock1");
RLock lock2 = redissonInstance2.getLock("lock2");
RLock lock3 = redissonInstance3.getLock("lock3");
RedissonMultiLock lock = new RedissonMultiLock(lock1, lock2, lock3);
// 同时加锁:lock1 lock2 lock3
// 所有的锁都上锁成功才算成功。
lock.lock();
...
lock.unlock();
大家都知道,如果负责储存某些分布式锁的某些Redis节点宕机以后,而且这些锁正好处于锁住的状态时,这些锁会出现锁死的状态。为了避免这种情况的发生,Redisson内部提供了一个监控锁的看门狗,它的作用是在Redisson实例被关闭前,不断的延长锁的有效期。默认情况下,看门狗的检查锁的超时时间是30秒钟,也可以通过修改Config.lockWatchdogTimeout来另行指定。
另外Redisson还通过加锁的方法提供了leaseTime的参数来指定加锁的时间。超过这个时间后锁便自动解开了。
RedissonMultiLock lock = new RedissonMultiLock(lock1, lock2, lock3);
// 给lock1,lock2,lock3加锁,如果没有手动解开的话,10秒钟后将会自动解开
lock.lock(10, TimeUnit.SECONDS);
// 为加锁等待100秒时间,并在加锁成功10秒钟后自动解开
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
...
lock.unlock();
5.5.4 红锁(RedLock)
基于Redis的Redisson红锁RedissonRedLock对象实现了Redlock介绍的加锁算法。该对象也可以用来将多个RLock对象关联为一个红锁,每个RLock对象实例可以来自于不同的Redisson实例。
- 1.如果有多个 redis 集群的时候,当且仅当从大多数(N/2+1,比如有3个 redis 节点,那么至少有2个节点)的 Redis 节点都取到锁,并且获取锁使用的总耗时小于锁失效时间时,锁才算获取成功
- 2.如果获取失败,客户端会在所有的 Redis 实例上进行解锁操作
- 3.集群环境下,redis 服务器直接不存在任何复制或者其他隐含的分布式协调机制,否则会存在实效的可能
RedissonRedLock
简单使用样例如下!
Config config1 = new Config();
config1.useSingleServer().setAddress("redis://192.168.3.111:6379").setPassword("a123456").setDatabase(0);
RedissonClient redissonClient1 = Redisson.create(config1);
Config config2 = new Config();
config2.useSingleServer().setAddress("redis://192.168.3.112:6379").setPassword("a123456").setDatabase(0);
RedissonClient redissonClient2 = Redisson.create(config2);
Config config3 = new Config();
config3.useSingleServer().setAddress("redis://192.168.3.113:6379").setPassword("a123456").setDatabase(0);
RedissonClient redissonClient3 = Redisson.create(config3);
//获取多个 RLock 对象
final String lockKey = "abc";
RLock lock1 = redissonClient1.getLock(lockKey);
RLock lock2 = redissonClient2.getLock(lockKey);
RLock lock3 = redissonClient3.getLock(lockKey);
//根据多个 RLock 对象构建 RedissonRedLock (最核心的差别就在这里)
RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);
try {
//尝试5秒内获取锁,如果获取到了,最长60秒自动释放
boolean res = redLock.tryLock(5L, 60L, TimeUnit.SECONDS);
if (res) {
//成功获得锁,在这里处理业务
System.out.println("获取锁成功");
}
} catch (Exception e) {
System.out.println("获取锁失败,失败原因:" + e.getMessage());
} finally {
//无论如何, 最后都要解锁
redLock.unlock();
}
大家都知道,如果负责储存某些分布式锁的某些Redis节点宕机以后,而且这些锁正好处于锁住的状态时,这些锁会出现锁死的状态。为了避免这种情况的发生,Redisson内部提供了一个监控锁的看门狗,它的作用是在Redisson实例被关闭前,不断的延长锁的有效期。默认情况下,看门狗的检查锁的超时时间是30秒钟,也可以通过修改Config.lockWatchdogTimeout来另行指定。
另外Redisson还通过加锁的方法提供了leaseTime的参数来指定加锁的时间。超过这个时间后锁便自动解开了。
RedissonRedLock lock = new RedissonRedLock(lock1, lock2, lock3);
// 给lock1,lock2,lock3加锁,如果没有手动解开的话,10秒钟后将会自动解开
lock.lock(10, TimeUnit.SECONDS);
// 为加锁等待100秒时间,并在加锁成功10秒钟后自动解开
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
...
lock.unlock();
5.5.5 读写锁(ReadWriteLock)
基于Redis的Redisson分布式可重入读写锁RReadWriteLock Java对象实现了java.util.concurrent.locks.ReadWriteLock接口。其中读锁和写锁都继承了RLock接口。
分布式可重入读写锁允许同时有多个读锁和一个写锁处于加锁状态。
RReadWriteLock rwlock = redisson.getReadWriteLock("anyRWLock");
// 最常见的使用方法
rwlock.readLock().lock();
// 或
rwlock.writeLock().lock();
大家都知道,如果负责储存这个分布式锁的Redis节点宕机以后,而且这个锁正好处于锁住的状态时,这个锁会出现锁死的状态。为了避免这种情况的发生,Redisson内部提供了一个监控锁的看门狗,它的作用是在Redisson实例被关闭前,不断的延长锁的有效期。默认情况下,看门狗的检查锁的超时时间是30秒钟,也可以通过修改Config.lockWatchdogTimeout来另行指定。
另外Redisson还通过加锁的方法提供了leaseTime的参数来指定加锁的时间。超过这个时间后锁便自动解开了。
// 10秒钟以后自动解锁
// 无需调用unlock方法手动解锁
rwlock.readLock().lock(10, TimeUnit.SECONDS);
// 或
rwlock.writeLock().lock(10, TimeUnit.SECONDS);
// 尝试加锁,最多等待100秒,上锁以后10秒自动解锁
boolean res = rwlock.readLock().tryLock(100, 10, TimeUnit.SECONDS);
// 或
boolean res = rwlock.writeLock().tryLock(100, 10, TimeUnit.SECONDS);
...
lock.unlock();
使用范例
/**
* 保证一定能读到最新数据,修改期间,写锁是一个排它锁(互斥锁、独享锁),读锁是一个共享锁
* 写锁没释放读锁必须等待
* 读 + 读 :相当于无锁,并发读,只会在Redis中记录好,所有当前的读锁。他们都会同时加锁成功
* 写 + 读 :必须等待写锁释放
* 写 + 写 :阻塞方式
* 读 + 写 :有读锁。写也需要等待
* 只要有读或者写的存都必须等待
* @return
*/
@GetMapping(value = "/write")
@ResponseBody
public String writeValue() {
String s = "";
RReadWriteLock readWriteLock = redisson.getReadWriteLock("rw-lock");
RLock rLock = readWriteLock.writeLock();
try {
//1、改数据加写锁,读数据加读锁
rLock.lock();
s = UUID.randomUUID().toString();
ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
ops.set("writeValue",s);
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
rLock.unlock();
}
return s;
}
@GetMapping(value = "/read")
@ResponseBody
public String readValue() {
String s = "";
RReadWriteLock readWriteLock = redisson.getReadWriteLock("rw-lock");
//加读锁
RLock rLock = readWriteLock.readLock();
try {
rLock.lock();
ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
s = ops.get("writeValue");
try { TimeUnit.SECONDS.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); }
} catch (Exception e) {
e.printStackTrace();
} finally {
rLock.unlock();
}
return s;
}
5.5.6 信号量(Semaphore)
基于Redis的Redisson的分布式信号量(Semaphore)Java对象RSemaphore采用了与java.util.concurrent.Semaphore相似的接口和用法。同时还提供了异步(Async)、反射式(Reactive)和RxJava2标准的接口。
RSemaphore semaphore = redisson.getSemaphore("semaphore");
semaphore.acquire();
//或
semaphore.acquireAsync();
semaphore.acquire(23);
semaphore.tryAcquire();
//或
semaphore.tryAcquireAsync();
semaphore.tryAcquire(23, TimeUnit.SECONDS);
//或
semaphore.tryAcquireAsync(23, TimeUnit.SECONDS);
semaphore.release(10);
semaphore.release();
//或
semaphore.releaseAsync();
使用范例
/**
* 车库停车
* 3车位
* 信号量也可以做分布式限流
*/
@GetMapping(value = "/park")
@ResponseBody
public String park() throws InterruptedException {
RSemaphore park = redisson.getSemaphore("park");
park.acquire(); //获取一个信号、获取一个值,占一个车位
boolean flag = park.tryAcquire();
if (flag) {
//执行业务
} else {
return "error";
}
return "ok=>" + flag;
}
@GetMapping(value = "/go")
@ResponseBody
public String go() {
RSemaphore park = redisson.getSemaphore("park");
park.release(); //释放一个车位
return "ok";
}
5.5.7 闭锁(CountDownLatch)
基于Redisson的Redisson分布式闭锁(CountDownLatch)Java对象RCountDownLatch采用了与java.util.concurrent.CountDownLatch相似的接口和用法。
RCountDownLatch latch = redisson.getCountDownLatch("anyCountDownLatch");
latch.trySetCount(1);
latch.await();
// 在其他线程或其他JVM里
RCountDownLatch latch = redisson.getCountDownLatch("anyCountDownLatch");
latch.countDown();
使用范例
/**
* 放假、锁门
* 1班没人了
* 5个班,全部走完,我们才可以锁大门
* 分布式闭锁
*/
@GetMapping(value = "/lockDoor")
@ResponseBody
public String lockDoor() throws InterruptedException {
RCountDownLatch door = redisson.getCountDownLatch("door");
door.trySetCount(5);
door.await(); //等待闭锁完成
return "放假了...";
}
@GetMapping(value = "/gogogo/{id}")
@ResponseBody
public String gogogo(@PathVariable("id") Long id) {
RCountDownLatch door = redisson.getCountDownLatch("door");
door.countDown(); //计数-1
return id + "班的人都走了...";
}
5.6 Redis命令和Redisson对象匹配列表
5.7 SpringBoot使用Redisson
在Spring Boot中,可以通过Redisson的Spring支持来创建RedissonClient对象。
@Configuration
public class RedissonConfig {
@Bean
public RedissonClient redissonClient() {
Config config = new Config();
config.useSingleServer().setAddress("redis://localhost:6379");
return Redisson.create(config);
}
}
在上述代码中,创建了一个RedissonClient对象,并配置了连接Redis的地址。
5.7.1 实现分布式锁
使用RedissonClient对象获取RLock对象,RLock是Redisson提供的分布式锁接口。
通过RLock对象的lock方法来获取锁,并在获取锁成功后执行业务逻辑。
通过RLock对象的unlock方法来释放锁。
@Service
public class DistributedLockService {
@Autowired
private RedissonClient redissonClient;
public void executeWithLock() {
RLock lock = redissonClient.getLock("my_lock");
try {
lock.lock();
// 执行业务逻辑...博客原文:
} finally {
lock.unlock();
}
}
}
在上述代码中,executeWithLock方法通过redissonClient获取了一个名为"my_lock"的锁,并通过lock方法获取锁。在获取锁成功后,可以执行业务逻辑。最后,通过unlock方法释放锁。
Redisson还提供了其他一些功能强大的分布式锁实现方式,如可重入锁、公平锁、红锁、读写锁等。这些锁的实现方式更加灵活和强大,可以根据实际需求进行选择和使用。
使用Redisson实现分布式锁时,需要确保Redis服务器的可用性和稳定性,以避免单点故障导致的锁失效或锁的不稳定情况。此外,还需要根据具体的应用场景和需求,合理设置锁的过期时间,避免锁的长时间占用。
5.7.2 可重入锁(Reentrant Lock)
可重入锁是指同一个线程可以多次获得同一个锁,而不会发生死锁。Redisson的可重入锁实现是基于Redis的分布式锁的一种特例。
@Service
public class ReentrantLockService {
@Autowired
private RedissonClient redissonClient;
public void executeWithReentrantLock() {
RLock lock = redissonClient.getLock("my_lock");
try {
lock.lock();
// 执行业务逻辑...小小鱼儿小小林的博客测试
executeWithReentrantLock();
} finally {
lock.unlock();
}
}
}
5.7.3 公平锁(Fair Lock)
公平锁是指按照线程请求锁的顺序来分配锁。Redisson的公平锁实现可以保证多个线程按照先后顺序获取锁。
@Service
public class FairLockService {
@Autowired
private RedissonClient redissonClient;
public void executeWithFairLock() {
RLock lock = redissonClient.getFairLock("my_lock");
try {
lock.lock();
// 执行业务逻辑...小小鱼儿小小林的博客测试
} finally {
lock.unlock();
}
}
}
在上述代码中,使用redissonClient获取了一个名为"my_lock"的公平锁,并通过lock方法获取锁。在获取锁成功后,可以执行业务逻辑。最后,通过unlock方法释放锁。
5.7.4 红锁(Red Lock)
红锁是指在多个Redis节点上获取锁,以提高分布式系统的可靠性和容错性。Redisson的红锁实现是基于Redis的分布式锁的一种优化方式。
@Service
public class RedLockService {
@Autowired
private RedissonClient redissonClient;
public void executeWithRedLock() {
RLock lock1 = redissonClient.getLock("lock1");
RLock lock2 = redissonClient.getLock("lock2");
RLock lock3 = redissonClient.getLock("lock3");
RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);
try {
redLock.lock();
// 执行业务逻辑...
} finally {
redLock.unlock();
}
}
}
在上述代码中,使用redissonClient分别获取了名为"lock1"、"lock2"和"lock3"的锁,并通过RedissonRedLock将这些锁组合成红锁。在获取红锁成功后,可以执行业务逻辑。最后,通过unlock方法释放红锁。
5.7.5 读写锁(ReadWrite Lock)
读写锁是指在多线程环境下,对于读操作可以并行进行,对于写操作必须互斥进行。Redisson的读写锁实现提供了读锁和写锁两种操作。
@Service
public class ReadWriteLockService {
@Autowired
private RedissonClient redissonClient;
public void readWithReadWriteLock() {
RReadWriteLock rwLock = redissonClient.getReadWriteLock("my_lock");
RLock readLock = rwLock.readLock();
try {
readLock.lock();
// 执行读操作...
} finally {
readLock.unlock();
}
}
public void writeWithReadWriteLock() {
RReadWriteLock rwLock = redissonClient.getReadWriteLock("my_lock");
RLock writeLock = rwLock.writeLock();
try {
writeLock.lock();
// 执行写操作...
} finally {
writeLock.unlock();
}
}
}
在上述代码中,使用redissonClient获取了一个名为"my_lock"的读写锁,并通过readLock方法获取读锁,通过writeLock方法获取写锁。在获取锁成功后,可以执行相应的读操作或写操作。最后,通过unlock方法释放锁。
5.8 Redisson和Jedis、Lettuce有什么区别?
Redisson和Jedis、Lettuce有什么区别?倒也不是雷锋和雷锋塔
Redisson和它俩的区别就像一个用鼠标操作图形化界面,一个用命令行操作文件。Redisson是更高层的抽象,Jedis和Lettuce是Redis命令的封装。
Jedis是Redis官方推出的用于通过Java连接Redis客户端的一个工具包,提供了Redis的各种命令支持
Lettuce是一种可扩展的线程安全的 Redis 客户端,通讯框架基于Netty,支持高级的 Redis 特性,比如哨兵,集群,管道,自动重新连接和Redis数据模型。Spring Boot 2.x 开始 Lettuce 已取代 Jedis 成为首选 Redis 的客户端。
Redisson是架设在Redis基础上,通讯基于Netty的综合的、新型的中间件,企业级开发中使用Redis的最佳范本
Jedis把Redis命令封装好,Lettuce则进一步有了更丰富的Api,也支持集群等模式。但是两者也都点到为止,只给了你操作Redis数据库的脚手架,而Redisson则是基于Redis、Lua和Netty建立起了成熟的分布式解决方案,甚至redis官方都推荐的一种工具集。