缓存
使用Redis实现分布式缓存
- Spring Boot 已经整合了Redis,使用时需要引入starter,并配置Redis的Server信息
- 提供了RedisTemplate和StringRedisTemplate来操作Redis ```java // 通用的Redis模板 @Autowired private RedisTemplate redisTemplate
// 只能存储String的Redis模版 @Autowired private StringRedisTemplate stringRedisTemplate
public void test() {
// 获取String数据类型的操作对象
ValueOperations
- 统一使用JSON作为value进行存储
- [Win系统Redis客户端Lettuce出现内存泄漏问题](https://www.bilibili.com/video/BV1np4y1C7Yf?p=154)
<a name="VfETF"></a>
# 缓存失效问题
**缓存穿透**
- 缓存穿透是指查询一个一定不存在的数据,由于缓存是不命中,将去查询数据库,但是数据库也无此记录,我们没有将这次查询的 null 写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。
- 在流量大时,可能DB就挂掉了,要是有人利用不存在的key频繁攻击我们的应用,这就是漏洞。
- 解决: **缓存空结果**、并且设置短的过期时间。
**缓存雪崩**
- 缓存雪崩是指在我们设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到 DB,DB 瞬时压力过重雪崩。
- 解决:**原有的失效时间基础上增加一个随机值**,比如 1-5 分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。
**缓存击穿**
- 对于一些设置了过期时间的key,如果这些key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。
- 这个时候,需要考虑一个问题:如果这个key在大量请求同时进来前正好失效,那么所有对这个 key 的数据查询都落到 db,我们称为缓存击穿。
- 解决:**加锁**
<a name="mvJNB"></a>
# Redis实现分布式锁
```java
public void getDataWithReidsLock() {
// 1. 占领分布式锁(加锁和设置过期时间要保证原子性)
String UUID = UUID.randomUUID().toString();
Boolen lock = redisTemplate.opsForValue().setIfAbsent("lock", UUID, 300, TimeUnit.SECONDS);
// 相当于 set lock UUID EX 300 NX
if(lock) {
Map<String, List<Catelog2Vo>> dataFromDb;
try {
dataFromDb = getDataFromDb();
System.out.println("获取数据");
return;
} finally {
// 2. 解锁时也要保证原子性
// 使用Lua脚本实现原子操作:获取值对比+对比成功删除=原子操作
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Long lock1 = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList("lock"), uuid);
}
} else {
// 获取锁失败,进行自旋
Thread.sleep(100);
getDataWithRedisLock();
}
}
Redisson
配置Redisson客户端
@Configuration public class MyRedissonConfig{ @Bean public RedissonClient redisson() throws IOExcepton{ // 默认连接地址 127.0.0.1:6379 RedissonClient redisson = Redisson.create(); Config config = new Config(); config.useSingleServer().setAddress("redis://192.168.56.10:6379"); RedissonClient redisson = Redisson.create(config); } }
使用分布式锁 ```java // 获取分布式锁,只要锁的名称一样,就是同一把锁 RLock lock = redisson.getLock(“anyLock”);
// 最常见的使用方法(上锁失败时进行阻塞等待),默认加锁30s 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(); } }
- Redisson提供的RLock实现了Lock接口
- 不指定过期时间时默认设置key的有效时间为30s
- 锁可以自动续期:业务执行时间超长时,运行期间会自动续期,每次续期30s
**读写锁**
- Redisson提供了读写锁`redisson.getReadWriteLock()`
- 保证一定可以读到最新数据,写锁释放前读锁必须等待
```java
RReadWriteLock lock = redisson.getReadWriteLock("rw-lock");
// 加上读锁
RLock rlock = lock.readLock();
rlock.lock();
- 写锁互斥,读锁共享
- 写-写、写-读、读-写都是互斥操作
- 只有写-写模式可以共享
信号量 Semaphore
- 使用信号量可以进行限流
// 获取信号量对象 RSemaphore semaphore = redisson.getSemaphore("semaphore"); // 获取信号,阻塞 semaphore.aquire(); // 尝试获取,不阻塞 semaphore.tryAcquire(); // 释放信号量 semaphore.release();
闭锁 CountDownLatch
- 等待所有任务都完成后再继续主线程 ```java RCountDownLatch latch = redisson.getCountDownLatch(“latch”);
// 设置等待任务数 latch.trySetCount(5); latch.await(); // 进行阻塞等待
latch.countDown(); // 计数-1 ```
缓存一致性
1、双写模式:写数据库 + 写缓存
- 解决方案:
1、加锁,将写数据库和写缓存操作放在一起操作
2、如果对于数据的实效性不敏感,可以只设置好缓存中的数据过期时间,数据过期后自动查询最新数据
2、失效模式:写数据库 + 删缓存
- 解决方案:
1、加锁,将写数据库和删缓存操作放在一起操作
2、如果对于数据的实效性不敏感,可以只设置好缓存中的数据过期时间,数据过期后自动查询最新数据
3、如果数据经常修改,则放弃缓存
一致性解决方案
1、过期时间
2、分布式锁
3、canal