一丶设置逻辑过期时间来处理缓存击穿
1.准备工作:
先准备一个redisdata类,该类包含redis的data属性,以及逻辑过期的时间属性。
package com.zzx.plus.dto;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class RedisData<T> {
private T data;
private LocalDateTime localDateTime;
}
2.处理缓存击穿的大致思路
3.编写逻辑过期的业务逻辑
private static final ExecutorService executorService = Executors.newFixedThreadPool(10);
public Result handelerlockhcs(Long id) throws Exception{
//根据id生成动态key值
String user_key = "user:" + id;
User user = null;
//查询缓存
String user1 = stringRedisTemplate.opsForValue().get(user_key);
//如果未命中缓存直接返回null
if (user1==null){
return Result.ok("null");
}
//如果命中缓存
//解析成user对象
RedisData redisData = JSONUtil.toBean(user1, RedisData.class);
user = JSONUtil.toBean((JSONObject) redisData.getData(), User.class);
//判断缓存是否过期
if (redisData.getLocalDateTime().isAfter(LocalDateTime.now())) {
//如果没有过期则直接返回缓存
return Result.ok(user);
}
//如果过期则开启一个线程去构建新的缓存
//RLock lock = redissonClient.getLock("lock_" + id);
boolean islock = tryLock("lock_" + id);
if (islock) {
//如果获取到锁,开启一个线程去查询数据库重建缓存
executorService.submit(() -> {
try {
handelechache(id, 1L);
} catch (Exception e) {
e.printStackTrace();
} finally {
//释放锁
unLock("lock_" + id);
}
});
}
//返回旧的缓存
return Result.ok(user);
}
private void handelechache(Long id, long time) {
//查询数据库数据
User user = userMapper.selectByid(id);
//新建rediasdata对象
RedisData<User> redisData = new RedisData<>();
redisData.setData(user);
//plusMinutes 推迟time分钟
redisData.setLocalDateTime(LocalDateTime.now().plusMinutes(time));
//存入redis
stringRedisTemplate.opsForValue().set("user:"+id,JSONUtil.toJsonStr(redisData));
}
public Boolean tryLock(String key){
return stringRedisTemplate.opsForValue().setIfAbsent(key,"1",10,TimeUnit.SECONDS);
}
/**
* 释放锁
* @param key
* @return
*/
public Boolean unLock(String key){
return stringRedisTemplate.delete(key);
}
4.逻辑过期时间一般用于事先需要准备好的缓存,就好比双十一搞活动事先把点击率高的都缓存一遍,然后再设置个逻辑过期时间,这样缓存不会真的失效,即使缓存过期了也会先用旧的数据先顶着用,这样大大提高了性能。缺点就是数据不能保持一致,如果数据库发送更新操作,需要等到缓存过期才能更新缓存。
二丶设置分布式互斥锁来解决
1.准备工作
加入redisson依赖
<!--redisson-->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.13.6</version>
</dependency>
编写配置类Redisson
@Configuration
public class RedissonConfig {
@Bean
public RedissonClient redissonClient(){
Config config = new Config();
//SingleServerConfig 对象有一系列的方法可以用于设置连接参数
config.useSingleServer().setAddress("redis://192.168.179.5:6379")
.setPassword("123")
.setDatabase(7);
// 创建 RedissonClient 实例,并将其返回作为 Bean。
return Redisson.create(config);
}
}
2.大致思路:
3.业务逻辑代码:
public Result handelerlockhc(Long id){
//根据id生成动态key值
String user_key = "user:"+id;
User user = null;
//查询缓存
String user1 = stringRedisTemplate.opsForValue().get(user_key);
//如果命中缓存
if (StrUtil.isNotBlank(user1)) {
//解析成user对象
user = JSONUtil.toBean(user1, User.class);
return Result.ok(user);
}
//如果命中为空在返回错误信息
if (user1!=null) {
return Result.erro("错误,请输入正确的地址");
}
//如果未命中缓存
//先判断该线程是否获取锁
//生成锁对象
RLock lock = redissonClient.getLock("lock_" + id);
try {
//获取锁
boolean lock1 = lock.tryLock( 1L, TimeUnit.MINUTES);
//如果在一分钟内没有获取锁
if (!lock1){
//返回错误信息
return Result.erro("请求超时");
}
//如果获取到了锁
// 再次检查缓存,避免多个线程同时查询数据库
String userJson = stringRedisTemplate.opsForValue().get(user_key);
if (StrUtil.isNotBlank(userJson)) {
user = JSONUtil.toBean(userJson, User.class);
return Result.ok(user);
}
//查询数据库
user = userMapper.selectByid(id);
//如果查询不到缓存空值
if (user==null) {
stringRedisTemplate.opsForValue().set(user_key,"");
return Result.erro("错误,请输入正确的地址");
}
//构建缓存
//生成一个随机的ttl过期时间防止key同时过期而造成的缓存雪崩
long time = RandomUtil.randomLong(20, 30);
stringRedisTemplate.opsForValue().set(user_key,JSONUtil.toJsonStr(user),time, TimeUnit.MINUTES);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//释放锁
lock.unlock();
}
//返回结果集
return Result.ok(user);
}
4.总结:分布式互斥锁处理缓存击穿的好处就是能保持数据库数据和缓存的一致性,但是缺点就是
效率低,性能低,如果数据库发送变化,为了保证缓存一直需要删除原有的缓存重构缓存,这个
时候就需要用到锁需要等待锁的释放性能相比较低。