文章目录
- 前言:redisson是什么?
- 一、几种操作Redis的Java Client
- 二、redisson分布式锁
- 1.引入背景
- 原生分布式锁
- 可重入锁和不可重入锁
- 改为可重入锁
- 加锁思路
- 解锁
- 最终代码
- Redisson分布式锁
- 引入依赖
- 配置config
- 启用分布式锁
- 信号量(如果遇到需要其他进程也能解锁的情况,请使用分布式信号量.)
- redis可重入锁(Reentrant Lock)
- 读写锁
- 闭锁
- 总结
前言:redisson是什么?
Redisson is a Redis Java client(客户端) with features of In-Memory(内存) Data Grid. It provides more convenient and easiest way to work with Redis. Redisson objects provides a separation(分离) of concern, which allows you to keep focus on the data modeling and application logic.
一、几种操作Redis的Java Client
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官方都推荐的一种工具集。
二、redisson分布式锁
1.引入背景
本地缓存问题:每个微服务都要有缓存服务、数据更新时只更新自己的缓存,造成缓存数据不一致
解决方案: 分布式缓存,微服务共用 缓存中间件
原生分布式锁
RedisTemplate来操作Redis
// 加锁
public Boolean tryLock(String key, String value, long timeout, TimeUnit unit) {
return redisTemplate.opsForValue()
.setIfAbsent(key, value, timeout, unit);
}// 解锁,防止删错别人的锁,以uuid为value校验是否自己的锁
public void unlock(String lockName, String uuid) {
if(uuid.equals(redisTemplate.opsForValue().get(lockName)){
redisTemplate.opsForValue().del(lockName);
}
}// 结构if(tryLock){ // todo}
finally{
unlock;
}
这是锁没错,但get和del操作非原子性,并发一旦大了,无法保证进程安全,我们可以使用Lua脚本
lockDel.lua文件如下
if redis.call("get",KEYS[1]) == ARGV[1]
then
return redis.call("del",KEYS[1])
else
return 0
end;
// 解锁脚本
DefaultRedisScript<Object> unlockScript = new DefaultRedisScript();
unlockScript.setScriptSource(
new ResourceScriptSource(new ClassPathResource("lockDel.lua")));
// 执行lua脚本解锁 , 后面两个参数是传入的值!!
redisTemplate.execute(unlockScript, Collections.singletonList(keyName), value);
这样设计的话,我们发现是一个不可重入锁. synchronized和ReentrantLock都很丝滑,因为他们都是可重入锁,一个线程多次拿锁也不会死锁,我们需要可重入。
可重入锁和不可重入锁
可重入锁就是一个类的A、B两个方法,A、B都有获得统一把锁,当A方法调用时,获得锁,在A方法的锁还没有被释放时,调用B方法时,B方法也获得该锁。
这种情景,可以是不同的线程分别调用这个两个方法。也可是同一个线程,A方法中调用B方法,这个线程调用A方法。
不可重入锁就是一个类的A、B两个方法,A、B都有获得统一把锁,当A方法调用时,获得锁,在A方法的锁还没有被释放时,调用B方法时,B方法也获得不了该锁,必须等A方法释放掉这个锁。
改为可重入锁
重点:
同一个线程多次获取同一把锁是允许的,不会造成死锁,这一点synchronized偏向锁提供了很好的思路,synchronized的实现重入是在JVM层面,JAVA对象头MARK WORD中便藏有线程ID和计数器来对当前线程做重入判断,避免每次CAS
参数 | 示例值 | 含义 |
KEY个数 | 1 | KEY个数 |
KEYS[1] | my_first_lock_name | 锁名 |
ARGV[1] | 60000 | 持有锁的有效时间:毫秒 |
ARGV[2] | 58c62432-bb74-4d14-8a00-9908cc8b828f:1 | 唯一标识:获取锁时set |
加锁思路
-- 存储结构 利用Redis的Hash来存储这些东西
-- lockname 锁名称
-- key1: threadId 唯一键,线程id
-- value1: count 计数器,记录该线程获取锁的次数
-- 计数器的加减 当同一个线程获取同一把锁时,我们需要对对应线程的计数器count做加减
-- 判断一个redis key是否存在,可以用exists,而判断一个hash的key是否存在,可以用hexists
-- redis也有hash自增的命令hincrby
-- 若锁不存在:则新增锁,并设置锁重入计数为1、设置锁过期时间 -- key1 为锁 arg1为过期时间 arg2为uuid
if (redis.call('exists', KEYS[1]) == 0)
then
redis.call('hset', KEYS[1], ARGV[2], 1);
redis.call('pexpire', KEYS[1], ARGV[1]);
return nil;
end;
-- 若锁存在,且唯一标识也匹配:则表明当前加锁请求为锁重入请求,故锁重入计数+1,并再次设置锁过期时间
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1)
then
redis.call('hincrby', KEYS[1], ARGV[2], 1);
redis.call('pexpire', KEYS[1], ARGV[1]);
return nil;
end;
-- 若锁存在,但唯一标识不匹配:表明锁是被其他线程占用,当前线程无权解他人的锁,直接返回锁剩余过期时间
return redis.call('pttl', KEYS[1]);
解锁
-- 若锁不存在:则直接广播解锁消息,并返回1
if (redis.call('exists', KEYS[1]) == 0) then
redis.call('publish', KEYS[2], ARGV[1]);
return 1;
end;
-- 若锁存在,但唯一标识不匹配:则表明锁被其他线程占用,当前线程不允许解锁其他线程持有的锁
if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then
return nil;
end;
-- 若锁存在,且唯一标识匹配:则先将锁重入计数减1
local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1);
if (counter > 0) then
-- 锁重入计数减1后还大于0:表明当前线程持有的锁还有重入,不能进行锁删除操作,但可以友好地帮忙设置下过期时期
redis.call('pexpire', KEYS[1], ARGV[2]);
return 0;
else
-- 锁重入计数已为0:间接表明锁已释放了。直接删除掉锁,并广播解锁消息,去唤醒那些争抢过锁但还处于阻塞中的线程
redis.call('del', KEYS[1]);
redis.call('publish', KEYS[2], ARGV[1]);
return 1;
end;
return nil;
最终代码
/**
* @description 原生redis实现分布式锁
**/@Getter@Setterpublic
class RedisLock {
private RedisTemplate redisTemplate;
private DefaultRedisScript<Long> lockScript;
private DefaultRedisScript<Object> unlockScript;
public RedisLock(RedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
lockScript = new DefaultRedisScript<>(); // 加载加锁的脚本
this.lockScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("lock.lua")));
this.lockScript.setResultType(Long.class); // 加载释放锁的脚本
unlockScript = new DefaultRedisScript<>();
this.unlockScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("unlock.lua")));
}
/**
* 获取锁
*/
public String tryLock(String lockName, long releaseTime) { // 存入的线程信息的前缀
String key = UUID.randomUUID().toString(); // 执行脚本
Long result = (Long) redisTemplate.execute(
lockScript,
Collections.singletonList(lockName),
key + Thread.currentThread().getId(),
releaseTime);
if (result != null && result.intValue() == 1) {
return key;
} else {
return null;
}
} /**
* 解锁
* @param lockName
* @param key
*/
public void unlock(String lockName, String key) {
redisTemplate.execute(unlockScript,
Collections.singletonList(lockName),
key + Thread.currentThread().getId()
);
}
}
- 加锁时判断nset
- 使用时续期
- 删除时判断uuid
public Map<String, List<Catalog2Vo>> getCatalogJsonDbWithRedisLock() {
// 生成uuid
String uuid = UUID.randomUUID().toString();
ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
// 尝试加锁,阻塞0.5s
Boolean lock = ops.setIfAbsent("lock", uuid,500, TimeUnit.SECONDS);
// 加锁成功
if (lock) {
// 执行业务
Map<String, List<Catalog2Vo>> categoriesDb = getCategoryMap();
// 去删除锁
String lockValue = ops.get("lock");
// get和delete原子操作
String script = "if redis.call(\"get\",KEYS[1]) == ARGV[1] then\n" +
" return redis.call(\"del\",KEYS[1])\n" +
"else\n" +
" return 0\n" +
"end";
stringRedisTemplate.execute(
new DefaultRedisScript<Long>(script, Long.class), // 脚本和返回类型
Arrays.asList("lock"), // 参数key
lockValue); // 参数值,锁的值 uuid
// 返回db
return categoriesDb;
}else { // 加锁失败
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 睡眠0.1s后,重新调用 //自旋
return getCatalogJsonDbWithRedisLock();
}
}
Redisson分布式锁
引入依赖
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.12.0</version>
</dependency>
配置config
@Configuration
public class MyRedissionConfig {
// @Value("${ipAddr}")
// private String ipAddr;
// redission通过redissonClient对象使用 // 如果是多个redis集群,可以配置
@Bean(destroyMethod = "shutdown") //销毁时调用shutdown方法
public RedissonClient redisson() {
Config config = new Config();
// 创建单例模式的配置
config.useSingleServer().setAddress("redis://192.168.122.129" + ":6379");
return Redisson.create(config); //返回实例
}
}
启用分布式锁
信号量(如果遇到需要其他进程也能解锁的情况,请使用分布式信号量.)
信号量为存储在redis中的一个数字,当这个数字大于0时,即可以调用acquire()
方法增加数量,也可以调用release()
方法减少数量,但是当调用release()
之后小于0的话方法就会阻塞,直到数字大于0
@Autowired
RedissonClient redissonClient;
// RSemaphore 可以理解为分布式的信号量,它的作用是用来限制同时访问共享区域的线程数量。
//引入分布式信号量 进行限流
RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + token); //设置key
//商品可以秒杀的数量作为信号量
semaphore.trySetPermits(seckillSkuVo.getSeckillCount()); // 设置value
redis可重入锁(Reentrant Lock)
锁其实也是一种资源,各线程争抢锁操作对应到redisson中就是争抢着去创建一个hash
结构,谁先创建就代表谁获得锁;hash的名称为锁名,hash里面内容仅包含一条键值对,键为redisson
客户端唯一标识+持有锁线程id,值为锁重入计数;给hash设置的过期时间就是锁的过期时间。
// 参数为锁名字
RLock lock = redissonClient.getLock("CatalogJson-Lock");//该锁实现了JUC.locks.lock接口
lock.lock();//阻塞等待
// 加锁以后10秒钟自动解锁
// 无需调用unlock方法手动解锁
lock.lock(10, TimeUnit.SECONDS);
// 尝试加锁,最多等待100秒,上锁以后10秒自动解锁
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
if (res) {
try {
...
} finally {
lock.unlock();
}
}
lock.unlock();
// 解锁放到finally // 如果这里宕机:有看门狗,不用担心
lock.unlock();
看门狗:
如果负责储存这个分布式锁的Redisson节点宕机以后,而且这个锁正好处于锁住的状态时,这个锁会出现锁死的状态。为了避免这种情况的发生,Redisson内部提供了一个监控锁的看门狗,它的作用是在Redisson实例被关闭前,不断的延长锁的有效期。默认情况下,看门狗的检查锁的超时时间是30秒钟(每到20s就会自动续借成30s,是1/3的关系),也可以通过修改Config.lockWatchdogTimeout来另行指定。并且加锁的业务只要运行完成,就不会给当前锁续期,即使不手动解锁,锁默认30s后自动删除
// 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 对象.
读写锁
写锁没有释放,读锁无法读取(等待写锁释放)(保证一定可以读到最新的数据)修改期间是一个互斥锁,只能存在一个写锁,但可以多个读锁
分布式可重入读写锁允许同时有多个读锁和一个写锁处于加锁状态。
RReadWriteLock rwlock = redisson.getReadWriteLock("anyRWLock"); //获取锁
// 10秒钟以后自动解锁
// 无需调用unlock方法手动解锁
rwlock.readLock().lock(10, TimeUnit.SECONDS); //加上读锁
//实例
RReadWriteLock lock = redisson.getReadWriteLock("rw-lock"); //获取锁
String s = "";
RLock rlock = rwlock.readLock(); //定义为写锁
try{
rLock.lock(); //加锁
s=redis.Template.opsForValue().get("wruteValue"); // 拿到数据
}catch(){
//xxxxx
}finally{
rLock.unlock();//释放锁!
}
// 或
rwlock.writeLock().lock(10, TimeUnit.SECONDS); //加上写锁
//实例
RReadWriteLock rwlock = redisson.getReadWriteLock("rw-lock"); //获取锁
String s = "";
RLock rlock = rwlock.writeLock(); //定义为写锁
try{
rLock.lock(); //加锁
s = UUID.randomUUID().toString(); // 随机一个文本
redis.Template.opsForValue().set("wruteValue",s);
}catch(){
//xxxxx
}finally{
rLock.unlock();//释放锁!
}
// 尝试加锁,最多等待100秒,上锁以后10秒自动解锁
boolean res = rwlock.readLock().tryLock(100, 10, TimeUnit.SECONDS);
// 或
boolean res = rwlock.writeLock().tryLock(100, 10, TimeUnit.SECONDS);
...
lock.unlock();
闭锁
以下代码只有offLatch()
被调用5次后 setLatch()
才能继续执行
RCountDownLatch latch = redisson.getCountDownLatch("anyCountDownLatch"); //拿到闭锁对象
latch.trySetCount(1); //设置次数
latch.await(); // 阻塞 等待减为0
// 在其他线程或其他JVM里
RCountDownLatch latch = redisson.getCountDownLatch("anyCountDownLatch"); //拿到闭锁对象
latch.countDown();
总结
这就是利用Redis或Ression做分布式缓存中一些应用