集成框架 -- Redission
- 前言
- Redisson
- 准备工作
- 常用的几把锁
- 可重入锁 -- 大家常用的分布式锁
- 公平锁
- 联锁
- 红锁(RedLock)
- 读写锁(ReadWriteLock)
- 信号量(Semaphore)
- 可过期性信号量(PermitExpirableSemaphore)
- 闭锁(CountDownLatch)
- 关于lock 看门狗源码解析
- lua语言
- 项目使用
- 配置
- SpringBoot启动器
- 配置单机运行
- 工具类
- 直接使用
- 附 redis 工具类
- 关于分布式锁 缓存一致性问题闲聊
前言
随着开发量的增加 本地锁已经不能满足架构的集群开发情况,这种情况下,系统需要再集成一些高级锁,同时能够支撑分布式锁,再上一篇的文章中,关于redis
是可以实现分布式锁的,但是关于超时时间有很多影响数量,关于超时还要进行一些双删的逻辑,急需要一种可以集成在分布式框架。
Redisson
Redisson
是一个在Redis
的基础上实现的Java驻内存数据
网格。它不仅提供了一系列的分布式的Java
常用对象,还提供了许多分布式服务。其中包括(BitSet
, Set
, Multimap
, SortedSet
, Map
, List
, Queue
, BlockingQueue
, Deque
, BlockingDeque
, Semaphore
, Lock
, AtomicLong
, CountDownLatch
, Publish
/ Subscribe
, Bloom filter
, Remote service
, Spring cache
, Executor service
, Live Object service
, Scheduler service
) Redisson
提供了使用Redis
的最简单和最便捷的方法。Redisson
的宗旨是促进使用者对Redis
的关注分离,从而让使用者能够将精力更集中地放在处理业务逻辑上。
关于Redisson
项目的详细介绍可以在官方网站找到。
每个Redis
服务实例都能管理多达1TB
的内存。
能够完美的在云计算环境里使用,并且支持AWS ElastiCache主备版
,AWS ElastiCache集群版
,Azure Redis Cache和阿里云
的云数据库Redis
版
以下是Redisson
的结构:
Redisson
作为独立节点 可以用于独立执行其他节点发布到分布式执行服务 和 分布式调度任务服务 里的远程任务。
如果你现在正在使用其他的Redis
的Java
客户端,那么Redis
命令和Redisson
对象匹配列表 能够帮助你轻松的将现有代码迁徙到Redisson
框架里来。
Redisson底层
采用的是Netty
框架。支持Redis 2.8
以上版本,支持Java1.6+以上
版本。
准备工作
组装常用jar
整合redission
作为分布锁框架
<!-- https://mvnrepository.com/artifact/org.redisson/redisson -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.12.5</version>
</dependency>
常用的几把锁
可重入锁 – 大家常用的分布式锁
基于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
的参数来指定加锁的时间。超过这个时间后锁便自动解开了。
RLock
对象完全符合Java
的Lock
规范。也就是说只有拥有锁的进程才能解锁
,其他进程解锁则会抛出``IllegalMonitorStateException错误
。但是如果遇到需要其他进程也能解锁的情况,请使用分布式信号量Semaphore
对象.
公平锁
基于Redis
的Redisson分布式可重入公平锁
也是实现了java.util.concurrent.locks.Lock
接口的一种RLock
对象。同时还提供了异步(Async)、反射式(Reactive)
接口。它保证了当多个Redisson客户端线程同时请求
加锁时,优先分配给先发出请求的线程。所有请求线程会在一个队列中排队,当某个线程出现宕机时,Redisson
会等待5秒
后继续下一个线程,也就是说如果前面有5个
线程都处于等待状态,那么后面的线程会等待至少25秒
。
RLock fairLock = redisson.getFairLock("anyLock");
// 最常见的使用方法
fairLock.lock();
联锁
分布式联锁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();
红锁(RedLock)
红锁RedissonRedLock
对象实现了Redlock
介绍的加锁算法
。该对象也可以用来将多个RLock对象
关联为一个红锁,每个RLock对象
实例可以来自于不同
的Redisson实例
。
RLock lock1 = redissonInstance1.getLock("lock1");
RLock lock2 = redissonInstance2.getLock("lock2");
RLock lock3 = redissonInstance3.getLock("lock3");
RedissonRedLock lock = new RedissonRedLock(lock1, lock2, lock3);
// 同时加锁:lock1 lock2 lock3
// 红锁在大部分节点上加锁成功就算成功。
lock.lock();
...
lock.unlock();
读写锁(ReadWriteLock)
可重入读写锁RReadWriteLock
Java对象实现了java.util.concurrent.locks.ReadWriteLock
接口。其中读锁和写锁都继承了RLock
接口。
分布式可重入读写锁允许同时有多个读锁和一个写锁处于加锁状态。
RReadWriteLock rwlock = redisson.getReadWriteLock("anyRWLock");
// 最常见的使用方法
rwlock.readLock().lock();
// 或
rwlock.writeLock().lock();
信号量(Semaphore)
基于Redis
的Redisson的
分布式信号量Semaphore
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();
可过期性信号量(PermitExpirableSemaphore)
基于Redis
的Redisson可过期性信号量
是在RSemaphore
对象的基础上,为每个信号增加了一个过期时间
。每个信号可以通过独立的ID
来辨识,释放时只能通过提交这个ID
才能释放。
RPermitExpirableSemaphore semaphore = redisson.getPermitExpirableSemaphore("mySemaphore");
String permitId = semaphore.acquire();
// 获取一个信号,有效期只有2秒钟。
String permitId = semaphore.acquire(2, TimeUnit.SECONDS);
// ...
semaphore.release(permitId);
闭锁(CountDownLatch)
基于Redisson
分布式闭锁CountDownLatch
RCountDownLatch latch = redisson.getCountDownLatch("anyCountDownLatch");
latch.trySetCount(1);
latch.await();
// 在其他线程或其他JVM里
RCountDownLatch latch = redisson.getCountDownLatch("anyCountDownLatch");
latch.countDown();
从上述中的示例中,可以看到redisson
是基于lock本地锁
的分布式锁
,
关于lock 看门狗源码解析
默认传了一个 -1 过期时间
@Override
public void lockInterruptibly(long leaseTime, TimeUnit unit) throws InterruptedException {
// 当前线程
long threadId = Thread.currentThread().getId();
// 获取锁
Long ttl = tryAcquire(leaseTime, unit, threadId);
// 获取锁 如果为空 获取到了
if (ttl == null) {
return;
}
// 否则开始循环
RFuture<RedissonLockEntry> future = subscribe(threadId);
commandExecutor.syncSubscription(future);
//-------------------------------------------- 无限循环处理
try {
while (true) {
// 尝试获取
ttl = tryAcquire(leaseTime, unit, threadId);
// 为空获取成功跳出
if (ttl == null) {
break;
}
// --------------------------------------------------------------------
// 等待消息
if (ttl >= 0) {
getEntry(threadId).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
} else {
getEntry(threadId).getLatch().acquire();
}
}
} finally {
unsubscribe(future, threadId);
}
// get(lockAsync(leaseTime, unit));
}
不等于-1 使用传入时间
如果指定了时间走的逻辑 写了一个lua脚本 发给redis进行执行如果没指定
如果不传 符合文中上文中默认的 30 秒 通过lua 发送给redis
如果成功了
增加监听器,匿名类 operationComplete打开组件 如果不成功 return 返回, 如果成功
成功进行调度 调用调度方法 scheduleExourationRenewal
判断是否有过期时间key 如果已经存在返回, 否则放到map里面维护 创建计时器任务 重写方法
定时任务什么时候执行,30秒过期
折叠一下
队列维护时间为
internalLockLeaseTime / 3, TimeUnit.MILLISECONDS); = 30秒 / 3 也就是 10 秒 单位毫秒
过期时间中如果过了3分之一执行定时任务任然线程还在
再调用自己续期
如果没有了
任务取消
lua语言
lua语言
:Lua
是一种轻量
小巧的脚本语言,用标准C语言编写
并以源代码形式开放
, 其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。
其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。
- 轻量级: 它用标准
C语言编写
并以源代码形式开放,编译后仅仅一百余K,可以很方便的嵌入别的程序里。 - 可扩展:
Lua
提供了非常易于使用的扩展接口
和机制
应用场景
- 独立应用脚本
- 游戏开发
- Web 应用脚本
- 扩展和数据库插件如:
MySQL Proxy
和MySQL WorkBench
通过面描述因该知道是什么,反正我们也不写lua
语言,所以知道为什么要用lua
就可以了。关于为什么要在代码里拼接脚本lua 脚本
为了保证原子性打包
发给redis
进行处理
项目使用
配置
由于是基于redis之上的框架,连接方式只要连接redis就可以让redission架构再上面再封装即可
spring.redis.host= ${redis.host}
spring.redis.port= 6379
spring.redis.password=${redis.password}
spring.redis.jedis.pool.max-active=30
spring.redis.jedis.pool.max-wait=3000
spring.redis.jedis.pool.max-idle=20
spring.redis.jedis.pool.min-idle=2
spring.redis.timeout=3000
SpringBoot启动器
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.7.5</version>
</dependency>
配置单机运行
import com.alibaba.genie.aftersale.common.util.LockUtil;
import com.alibaba.genie.aftersale.common.util.redisson.RedissonLocker;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.io.IOException;
/**
* 〈功能描述〉<br>
* ----- : 备用
*
* @date 2021/10/9 11:55
*/
@Configuration
public class RedissonConfig {
@Value("${spring.redis.host}")
private String host;
@Value("${spring.redis.port}")
private String port;
@Value("${spring.redis.password}")
private String password;
/**
* RedissonClient,单机模式
*/
@Bean(destroyMethod = "shutdown")
public RedissonClient redisson() {
Config config = new Config();
config.useSingleServer().setAddress("redis://" + host + ":" + port).setPassword(password);
return Redisson.create(config);
}
@Bean
public RedissonLocker redissonLocker(RedissonClient redissonClient) {
RedissonLocker locker = new RedissonLocker(redissonClient);
//设置LockUtil的锁处理对象
LockUtil.setLocker(locker);
return locker;
}
}
工具类
import java.util.concurrent.TimeUnit;
/**
* 〈功能描述〉<br>
* ----- :
*
* @date 2021/10/9 11:56
*/
public interface Locker {
/**
* 获取锁,如果锁不可用,则当前线程处于休眠状态,直到获得锁为止。
*
* @param lockKey
*/
void lock(String lockKey);
/**
* 释放锁
*
* @param lockKey
*/
void unlock(String lockKey);
/**
* 获取锁,如果锁不可用,则当前线程处于休眠状态,直到获得锁为止。如果获取到锁后,执行结束后解锁或达到超时时间后会自动释放锁
*
* @param lockKey
* @param timeout
*/
void lock(String lockKey, int timeout);
/**
* 获取锁,如果锁不可用,则当前线程处于休眠状态,直到获得锁为止。如果获取到锁后,执行结束后解锁或达到超时时间后会自动释放锁
*
* @param lockKey
* @param unit
* @param timeout
*/
void lock(String lockKey, TimeUnit unit, int timeout);
/**
* 尝试获取锁,获取到立即返回true,未获取到立即返回false
*
* @param lockKey
* @return
*/
boolean tryLock(String lockKey);
/**
* 尝试获取锁,在等待时间内获取到锁则返回true,否则返回false,如果获取到锁,则要么执行完后程序释放锁,
* 要么在给定的超时时间leaseTime后释放锁
*
* @param lockKey
* @param waitTime
* @param leaseTime
* @param unit
* @return
*/
boolean tryLock(String lockKey, long waitTime, long leaseTime, TimeUnit unit)
throws InterruptedException;
/**
* 锁是否被任意一个线程锁持有
*
* @param lockKey
* @return
*/
boolean isLocked(String lockKey);
}
import java.util.concurrent.TimeUnit;
/**
* 〈功能描述〉<br>
* ----- :
*
* @date 2021/10/9 11:53
*/
@Data
public class RedissonLocker implements Locker {
// 配置构造方法注入
private RedissonClient redissonClient;
public RedissonLocker(RedissonClient redissonClient) {
super();
this.redissonClient = redissonClient;
}
@Override
public void lock(String lockKey) {
RLock lock = redissonClient.getLock(lockKey);
lock.lock();
}
@Override
public void unlock(String lockKey) {
RLock lock = redissonClient.getLock(lockKey);
lock.unlock();
}
@Override
public void lock(String lockKey, int leaseTime) {
RLock lock = redissonClient.getLock(lockKey);
lock.lock(leaseTime, TimeUnit.SECONDS);
}
@Override
public void lock(String lockKey, TimeUnit unit, int timeout) {
RLock lock = redissonClient.getLock(lockKey);
lock.lock(timeout, unit);
}
@Override
public boolean tryLock(String lockKey) {
RLock lock = redissonClient.getLock(lockKey);
return lock.tryLock();
}
@Override
public boolean tryLock(String lockKey, long waitTime, long leaseTime,
TimeUnit unit) throws InterruptedException {
RLock lock = redissonClient.getLock(lockKey);
return lock.tryLock(waitTime, leaseTime, unit);
}
@Override
public boolean isLocked(String lockKey) {
RLock lock = redissonClient.getLock(lockKey);
return lock.isLocked();
}
public class LockUtil {
private static Locker locker;
/**
* 设置工具类使用的locker
*
* @param locker
*/
public static void setLocker(Locker locker) {
LockUtil.locker = locker;
}
/**
* 获取锁
*
* @param lockKey
*/
public static void lock(String lockKey) {
locker.lock(lockKey);
}
/**
* 释放锁
*
* @param lockKey
*/
public static void unlock(String lockKey) {
locker.unlock(lockKey);
}
/**
* 获取锁,超时释放
*
* @param lockKey
* @param timeout
*/
public static void lock(String lockKey, int timeout) {
locker.lock(lockKey, timeout);
}
/**
* 获取锁,超时释放,指定时间单位
*
* @param lockKey
* @param unit
* @param timeout
*/
public static void lock(String lockKey, TimeUnit unit, int timeout) {
locker.lock(lockKey, unit, timeout);
}
/**
* 尝试获取锁,获取到立即返回true,获取失败立即返回false
*
* @param lockKey
* @return
*/
public static boolean tryLock(String lockKey) {
return locker.tryLock(lockKey);
}
/**
* 尝试获取锁,在给定的waitTime时间内尝试,获取到返回true,获取失败返回false,获取到后再给定的leaseTime时间超时释放
*
* @param lockKey
* @param waitTime
* @param leaseTime
* @param unit
* @return
* @throws InterruptedException
*/
public static boolean tryLock(String lockKey, long waitTime, long leaseTime,
TimeUnit unit) throws InterruptedException {
return locker.tryLock(lockKey, waitTime, leaseTime, unit);
}
/**
* 锁释放被任意一个线程持有
*
* @param lockKey
* @return
*/
public static boolean isLocked(String lockKey) {
return locker.isLocked(lockKey);
}
}
直接使用
LockUtil.lock("建", 超时时间4);
附 redis 工具类
package com.alibaba.genie.aftersale.common.util;
import com.alibaba.genie.aftersale.biz.repository.UserRepository;
import com.alibaba.genie.aftersale.dal.model.UserDO;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.support.atomic.RedisAtomicLong;
import org.springframework.stereotype.Component;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
/**
* 〈功能描述〉<br>
* ----- :
*
* @date 2021/3/16 13:36
*/
@Component
public class RedisUtil {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
UserRepository userRepository;
/**
* 判断key 是否存在,不存在创建key 自增,存在key 自增
*
* @param key
* @return
*/
public Long incrementChecker(String key) {
// 判断key是否存在
if (!hasKey(key)) {
set(key, 0);
}
return increment(key);
}
/**
* 普通缓存放入
*
* @param key 键
* @param value 值
* @return true成功 false失败
*/
public boolean set(String key, Object value) {
try {
redisTemplate.opsForValue().set(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 普通缓存获取
*
* @param key 键
* @return
*/
public Object get(String key) {
return redisTemplate.opsForValue().get(key);
}
/**
* 判断key是否存在
*
* @param key 键
* @return true 存在 false不存在
*/
public boolean hasKey(String key) {
try {
return redisTemplate.hasKey(key);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 根据key获取自增key的redis值
*
* @param key
* @return
*/
public Long getIncr(String key) {
RedisAtomicLong entityIdCounter = new RedisAtomicLong(key, Objects.requireNonNull(redisTemplate.getConnectionFactory()));
return entityIdCounter.getAndIncrement();
}
/**
* 普通缓存放入并设置时间
*
* @param key 键
* @param value 值
* @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期
* @return true成功 false 失败
*/
public boolean set(String key, Object value, long time) {
try {
if (time > 0) {
redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
} else {
set(key, value);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 自增
*
* @param key
* @return 自增后的值
*/
public Long increment(String key) {
return this.redisTemplate.opsForValue().increment(key);
}
/**
* 自增 num
*
* @param key
* @return 自增后的值
*/
public Long increment(String key, long num) {
return this.redisTemplate.opsForValue().increment(key, num);
}
/**
* 获取不可变 常量工号 真实姓名
*
*
* @param workId
* @return
*/
public String getNick(String workId) {
if (get(workId) == null) {
UserDO userInfoByWorkId = userRepository.getUserInfoByWorkId(workId);
if (userInfoByWorkId != null && StringUtils.isNotBlank(userInfoByWorkId.getNickName())) {
set(workId, userInfoByWorkId.getNickName());
return userInfoByWorkId.getNickName();
}
}
return (String) get(workId);
}
}
try {
LockUtil.lock("createLock", TimeUnit.SECONDS, 5);
} catch (Exception e) {
e.printStackTrace();
} finally {
LockUtil.unlock("createByCooperateLock");
}
关于分布式锁 缓存一致性问题闲聊
题外篇, 说说个人理解 一般解决莫过于双删
, 以及过期
时间, 但是这都可能会存在最终一致性
问题,个人常用解决办法, 通过mq
自己消费自己进行延时双删
,或者通过服务器加入中间件,默认一般都会有毕竟项目用检索同步的比较多。而且大型项目hbase
也常用,关于数据同步问题可以通过binlog日志
服务器嵌入 Canal
同步redis
删除一次搭建再也不管。