什么是缓存击穿:
缓存击穿是指在高并发环境下,某个热点数据的缓存过期,导致大量请求同时访问后端存储系统,引起系统性能下降和后端存储压力过大的现象。
解决方案:
1. redisson分布式锁
本质上是缓存重建的过程中,大量的请求访问到后端的数据库导致数据库压力过大
那么可以使用redisson分布式锁来对缓存重建的过程加锁
其它的线程只有缓存重建完毕之后才可以访问
缺点:所有的请求都要等待拿到锁的线程来进行缓存重建
优点:数据拥有高一致性,适用于某些涉及“钱”的业务,或者要求数据的强一致性的。
- 新建redisson子工程单独作为微服务名字叫redisson-starter
- 引入redisson相关依赖
<dependency><groupId>org.redisson</groupId><artifactId>redisson-spring-boot-starter</artifactId><version>3.17.5</version></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.18.18</version></dependency>
- 项目结构
RedissonConfigProperties:一些redisson需要的配置项,如果是集群此处不能用这种方式
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;@Data
@ConfigurationProperties(prefix = "redisson-lock")
public class RedissonConfigProperties {private String redisHost;private String redisPort;}
RedissonConfig:配置RedissonClient,并且加入了一些自动装配的配置
import com.yonchao.redisson.service.RedissonLockService;
import lombok.Data;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;import java.io.IOException;@Data
@Configuration
@EnableConfigurationProperties({RedissonConfigProperties.class})
//当引入Service接口时
@ConditionalOnClass(RedissonLockService.class)
public class RedissonConfig {@Autowiredprivate RedissonConfigProperties redissonConfigProperties;/*** 对 Redisson 的使用都是通过 RedissonClient 对象* @return*/@Bean(destroyMethod="shutdown") // 服务停止后调用 shutdown 方法。public RedissonClient redisson() {// 1.创建配置Config config = new Config();// 集群模式// config.useClusterServers().addNodeAddress("127.0.0.1:7004", "127.0.0.1:7001");// 2.根据 Config 创建出 RedissonClient 示例。config.useSingleServer().setAddress("redis://"+redissonConfigProperties.getRedisHost()+":"+redissonConfigProperties.getRedisPort());return Redisson.create(config);}
}
spring.factories:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\com.yonchao.redisson.service.RedissonLockService
业务类:
import com.yonchao.redisson.service.RedissonLockService;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;import java.util.concurrent.TimeUnit;@Service
public class RedissonLockServiceImpl implements RedissonLockService {@Autowiredprivate RedissonClient redissonClient;/*** 加锁* @param lockKey* @return*/public boolean acquireLock(String lockKey) {RLock lock = redissonClient.getLock(lockKey);try {return lock.tryLock(30, TimeUnit.SECONDS);} catch (InterruptedException e) {Thread.currentThread().interrupt();return false;}}/*** 释放锁* @param lockKey* @return*/public void releaseLock(String lockKey) {RLock lock = redissonClient.getLock(lockKey);lock.unlock();}
}
- 其它微服务通过pom引入redisson-starter微服务
- 重建缓存过程中使用分布式锁
首先注入RedissonClient
接着判断布隆过滤器
接着从缓存中读数据
读不到的话需要重建缓存
重建缓存:
首先获取分布式锁
获取成功了就查询数据库并且重建缓存返回数据最后释放锁
获取分布式锁失败就等待1秒接着递归这个方法,直到有一个线程重建缓存成功。
/*** 使用逻辑过期的方式* @param id* @return*/@Overridepublic ResponseResult selectArticleLogicalExpiration(Long id) {// 首先经过布隆过滤器// 判断这个id是不是在布隆过滤器中boolean mightContain = bloomFilter.mightContain(id);// 不存在直接返回if (!mightContain) {return ResponseResult.okResult();}// 首先从缓存中获取数据Object articleObj = redisTemplate.opsForValue().get(ARTICLE_KEY + id);if (Objects.nonNull(articleObj)){String articleJSON = (String) articleObj;JSONObject jsonObject = JSON.parseObject(articleJSON);Long expired = jsonObject.getLong("expired");// 旧的文章对象ApArticle article = JSON.parseObject(articleJSON, ApArticle.class);if (Objects.nonNull(expired)){// 未过期直接返回if (expired - System.currentTimeMillis() > 0) {return ResponseResult.okResult(article);}// 过期了进行缓存的重建boolean acquiredLock = redissonLockService.acquireLock(lockKeyRedisson);// 拿到锁了就 新开一个线程 进行缓存的重建 此处使用分布式锁,只会有一个线程抢占到缓存重建所以不用使用线程池if (acquiredLock) {try {new Thread(() -> {// 直接重建缓存,不关心返回值rebuildCache(id);}).start();} finally {// 最后释放锁redissonLockService.releaseLock(lockKeyRedisson);}}// 开启多线程后直接返回旧的数据return ResponseResult.okResult(article);}}// 缓存中根本没有,那么需要直接加锁重建缓存,此时不能多线程的去重建缓存,只能通过分布式锁的方式,boolean lockAcquired = redissonLockService.acquireLock(lockKeyLogicalExpiration);if (lockAcquired){try {ApArticle apArticle = rebuildCache(id);if (Objects.isNull(apArticle)){// 数据不存在就直接返回return ResponseResult.errorResult(AppHttpCodeEnum.DATA_NOT_EXIST);}// 返回获得的文章数据return ResponseResult.okResult(apArticle);} finally {// 最后释放锁redissonLockService.releaseLock(lockKeyLogicalExpiration);}} else {// 没有获取到锁就等待一段时间然后再次尝试获取锁try {Thread.sleep(1000);}catch (Exception e){log.error(e.getMessage());}// 等待一段时间重新校验有没有缓存return selectArticleLogicalExpiration(id);}}public ApArticle rebuildCache(Long id){// 重建缓存ApArticle articleDatabase = getById(id);// 重建缓存if (Objects.nonNull(articleDatabase)) {// 设置逻辑过期时间// 转为jsonStringString articleJsonString = JSON.toJSONString(articleDatabase);// 转为JSONObjectJSONObject articleJsonObject = JSON.parseObject(articleJsonString);// 当前时间戳加上 设置的过期时间*1000 因为时间戳是毫秒articleJsonObject.put("expired", System.currentTimeMillis() + ARTICLE_EXPIRED * 1000);redisTemplate.opsForValue().set(ARTICLE_KEY + id, JSON.toJSONString(articleJsonObject));// 布隆过滤器过滤过的,这个肯定存在return articleDatabase;}return null;}