介绍:
Redisson - 是一个高级的分布式协调Redis客服端,能帮助用户在分布式环境中轻松实现一些Java的对象,Redisson、Jedis、Lettuce 是三个不同的操作 Redis 的客户端,Jedis、Lettuce 的 API 更侧重对 Reids 数据库的 CRUD(增删改查),而 Redisson API 侧重于分布式开发
使用步骤:
1.引入maven依赖
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.12.0</version>
</dependency>
2.编写配置类
@Configuration
public class MyRedissonConfig {
@Bean
public RedissonClient redissonClient() {
Config config = new Config();
config.useSingleServer().setAddress("redis://192.168.1.8:6379"); //单机模式
return Redisson.create(config);
}
}
3.使用
@Autowired
private RedissonClient redissonClient; //注入客户端操作类
public void test() {
RLock mylock = redissonClient.getLock("mylock"); //获取一把锁,只要锁名称一样就是同一把锁
//加锁:阻塞式等待,默认加锁时间是30s
//watch看门狗机制:锁自动续期;如果业务超长,运行期间会自动给锁续上新的30s;不用但心业务超长,锁自动过期会被删掉
//加锁的业务只要运行完成,就不会给当前锁续期,即使不手动解锁,锁默认在30s以后删除
mylock.lock();
try {
//业务逻辑
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}finally {
mylock.unlock(); //解锁
}
}
mylock.lock(10, TimeUnit.SCONDS);
如果使用了以上设置锁过期时间的方式,在锁时间到以后,不会自动续期
依据源码得出结论:
如果传递了超时时间,就发送给redis执行lua脚本,进行占锁,默认超时时间就是我们指定的时间;
如果未指定超时时间,就使用30*1000(看门狗的默认时间),只要占锁成功就会启用一个定时任务重新给锁设置新的过期时间,时间为看门狗默认时间,定时任务10s执行一次,即每隔10s自动续期成30s。
推荐还是使用手动指定过期时间,省掉了续期操作,手动解锁
公平锁:
RLock mylock = redissonClient.getFairLock("mylock");
可以保证多个客户端线程同时请求加锁时优先分配给先发出请求的线程,所有线程会在一个队列中排队,
当某个线程出现宕机时,redisson会等待5s后继续下一个线程,也就是说如果前面有5个线程都处于等待状态,那么后面的线程会至少等待25s
其余使用情况和 redissonClient.getLock("mylock")方式一样
读写锁:
可以保证一定读取到最新数据,写锁是一个排它锁,读锁是一个共享锁;
在并发场景下,如果写锁加锁没有释放过程中,读锁必须等待;等写锁解锁,读锁才能够继续加锁执行业务
//读锁
public void readLock() {
RReadWriteLock readWriteLock = redissonClient.getReadWriteLock("rw-lock");
RLock rLock = readWriteLock.readLock();
rLock.lock();//加读锁
try {
//业务逻辑
redisTemplate.opsForValue().get("lable");
}finally {
rLock.unlock(); //解锁
}
}
//写锁
public void writeLock() {
RReadWriteLock readWriteLock = redissonClient.getReadWriteLock("rw-lock");
RLock rLock = readWriteLock.writeLock();
try {
rLock.lock();//加写锁
//改数据加写锁,读数据加读锁
redisTemplate.opsForValue().set("lable", System.currentTimeMillis() + "");
}finally {
rLock.unlock(); //解锁
}
}
各种使用方式测试结果:
读+读: 相当于无锁,并发读,只会在redis中记录好,所有当前的读锁,都会同时加锁成功
写+读: 等待写锁释放
写+写:阻塞方式。等待第一个写锁释放
读+写: 有读锁,写锁也需要等待
结论: 有写锁的地方,都必须等待。
闭锁:
public void lockDoor() throws InterruptedException {
RCountDownLatch door = redissonClient.getCountDownLatch("door");
//设置五个闭锁
door.trySetCount(5);
//阻塞等待闭锁全部完成才往下执行
door.await();
log.info("全部执行完成");
}
public void implDoor() {
RCountDownLatch door = redissonClient.getCountDownLatch("door");
//计数减一
door.countDown();
log.info("执行完成,执行数量减一");
}
信号量:
public void acquire() throws InterruptedException {
RSemaphore semaphore = redissonClient.getSemaphore("semaphore");
semaphore.acquire();
log.info("获取一个信号量。。。");
}
public void release() {
RSemaphore semaphore = redissonClient.getSemaphore("semaphore");
semaphore.release();
log.info("释放一个信号量。。。");
}
提前存一个键名为semaphore值为3到redis中
执行3次acquire()方法后,第四次执行则阻塞,等执行release()方法释放信号量后才继续往下执行
boolean b = semaphore.tryAcquire();
if(b) {
//执行业务
}else {
return "error";
}
还可以使用以上方式
可以用作分布式限流
缓存数据一致性:
了解canal,是一个同步数据的中间件技术:
缓存数据一致性-分析-解决方案:
无论是双写模式还是失效模式,都会导致缓存的不一致性问题,即多个实例同时更新会出问题,解决方案如下:
1.如果是用户维度(订单数据、用户数据),这种并发几率非常小,缓存数据加上过期时间,每隔一段时间触发读的主动更新即可
2.如果是菜单,商品介绍等基础数据,也可以使用canal订阅binlog的方式(技术如图上介绍)
3.缓存数据+过期时间也足够解决大部分业务对于缓存的要求
4.通过加锁保证并发读写,写写的时候按顺序排队,读读无所谓,所以适用读写锁(业务不关心脏数据,允许临时脏数据可忽略)
总结:
能放入缓存的数据本就是不应该是实时性、一致性要求高的数据,所以缓存数据的时候加上过期时间,保证每天拿到的数据是最新数据即可。
不应该过度设置,增加系统的复杂性
遇到实时、一致性要求高的数据就应该查数据库,即使慢一点。
系统一致性解决方案:
1、缓存的所有数据都有过期时间,数据过期下一次查询触发主动更新
2、读写数据的时候,加上分布式读写锁