Spring Boot使用Redisson分布式锁解决缓存击穿问题
文章目录
1 什么是缓存击穿
一份热点数据,它的访问量非常大。在其缓存失效的瞬间,大量请求直达存储层,导致服务崩溃。
2 为什么要使用分布式锁
在项目中,当共享资源出现竞争情况的时候,为了防止出现并发问题,我们一般会采用锁机制来控制。在单机环境下,可以使用synchronized或Lock来实现;但是在分布式系统中,因为竞争的线程可能不在同一个节点上(同一个jvm中),所以需要一个让所有进程都能访问到的锁来实现,比如mysql、redis、zookeeper。
3 什么是Redisson
Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还实现了可重入锁(Reentrant Lock)、公平锁(Fair Lock、联锁(MultiLock)、 红锁(RedLock)、 读写锁(ReadWriteLock)等,还提供了许多分布式服务。Redisson提供了使用Redis的最简单和最便捷的方法。Redisson的宗旨是促进使用者对Redis的关注分离(Separation of Concern),从而让使用者能够将精力更集中地放在处理业务逻辑上。
4 Spring Boot集成Redisson
4.1 添加maven依赖
不再需要spring-boot-starter-data-redis依赖,但是都添加也不会报错
<!--redisson-->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.17.0</version>
</dependency>
4.2 配置yml
spring:
datasource:
username: xx
password: xxxxxx
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf-8&serverTimezone=CTT
cache:
type: redis
redis:
database: 0
port: 6379 # Redis服务器连接端口
host: localhost # Redis服务器地址
password: xxxxxx # Redis服务器连接密码(默认为空)
timeout: 5000 # 超时时间
4.3 配置RedissonConfig
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.time.Duration;
import java.util.Random;
@EnableCaching
@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;
@Bean(destroyMethod = "shutdown") // bean销毁时关闭Redisson实例,但不关闭Redis服务
public RedissonClient redisson() {
//创建配置
Config config = new Config();
/**
* 连接哨兵:config.useSentinelServers().setMasterName("myMaster").addSentinelAddress()
* 连接集群: config.useClusterServers().addNodeAddress()
*/
config.useSingleServer()
.setAddress("redis://" + host + ":" + port)
.setPassword(password)
.setTimeout(5000);
//根据config创建出RedissonClient实例
return Redisson.create(config);
}
@Bean
public CacheManager RedisCacheManager(RedisConnectionFactory factory) {
RedisSerializer<String> redisSerializer = new StringRedisSerializer();
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
// 解决查询缓存转换异常的问题
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
/**
* 新版本中om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL)已经被废弃
* 建议替换为om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL)
*/
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
// 配置序列化解决乱码的问题
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
// 设置缓存过期时间 为解决缓存雪崩,所以将过期时间加随机值
.entryTtl(Duration.ofSeconds(60 * 60 + new Random().nextInt(60 * 10)))
// 设置key的序列化方式
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
// 设置value的序列化方式
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer));
// .disableCachingNullValues(); //为防止缓存击穿,所以允许缓存null值
RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
.cacheDefaults(config)
// 启用RedisCache以将缓存 put/evict 操作与正在进行的 Spring 管理的事务同步
.transactionAware()
.build();
return cacheManager;
}
}
5 使用Redisson的分布式锁解决缓存击穿
package com.admin.sys.service.impl;
import com.admin.sys.dao.SysRoleDao;
import com.admin.sys.model.domain.SysRoleDO;
import com.admin.sys.service.SysRoleService;
import com.alibaba.fastjson2.JSON;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
@Service
public class SysRoleServiceImpl extends ServiceImpl<SysRoleDao, SysRoleDO> implements SysRoleService {
@Resource
private RedissonClient redissonClient;
@Override
public List<SysRoleDO> test() throws Exception {
Object roles = redissonClient.getBucket("role").get();
// 先查询缓存,缓存中有则直接返回
if (Objects.nonNull(roles)) {
return JSON.parseArray(roles.toString(), SysRoleDO.class);
}
RLock lock = redissonClient.getLock("role-lock");
boolean isLock = lock.tryLock();
if (isLock) {
// 获取到锁查询数据库,并将查询结果放入缓存
try {
Object roleList = redissonClient.getBucket("role").get();
// 双重检查锁,当多个线程同时判断到缓存中取不到值,上一个获取到锁的线程已经将数据放入缓存,下一个线程直接取缓存
if (Objects.nonNull(roleList)) {
return JSON.parseArray(roleList.toString(), SysRoleDO.class);
}
// 查询数据库
List<SysRoleDO> list = this.list();
// 将数据放入缓存
redissonClient.getBucket("role").set(list, 60L, TimeUnit.SECONDS);
return list;
} finally {
lock.unlock();
}
}
int retryTimes = 3;
Object roleList = null;
// 当缓存中取不到值时sleep300毫秒,最多循环3次
while (Objects.isNull(roleList) && retryTimes > 0) {
// 休眠300ms后递归
TimeUnit.MILLISECONDS.sleep(300L);
roleList = redissonClient.getBucket("role").get();
retryTimes--;
}
// 循环等待后缓存中取到值直接返回,仍然取不到值则抛异常
if (Objects.nonNull(roleList)) {
return JSON.parseArray(roleList.toString(), SysRoleDO.class);
}
throw new RuntimeException("查询异常");
}
}