什么是缓存击穿:
缓存击穿是指在高并发环境下,某个热点数据的缓存过期,导致大量请求同时访问后端存储系统,引起系统性能下降和后端存储压力过大的现象。
解决方案:
1. redisson分布式锁
本质上是缓存重建的过程中,大量的请求访问到后端的数据库导致数据库压力过大
那么可以使用redisson分布式锁来对缓存重建的过程加锁
其它的线程只有缓存重建完毕之后才可以访问
缺点:所有的请求都要等待拿到锁的线程来进行缓存重建
优点:数据拥有高一致性,适用于某些涉及"钱"的业务,或者要求数据的强一致性的。
- 新建redisson子工程单独作为微服务名字叫redisson-starter
- 引入redisson相关依赖
java
<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需要的配置项,如果是集群此处不能用这种方式
java
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,并且加入了一些自动装配的配置
java
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 {
@Autowired
private 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:
java
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.yonchao.redisson.service.RedissonLockService
业务类:
java
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 {
@Autowired
private 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秒接着递归这个方法,直到有一个线程重建缓存成功。
java
/**
* 使用逻辑过期的方式
* @param id
* @return
*/
@Override
public 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)) {
// 设置逻辑过期时间
// 转为jsonString
String articleJsonString = JSON.toJSONString(articleDatabase);
// 转为JSONObject
JSONObject 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;
}