redis-Redisson 工具类,悲观锁
摘要:介绍Redisson中分布式对象和集合的基础操作,包括对象桶、集合、列表和散列。
综述
测试环境为:Spring Boot版本 2.5.x 和 Redisson 单机。关于如何中Spring Boot项目集成Redisson,请戳《Spring Boot 整合Redisson配置篇》。
RedissonClient是线程安全的,由于其内部是通过Netty通信,所以除了同步执行方式,也支持异步执行。
Redisson 工具类
首先提供一个Redisson 工具类,方便下文用于演示。
import org.redisson.api.*;
import org.redisson.client.codec.StringCodec;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.function.BiConsumer;
@Component
public class RedisUtils {
private RedisUtils() {
}
/**
* 默认缓存时间
*/
private static final Long DEFAULT_EXPIRED = 32000L;
/**
* 自动装配redisson client对象
*/
@Resource
private RedissonClient redissonClient;
/**
* 用于操作key
* @return RKeys 对象
*/
public RKeys getKeys() {
return redissonClient.getKeys();
}
/**
* 移除缓存
*
* @param key
*/
public void delete(String key) {
redissonClient.getBucket(key).delete();
}
/**
* 获取getBuckets 对象
*
* @return RBuckets 对象
*/
public RBuckets getBuckets() {
return redissonClient.getBuckets();
}
/**
* 读取缓存中的字符串,永久有效
*
* @param key 缓存key
* @return 字符串
*/
public String getStr(String key) {
RBucket<String> bucket = redissonClient.getBucket(key);
return bucket.get();
}
/**
* 缓存字符串
*
* @param key
* @param value
*/
public void setStr(String key, String value) {
RBucket<String> bucket = redissonClient.getBucket(key);
bucket.set(value);
}
/**
* 缓存带过期时间的字符串
*
* @param key 缓存key
* @param value 缓存值
* @param expired 缓存过期时间,long类型,必须传值
*/
public void setStr(String key, String value, long expired) {
RBucket<String> bucket = redissonClient.getBucket(key, StringCodec.INSTANCE);
bucket.set(value, expired <= 0L ? DEFAULT_EXPIRED : expired, TimeUnit.SECONDS);
}
/**
* string 操作,如果不存在则写入缓存(string方式,不带有redisson的格式信息)
*
* @param key 缓存key
* @param value 缓存值
* @param expired 缓存过期时间
*/
public Boolean setIfAbsent(String key, String value, long expired) {
RBucket<String> bucket = redissonClient.getBucket(key, StringCodec.INSTANCE);
return bucket.trySet(value, expired <= 0L ? DEFAULT_EXPIRED : expired, TimeUnit.SECONDS);
}
/**
* 如果不存在则写入缓存(string方式,不带有redisson的格式信息),永久保存
*
* @param key 缓存key
* @param value 缓存值
*/
public Boolean setIfAbsent(String key, String value) {
RBucket<String> bucket = redissonClient.getBucket(key, StringCodec.INSTANCE);
return bucket.trySet(value);
}
/**
* 判断缓存是否存在
*
* @param key
* @return true 存在
*/
public Boolean isExists(String key) {
return redissonClient.getBucket(key).isExists();
}
/**
* 获取RList对象
*
* @param key RList的key
* @return RList对象
*/
public <T> RList<T> getList(String key) {
return redissonClient.getList(key);
}
/**
* 获取RMapCache对象
*
* @param key
* @return RMapCache对象
*/
public <K, V> RMapCache<K, V> getMap(String key) {
return redissonClient.getMapCache(key);
}
/**
* 获取RSET对象
*
* @param key
* @return RSET对象
*/
public <T> RSet<T> getSet(String key) {
return redissonClient.getSet(key);
}
/**
* 获取RScoredSortedSet对象
*
* @param key
* @param <T>
* @return RScoredSortedSet对象
*/
public <T> RScoredSortedSet<T> getScoredSortedSet(String key) {
return redissonClient.getScoredSortedSet(key);
}
}
常用RKeys的API操作
每个Redisson对象实例都会有一个与之对应的Redis数据实例,可以通过调用getName方法来取得Redis数据实例的名称(key)。所有与Redis key相关的操作都归纳在RKeys这个接口里:
RKeys keys = client.getKeys();
//获取所有key值
Iterable<String> allKeys = keys.getKeys();
//模糊查询所有包含关键字key的值
Iterable<String> foundedKeys = keys.getKeysByPattern("key");
//删除多个key值
long numOfDeletedKeys = keys.delete("obj1", "obj2", "obj3");
//模糊删除key值
long deletedKeysAmount = keys.deleteByPattern("test?");
//随机获取key
String randomKey = keys.randomKey();
//查询当前有多少个key
long keysAmount = keys.count();
具体demo如下:
private void getKeys() {
RKeys keys = redisUtils.getRedisKeys();
Iterable<String> allKeys = keys.getKeys();
StringBuilder sb = new StringBuilder();
for (String key : allKeys) {
sb = sb.append(key).append(",");
}
log.info("所有的key:{}", sb.substring(0, sb.length() - 1));
// 模糊查询以 map 打头的所有 key
allKeys = keys.getKeysByPattern("map*");
sb = new StringBuilder();
for (String key : allKeys) {
sb = sb.append(key).append(",");
}
log.info("模糊匹配到的key:{}", sb.substring(0, sb.length() - 1));
}
其中,getKeysByPattern是基于redis 的 scan 命令实现的,匹配规则示例如下:
- h?llo subscribes to hello, hallo and hxllo
- h*llo subscribes to hllo and heeeello
- h[ae]llo subscribes to hello and hallo, but not hillo
通用对象桶(Object Bucket)
Redisson的分布式RBucket Java对象是一种通用对象桶,可以用来存放任意类型的对象。除了同步接口外,还提供了异步(Async)、反射式(Reactive)和RxJava2标准的接口。还可以通过RBuckets接口实现批量操作多个RBucket对象:
/**
* String 数据类型
*/
private void strDemo() {
redisUtils.setStr(DEMO_STR, "Hello, String.");
log.info("String 测试数据:{}", redisUtils.getStr(DEMO_STR));
redisUtils.setStr("myBucket", "myBucketIsXxx");
RBuckets buckets = redisUtils.getBuckets();
Map<String, String> foundBuckets = buckets.get("myBucket*");
Map<String, Object> map = new HashMap<>();
map.put("myBucket1", "value1");
map.put("myBucket2", 30L);
// 同时保存全部通用对象桶。
buckets.set(map);
Map<String, String> loadedBuckets = buckets.get("myBucket1", "myBucket2", "myBucket3");
log.info("跨桶String 测试数据:{}", loadedBuckets);
map.put("myBucket3", 320L);
}
散列(Hash)
基于Redisson的分布式映射结构的RMap Java对象实现了java.util.concurrent.ConcurrentMap接口和java.util.Map接口。与HashMap不同的是,RMap保持了元素的插入顺序。该对象的最大容量受Redis限制,最大元素数量是4 294 967 295个。
/**
* Hash类型
*/
private void hashDemo() {
RMap<Object, Object> map = redisUtils.getMap("mapDemo");
map.put("demoId1", "123");
map.put("demoId100", "13000");
Object demoId1Obj = map.get("demoId1");
log.info("Hash 测试数据:{}", demoId1Obj);
}
集合(Set)
基于Redisson的分布式Set结构的RSet Java对象实现了java.util.Set接口。通过元素的相互状态比较保证了每个元素的唯一性。该对象的最大容量受Redis限制,最大元素数量是4 294 967 295个。
/**
* Set 测试
*/
private void setDemo() {
RSet<String> set = redisUtils.getSet("setKey");
set.add("value777");
log.info("Set 测试数据");
Iterator<String> iterator = set.iterator();
while (iterator.hasNext()) {
String next = iterator.next();
log.info(next);
}
}
列表(List)
基于Redisson分布式列表(List)结构的RList Java对象在实现了java.util.List接口的同时,确保了元素插入时的顺序。该对象的最大容量受Redis限制,最大元素数量是4 294 967 295个。
/**
* List数据类型
*/
private void listDemo() {
RList<String> list = redisUtils.getList("listDemo");
list.add("listValue1");
list.add("listValue2");
log.info("List 测试数据:{}", list.get(1));
}
综合示例
将上述各个demo放入一个API中,以便快速测试:
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.*;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
@Slf4j
@RestController
@RequestMapping(value = "/redisson", method = RequestMethod.POST)
public class StudyRedissonController {
@Resource
private RedisUtils redisUtils;
private static String DEMO_STR = "demoStr";
@PostMapping("/learnRedisson")
public void learnRedisson() {
//三种数据结构使用示例
strDemo();
hashDemo();
listDemo();
setDemo();
getKeys();
}
private void getKeys() {
RKeys keys = redisUtils.getKeys();
Iterable<String> allKeys = keys.getKeys();
StringBuilder sb = new StringBuilder();
for (String key : allKeys) {
sb = sb.append(key).append(",");
}
log.info("所有的key:{}", sb.substring(0, sb.length() - 1));
// 模糊查询以 map 打头的所有 key
allKeys = keys.getKeysByPattern("map*");
sb = new StringBuilder();
for (String key : allKeys) {
sb = sb.append(key).append(",");
}
log.info("模糊匹配到的key:{}", sb.substring(0, sb.length() - 1));
}
/**
* Hash类型
*/
private void hashDemo() {
RMap<Object, Object> map = redisUtils.getMap("mapDemo");
map.put("demoId1", "123");
map.put("demoId100", "13000");
Object demoId1Obj = map.get("demoId1");
log.info("Hash 测试数据:{}", demoId1Obj);
}
/**
* String 数据类型
*/
private void strDemo() {
redisUtils.setStr(DEMO_STR, "Hello, String.");
log.info("String 测试数据:{}", redisUtils.getStr(DEMO_STR));
redisUtils.setStr("myBucket", "myBucketIsXxx");
RBuckets buckets = redisUtils.getBuckets();
Map<String, String> foundBuckets = buckets.get("myBucket*");
Map<String, Object> map = new HashMap<>();
map.put("myBucket1", "value1");
map.put("myBucket2", 30L);
// 同时保存全部通用对象桶。
buckets.set(map);
Map<String, String> loadedBuckets = buckets.get("myBucket1", "myBucket2", "myBucket3");
log.info("跨桶String 测试数据:{}", loadedBuckets);
map.put("myBucket3", 320L);
}
/**
* List数据类型
*/
private void listDemo() {
RList<String> list = redisUtils.getList("listDemo");
list.add("listValue1");
list.add("listValue2");
log.info("List 测试数据:{}", list.get(1));
}
/**
* Set 测试
*/
private void setDemo() {
RSet<String> set = redisUtils.getSet("setKey");
set.add("value777");
log.info("Set 测试数据");
Iterator<String> iterator = set.iterator();
while (iterator.hasNext()) {
String next = iterator.next();
log.info(next);
}
}
}
启动服务,调用如上API,则控制台打印的执行结果如下:
redissonclient.getlock redissonclient.getlock悲观锁
—、redisson
1. 与redistempplate区别:

2. 看门狗机制:



一、 简介
Redisson是一个Redis的基础上实现的Java驻内存数据网格,它不仅提供了一系列分布式的Java常用对象,还有一个重要的分布式锁的实现,主要作用为了防止分布式系统中的多个进程之间相互干扰。比如单机模式下的多线程用同步锁synchronized等解决数据一致性的并发操作,而分布式系统中则需要用redisson的lock或其他方式来解决。
二、 原理
底层其实就是基于分布式的Redis集群实现的。
用key作为是否上锁的标识,当通过getLock(String key)方法获得相应的锁后,这个key即作为一个锁存储到Redis集群中。 之后如果有其他的线程尝试获取名为key的锁时,便会向集群中进行查询,如果能够查到这个锁并发现相应的value的值不为0,则表示已经有其他线程申请了这个锁同时还没有释放,则当前线程进入阻塞,否则由当前线程获取这个锁并将value值加1。
三、 Watch Dog(看门狗机制)
作用就是:自动续期
- 解决指定解锁时间的重复解锁问题。(业务执行的时间超过指定时间,redis会自动解锁;当前业务执行完后又要解锁,可能会解锁到另一条线程加的锁或当前锁已失效)
2. 解决死锁。(加锁的业务只要运行完成,就不会给当前锁续期,即使不手动解锁,锁默认会在30s内自动过期)
四、 锁类型
根据不同业务场景需要redisson提供了多种锁的实现类型:可重入锁,公平锁,联锁,红锁,读写锁,信号量,可过期性信号量,闭锁。
这里看下最常用的可重入锁,特点是同一个线程可以重复拿到同一个资源的锁,非常有利于资源的高效利用。
底层实现:1. Redis存储锁的数据类型是Hash
- Hash数据类型的key值包含了当前线程信息
五、 redlock算法
底层的一个算法,可以了解一下。
当redis宕机时,即使有主从,但是依然会有一个同步间隔,如果造成数据流失,服务器A丢失锁,服务器B就可以获取锁,这样就造成数据错误。
redlock主要思想是做数据冗余。比如5台独立的集群,当我们发送一个数据的时候,要保证3台(n/2+1)以上的机器接受成功才算成功,否则重试或报错。
六、 项目配置
- 引入依赖
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.12.0</version>
</dependency>
2.配置类RedissonClient
3.操作示例
二、数据库锁:
Mysql 并发事务 会引起更新丢失问题,解决办法是锁。所以本文将对锁(乐观锁、悲观锁)进行分析。
1. 悲观锁:
1 概念(来自百科)
悲观锁,正如其名,指数据被 外界(包括当前系统的其它事务,以及来自外部系统的事务处理)修改持保守态度,因此,在整个数据处理过程中,将数据处于锁定状态。悲观锁的实现,往往依靠数据库提 供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排它性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据)
还可以理解,就是Java中的 Synchronized 关键字。只要对代码加了 Synchronized 关键字,JVM 底层就能保证其线程安全性。
2 命令行演示
2.1 准备数据

2.2 测试
测试准备:
- 两个会话(终端),左边会话是白色背景、右边会话是黑色背景
开始测试:
第一步:两个终端均关闭自动提交
左边:

右边:

第二步:左边利用 select .... for update 的悲观锁语法锁住记录

第三步:右边也尝试利用 select .... for update 的悲观锁语法锁住记录

可以看到,Sql语句被挂起(被阻塞)!
提示:如果被阻塞的时间太长,会提示如下:

第四步:左边执行更新操作并提交事务
Sql语句:
结果:

分析:
- Money 的旧值为0,所以更新时 Money=0+1
- 一执行 commit 后,注意查看右边Sql语句的变化
第五步:查看右边Sql语句的变化

分析:
- 被左边悲观锁阻塞了 11.33 秒
- Money=1,这是左边更新后的结果
2.3 结论
可以看到,当左边(事务A)使用了 select ... for update 的悲观锁后,右边(事务B)再想使用将被阻塞,同时,阻塞被解除后事务B能看到事务A对数据的修改,所以,这就可以很好地解决并发事务的更新丢失问题啦(诚然,这也是人家悲观锁的分内事)
2.乐观锁:
1 概念
理解方式一:
乐观锁认为一般情况下数据不会造成冲突,所以在数据进行提交更新时才会对数据的冲突与否进行检测。如果没有冲突那就OK;如果出现冲突了,则返回错误信息并让用户决定如何去做。
理解方式二:
乐观锁的特点是先进行业务操作,不到万不得已不会去拿锁。乐观地认为拿锁多半会是成功的,因此在完成业务操作需要实际更新数据的最后一步再去拿一下锁。
我的理解
理解一:就是 CAS 操作
理解二:类似于 SVN、GIt 这些版本管理系统,当修改了某个文件需要提交的时候,它会检查文件的当前版本是否与服务器上的一致,如果一致那就可以直接提交,如果不一致,那就必须先 更新服务器上的最新代码然后再提交(也就是先将这个文件的版本更新成和服务器一样的版本)
2 表设计
表task,分别有三个字段id,value、version
3 具体实现
-首先读取task表中的数据,得到version的值为versionValue
-在每次更新task表value字段时,因为要防止可能发生的冲突,我们需要这样操作
只有当这条语句执行成功了,本次更新value字段的值才会表示成功。
我们假设有两个节点A与B都需要更新task表中的value字段值,在相同时刻,A和B节点从task表中读到的version值都为2,那么A节点和B节点在更新value字段值的时候,都需要操作
update task set value = newValue,version = 3 where version = 2;
实际上其实只有1个节点执行该SQL语句成功,我们假设A节点执行成功,那么此时task表的version字段的值是3,B节点再操作
这条SQL语句是不执行的,这样就保证了更新task表时不发生冲突
3.总结、对比
|
|
悲观锁 |
乐观锁 |
|
概念 |
查询时 直接锁住记录 使其它事务不能查询,更不能更新 |
提交更新时 检查 版本或时间戳 是否相等 |
|
语法 |
select ... for update |
使用 version 或者 timestamp 进行比较 |
|
实现者 |
数据库本身 |
开发者 |
|
适用场景 |
并发量大 |
并发量小 |
|
类比Java |
Synchronized关键字 |
CAS 算法 |
浙公网安备 33010602011771号