目录
RedissonClient实现最基本的锁
RedissonClient实现读写锁
RedissonClient实现闭锁
RedissonClient信号量
缓存不一致问题解决方案
一、双写模式
二、失效模式
RedissonClient实现最基本的锁
// 1、获取一把锁,只要锁的名字一样,就是同一把锁
RLock mylock = redisson.getLock("mylock");
// 2、获取锁并执行业务
//mylock.lock(); // 加锁,阻塞式等待,默认加的锁都是30s时间
// 1) 锁的自动续期,如果业务超长,运行期间自动给锁续上新的30s(看门狗机制),不用担心业务时间长导致锁自动过期被删掉
// 2) 加锁的业务只要运行完成,就不会给当前锁续期,即使不手动解锁,锁默认30s以后自动删除
mylock.lock(10, TimeUnit.SECONDS);
// 上面这个锁时间到了以后,不会自动续费
// 1、如果我们传递了锁的超时时间,就发送给redis执行脚本,进行占领,默认超时时间就是我们指定的时间
// 2、如果我们未指定锁的超时时间,就是用30*1000【lockWatchDogTimeout看门狗的默认时间】
// 只要占领成功,就会启动一个定时任务【重新给锁设置过期时间,新的过期时间就是看门狗的默认时间】,每隔10s都会自动续期
// internalLockLeaseTime【看门狗时间】 / 3 == 10s 10s一续期
try {
System.out.println("加锁成功,执行业务," + Thread.currentThread().toString());
} catch (Exception e) {
} finally {
// 无论业务执行的如何都得释放锁
mylock.unlock();
System.out.println("释放锁," + Thread.currentThread().toString());
}
RedissonClient实现读写锁
// 保证一定能读到最新数据,修改期间,写锁是一个排他锁(互斥锁)。读锁是一个共享锁
// 写锁没释放读就必须等待
// 读 + 读 : 相当于无锁,并发读,只会在redis中记录好,所有当前的读锁,他们都会同时加锁成功
// 写 + 读 : 等待写锁释放
// 写 + 写 : 阻塞方式
// 读 + 写 : 有读锁,写也需要等待
//
@GetMapping("/write")
@ResponseBody
public String writeValue() {
RReadWriteLock lock = redisson.getReadWriteLock("rw-lock");
String s = "";
// 写锁
RLock rLock = lock.writeLock();
try {
// 写锁上锁
rLock.lock();
System.out.println("写锁加锁成功。。。" + Thread.currentThread().toString());
s = UUID.randomUUID().toString();
Thread.sleep(30000);
redisTemplate.opsForValue().set("writeValue", s);
} catch (Exception e) {
e.printStackTrace();
} finally {
rLock.unlock();
System.out.println("写锁释放。。。" + Thread.currentThread().toString());
}
return s;
}
@GetMapping("/read")
@ResponseBody
public String readValue() {
RReadWriteLock lock = redisson.getReadWriteLock("rw-lock");
String s = "";
// 写锁
RLock rLock = lock.readLock();
try {
// 写锁上锁
rLock.lock();
System.out.println("读锁加锁成功。。。" + Thread.currentThread().toString());
s = UUID.randomUUID().toString();
Thread.sleep(30000);
s = redisTemplate.opsForValue().get("writeValue");
} catch (Exception e) {
e.printStackTrace();
} finally {
rLock.unlock();
System.out.println("读锁释放。。。" + Thread.currentThread().toString());
}
return s;
}
RedissonClient实现闭锁
// 等待房间内的5个人离开后闭锁才能成功,否则await一直阻塞
@GetMapping("/lockDoor")
@ResponseBody
public String lockDoor() throws InterruptedException {
RCountDownLatch lock = redisson.getCountDownLatch("door");
lock.trySetCount(5);
lock.await(); // 阻塞等待闭锁完成
return "锁门成功";
}
@GetMapping("/leave/{id}")
@ResponseBody
public String leave(@PathVariable("id")Long id) throws InterruptedException {
RCountDownLatch lock = redisson.getCountDownLatch("door");
lock.countDown(); // 计数-1
return id + "离开了房间";
}
RedissonClient信号量
// 需要先在redis中存储semaphore这个key并设置数量
// 信号量也可以用作分布式限流
@GetMapping("/semaphore")
@ResponseBody
public String park() throws InterruptedException {
RSemaphore semaphore = redisson.getSemaphore("semaphore");
semaphore.acquire(); // 获得信号量,必须获得,否则阻塞
/*
boolean b = semaphore.tryAcquire();// 尝试获得,可通过返回值进行一定的操作
if(b) {
// ....
} else {
// .... return error啥的
}
*/
return "获得信号量成功";
}
@GetMapping("/go")
@ResponseBody
public String go() throws InterruptedException {
RSemaphore semaphore = redisson.getSemaphore("semaphore");
semaphore.release(); // 释放信号量
return "go";
}
缓存不一致问题解决方案
一、双写模式
在更新数据库后立即更新缓存,但是有漏洞,
由于卡顿等原因,导致写缓存
2
在最前,写缓存
1
在后面就出现了不一致
这是暂时性的脏数据问题,但是在数据稳定,缓存过期以后,又能得到最新的正确数据,即能够保证最终一致性
二、失效模式
在更新数据库后将缓存直接删除,简单方便,但是也可能出现漏洞
读缓存线程读取的仍然是旧数据,写进缓存的也是旧的数据
无论是双写模式还是失效模式,都会导致缓存的不一致问题。即多个实例同时更新会出事。怎么办?
- 1、如果是用户纬度数据(订单数据、用户数据),这种并发几率非常小,不用考虑这个问题,缓存数据加上过期时间,每隔一段时间触发读的主动更新即可
- 2、如果是菜单,商品介绍等基础数据,也可以去使用canal订阅binlog的方式。
- 3、缓存数据+过期时间也足够解决大部分业务对于缓存的要求。
- 4、通过加锁保证并发读写,写写的时候按顺序排好队。读读无所谓。所以适合使用读写锁(业务不关心脏数据,允许临时脏数据可忽略);
总结:
- 我们能放入缓存的数据本就不应该是实时性、一致性要求超高的。所以缓存数据的时候加上过期时间,保证每天拿到当前最新数据即可。
- 我们不应该过度设计,增加系统的复杂性
- 遇到实时性、一致性要求高的数据,就应该查数据库,即使慢点。