缓存问题(四) 缓存穿透、缓存雪崩、缓存并发 解决案例
视频地址: https://www.bilibili.com/video/BV1Ha411c7hB
代码地址: https://gitee.com/crazyliyang/video-teaching.git
1. 缓存穿透 解决案例
使用布隆过滤器
核心代码, 使用 Redisson 库的布隆过滤器: org.redisson.api.RBloomFilter
<!-- Redis 客户端工具 redisson 实现对 Redisson 的自动化配置--> <dependency> <groupId>org.redisson</groupId> <artifactId>redisson-spring-boot-starter</artifactId> <version>3.13.4</version> </dependency>
配置类, 实例化布隆过滤器 RBloomFilter
@Configuration public class RedisConfiguration { /** * 预计要插入多少数据 */ private static int size = 100000; // 十万 /** * 期望的误判率 */ private static double fpp = 0.001; @Value("${spring.redis.host}") private String redistHost; @Value("${spring.redis.port}") private Integer redistPort; @Autowired private ProductService productService; // 商品服务 @Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) { // 创建 RedisTemplate 对象 RedisTemplate<String, Object> template = new RedisTemplate<>(); // 设置开启事务支持 template.setEnableTransactionSupport(true); // 设置 RedisConnection 工厂。 它就是实现多种 Java Redis 客户端接入的秘密工厂 template.setConnectionFactory(factory); // 使用 String 序列化方式,序列化 KEY 。 template.setKeySerializer(RedisSerializer.string()); // 使用 JSON 序列化方式(库是 Jackson ),序列化 VALUE 。 template.setValueSerializer(RedisSerializer.json()); return template; } /** * 实例化布隆过滤器 * */ @Bean public RBloomFilter redisBloomFilter(){ String redisAddress = "redis://"+redistHost+ ":"+redistPort; Config config = new Config(); config.useSingleServer().setAddress(redisAddress); RedissonClient redisson = Redisson.create(config); // 创建RedisBloomFilter RBloomFilter<String> redisBloomFilter = redisson.getBloomFilter("productBloomFilter03"); //初始化布隆过滤器:预计元素为10000L ( 十万), 误差率为 0.001 redisBloomFilter.tryInit(size, fpp); return redisBloomFilter; } }
使用隆过滤器 RBloomFilter
@Service public class ProductServiceImpl extends ServiceImpl<ProductMapper, ProductEntity> implements ProductService { //redisKey前缀 private static final String KEY_PREFIX = "product:"; // 格式化一下 private static String buildKey(String id) { return KEY_PREFIX + id; // product:商品ID } @Resource(name = "redisTemplate") private ValueOperations<Serializable, String> redisOperations; @Autowired private RBloomFilter<String> redisBloomFilter; /** * 重载 getById(id) 做第一点增强 * */ @Override public ProductEntity getById(Serializable id) { String redisKey = buildKey(String.valueOf(id)); // 构建rediskey if(!redisBloomFilter.contains(redisKey)){ // 布隆过滤器判断 redisKey 不存在 return null; } String redisValue = redisOperations.get(redisKey); // 从缓存中去查 if (redisValue != null) { // 查到了 return JSONUtil.parseObject(redisValue, ProductEntity.class); //直接返回; } else { // 没查到 ProductEntity product = getBaseMapper().selectById(id); // 从数据库中查, 然后放到缓存中; if(product !=null){ String jsonString = JSONUtil.toJSONString(product); redisOperations.set(redisKey, jsonString); // 放到缓存中 return product; } return null; } } }
2. 缓存雪崩 解决案例
核心代码: 使用一个过期时间的随机数, 让 key 的过期时间分散化, 防止 key 大批集中过期;
@Service public class ProductServiceImpl extends ServiceImpl<ProductMapper, ProductEntity> implements ProductService { private static final String KEY_PREFIX = "product:"; private static String buildKey(String id) { return KEY_PREFIX + id; // product:商品ID } @Resource(name = "redisTemplate") private ValueOperations<Serializable, String> redisOperations; @Autowired private RBloomFilter<String> redisBloomFilter; /** * 重载 getById(id) 做第一点增强 */ @Override public ProductEntity getById(Serializable id) { String redisKey = buildKey(String.valueOf(id)); // 构建rediskey if(!redisBloomFilter.contains(redisKey)){ // 布隆过滤器判断 redisKey 不存在 return null; } String redisValue = redisOperations.get(redisKey); // 从缓存中去查 if (redisValue != null) { // 查到了 return JSONUtil.parseObject(redisValue, ProductEntity.class); //直接返回; } else { // 没查到 ProductEntity product = getBaseMapper().selectById(id); // 从数据库中查, 然后放到缓存中; if(product !=null){ String jsonString = JSONUtil.toJSONString(product); long radomLong = radomLong(); // 随机产生一个数 ( 24 ~ 72 ) redisOperations.set(redisKey, jsonString, radomLong, TimeUnit.HOURS); // 放到缓存中, 时间取随机, 时间单位小时 return product; } return null; } } public static long radomLong(){ long min = 24; long max = 72; long rangeLong = min + (((long) (new Random().nextDouble() * (max - min)))); return rangeLong; } }
3. 缓存并发 解决案例
使用Redis 分布式锁, 核心代码:
封装 RedisLock
@Component public class RedisLock { @Autowired private RedissonClient redissonClient; /** * 加锁 * @param lockKey * @return */ public RLock lock(String lockKey) { RLock lock = redissonClient.getLock(lockKey); lock.lock(); return lock; } /** * 释放锁 * @param lockKey */ public void unlock(String lockKey) { RLock lock = redissonClient.getLock(lockKey); lock.unlock(); } /** * 释放锁 * @param lock */ public void unlock(RLock lock) { lock.unlock(); } /** * 带超时的锁 * @param lockKey * @param timeout 超时时间 单位:秒 */ public RLock lock(String lockKey, int timeout) { RLock lock = redissonClient.getLock(lockKey); lock.lock(timeout, TimeUnit.SECONDS); return lock; } /** * 带超时的锁 * @param lockKey * @param unit 时间单位 * @param timeout 超时时间 */ public RLock lock(String lockKey, TimeUnit unit , int timeout) { RLock lock = redissonClient.getLock(lockKey); lock.lock(timeout, unit); return lock; } /** * 尝试获取锁 * @param lockKey * @param waitTime 最多等待时间 * @param leaseTime 上锁后自动释放锁时间 * @return */ public boolean tryLock(String lockKey, int waitTime, int leaseTime) { RLock lock = redissonClient.getLock(lockKey); try { return lock.tryLock(waitTime, leaseTime, TimeUnit.SECONDS); } catch (InterruptedException e) { return false; } } /** * 尝试获取锁 * @param lockKey * @param unit 时间单位 * @param waitTime 最多等待时间 * @param leaseTime 上锁后自动释放锁时间 * @return */ public boolean tryLock(String lockKey, TimeUnit unit, int waitTime, int leaseTime) { RLock lock = redissonClient.getLock(lockKey); try { return lock.tryLock(waitTime, leaseTime, unit); } catch (InterruptedException e) { return false; } } }
配置类 实例化 注册 RedissonClient
@Configuration public class RedisConfiguration { @Value("${spring.redis.host}") private String redistHost; @Value("${spring.redis.port}") private Integer redistPort; @Autowired private ProductService productService; // 商品服务 /** * RedissonClient 注册 */ @Bean public RedissonClient redissonClient(){ String redisAddress = "redis://"+redistHost+ ":"+redistPort; Config config = new Config(); config.useSingleServer().setAddress(redisAddress); RedissonClient redissonClient = Redisson.create(config); return redissonClient; } }
使用Redis 分布式锁:
@Service public class ProductServiceImpl extends ServiceImpl<ProductMapper, ProductEntity> implements ProductService { //前缀 private static final String KEY_PREFIX = "product:"; private static String buildKey(String id) { return KEY_PREFIX + id; // product:商品ID } @Resource(name = "redisTemplate") private ValueOperations<Serializable, String> redisOperations; @Autowired private RBloomFilter<String> redisBloomFilter; @Autowired private RedisLock redisLock; // 自己封装的 redisLock /** * 重载 getById(id) 做第一点增强 */ @Override public ProductEntity getById(Serializable id) { String redisKey = buildKey(String.valueOf(id)); // 构建rediskey if (!redisBloomFilter.contains(redisKey)) { // 布隆过滤器判断 redisKey 不存在 return null; } String redisValue = redisOperations.get(redisKey); // 从缓存中去查 if (redisValue != null) { // 查到了 return JSONUtil.parseObject(redisValue, ProductEntity.class); //直接返回; } // 在查询 DB 之前加 分布式锁 redisLock.lock(String.valueOf(id)); try{ // 再一次查询缓存, 是为了分布式并发情况下, 其他并发的请求, 能在锁解开之后, 再次查缓存 String redisValue2 = redisOperations.get(redisKey); if (redisValue2 != null) { return JSONUtil.parseObject(redisValue, ProductEntity.class); } ProductEntity product = getBaseMapper().selectById(id); // 从数据库中查 if (product != null) { String jsonString = JSONUtil.toJSONString(product); redisOperations.set(redisKey, jsonString); // 放到缓存中 return product; } return null; }finally { redisLock.unlock(String.valueOf(id)); // 释放锁 } } }