目录
一、缓存预热
二、缓存雪崩
三、缓存穿透
3.1、解决方案
3.1.1、空对象缓存或者缺省值
3.1.2、Goolge布隆过滤器Guava解决缓存穿透
四、缓存击穿
4.1、危害
4.2、解决方案
4.3、模拟百亿补贴活动案例
一、缓存预热
场景:MySQL有N条新记录,redis没有
1、MySQL做数据新增 ,redis利用回写机制,让它逐步实现100条新增记录的同步,部署发布版本的时候,自己人提前做一次redis同步。
2、通过中间件或者程序自行完成。
3、使用白名单
二、缓存雪崩
场景:
1、redis主机挂掉,redis全盘崩溃,偏硬件运维。
2、redis中有大量key同时过期大面积失效,偏软件开发。
预防和解决:
1、redis中key设置为永不过期或者过期时间错开
2、redis缓存集群实现高可用:①、主从+哨兵。②、redis集群。③、开启redis持久化机制aof/rdb,尽快恢复缓存集群。
3、多缓存结合预防雪崩:ehcache本地缓存+redis缓存。
4、服务降级:Hystrix或者阿里sentinel限流和降级
5、用钱购买阿里云---》云数据库redis。
三、缓存穿透
查询一条或N条数据,redis中没有,mysql中也没有,但请求每次都会去查询数据库,导致后台数据库压力暴增,就是缓存穿透。
3.1、解决方案
3.1.1、空对象缓存或者缺省值
黑客或恶意攻击:
黑客会对系统进行攻击,拿一个不存在的id去查询数据,会产生大量的请求到数据库查询。可能会导致数据库压力大而宕掉。
key相同攻击系统:
第一次访问到MySQL,空对象缓存后第二次就返回defaultNull缺省值,避免MySQL被攻击,不用到数据库中去走一圈了。
key不同攻击系统:
由于存在空对象缓存和缓存回写,redis中的垃圾key会越写越多(设置key过期时间)。
3.1.2、Goolge布隆过滤器Guava解决缓存穿透
白名单过滤器:
1、误判问题,概率小可以接受,不能从布隆过滤器中删除
2、全部合法的key都需要放入Guava版布隆过滤器+redis里面,不然数据就是返回null
代码实现:
改pom
<!--google开源guava-->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>23.0</version>
</dependency>
测试案例:
/**
* 创建Guava布隆过滤器测试
*/
@Test
public void testGuavaAndBloomFilter(){
//创建Guava布隆过滤器
BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), 100);
//判断指定的元素是否存在
System.out.println(bloomFilter.mightContain(1));
System.out.println(bloomFilter.mightContain(2));
System.out.println("加入后");
//将元素添加进布隆过滤器
bloomFilter.put(1);
bloomFilter.put(2);
System.out.println(bloomFilter.mightContain(1));
System.out.println(bloomFilter.mightContain(2));
}
使用百万数据测试:
@Service
@Slf4j
public class GuavaBloomFilterServiceImpl implements GuavaBloomFilterService {
public static final Integer _1W = 10000;
//定义guava布隆过滤器,初始容量
public static final Integer SIZE = 100 * _1W;
//误判率,误判率越小误判个数越少
public static double fpp = 0.03;
//创建guava布隆过滤器
private static BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), SIZE,fpp);
@Override
public void guavaBloomFilter() {
//在guava布隆过滤器加入一百万白名单数据
for (int i = 0; i < SIZE; i++) {
bloomFilter.put(i);
}
//取十万个不在合法范围的数据测试
ArrayList<Integer> list = new ArrayList<>(10 * _1W);
//测试
for (int i = SIZE + 1; i < SIZE+(10 * _1W) ; i++) {
//判断在guava布隆过滤器是否存在
if (bloomFilter.mightContain(i)){
log.info("被误判:{}",i);
list.add(i);
}
}
log.info("误判总数为:{}",list.size());
}
}
@RestController
@Api(tags = "Google的guavaBloomFilter")
@Slf4j
public class GuavaBloomFilterController {
@Autowired
private GuavaBloomFilterService guavaBloomFilterService;
@RequestMapping(value = "/guavaBloomFilter",method = RequestMethod.GET)
@ApiOperation("guava布隆过滤器插入100万样本数据和10万测试数据")
public void guavaBloomFilter(){
guavaBloomFilterService.guavaBloomFilter();
}
}
结论 :100000 / 3033 = 0.03033
源码分析:
四、缓存击穿
大量的请求同时查询一个key时,而这个热点key正好失效,就会导致大量的请求都打到数据库上面去。
4.1、危害
会造成某一刻数据库请求量过大,压力剧增。
4.2、解决方案
热点key失效的原因:
1、时间到了自然被清除但还是被访问到
2、删除的key,刚好被访问。
方案1:
差异失效时间,对于访问频繁的热点key,不设置过期时间。
方案2:
互斥更新,采用双检加锁策略。
4.3、模拟百亿补贴活动案例
@Data
@AllArgsConstructor
@NoArgsConstructor
@ApiModel(value = "百亿补贴活动模拟")
public class Product {
//商品id
private Long id;
//商品名
private String name;
//商品价格
private Integer price;
//商品描述
private String detail;
}
@Service
@Slf4j
public class BYBTTaskServiceImpl implements BYBTTaskService {
public static final String BYBT_KEY = "bybt";
public static final String BYBT_KEY_A = "bybt:a";
public static final String BYBT_KEY_B = "bybt:b";
@Autowired
private RedisTemplate redisTemplate;
/**
* 模拟MySQL添加商品
* @return
*/
public List<Product> getProductFromById(){
List<Product> list = new ArrayList<>();
for (int i = 1; i <= 20; i++) {
Random random = new Random();
int id = random.nextInt(1000);
Product product = new Product((long) id, "product" + i, i, "鸽子蛋" + i);
list.add(product);
}
return list;
}
// @PostConstruct
public void initBYBT(){
log.info("启动定时器模拟百亿补贴活动开始。。。");
//使用线程模拟定时任务,后台任务定时将MySQL里的活动商品刷新到redis中
new Thread(()->{
while (true){
//从MySQL中查询数据,写入redis
List<Product> list = this.getProductFromById();
//删除过期key
redisTemplate.delete(BYBT_KEY);
//使用redis的list数据结果存储最新数据
redisTemplate.opsForList().leftPushAll(BYBT_KEY,list);
//暂停2分钟模拟,模拟百亿补贴参加活动商品
try {
TimeUnit.MINUTES.sleep(2);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
},"a1").start();
}
@PostConstruct
public void initBYBTAB(){
log.info("启动AB定时器模拟百亿补贴活动开始。。。" + DateUtil.now());
//使用线程模拟定时任务,后台任务定时将MySQL里的活动商品刷新到redis中
new Thread(()->{
while (true){
//从MySQL中查询数据,写入redis
List<Product> list = this.getProductFromById();
//先更新缓存B,让缓存B过期时间超过缓存A,缓存A突然失效,还有缓存B,以防止缓存击穿
redisTemplate.delete(BYBT_KEY_B);
redisTemplate.opsForList().leftPushAll(BYBT_KEY_B,list);
//设置过期时间
redisTemplate.expire(BYBT_KEY_B,86410L,TimeUnit.SECONDS);
//更新缓存A
redisTemplate.delete(BYBT_KEY_A);
redisTemplate.opsForList().leftPushAll(BYBT_KEY_A,list);
//设置过期时间
redisTemplate.expire(BYBT_KEY_A,86400L,TimeUnit.SECONDS);
//暂停2分钟模拟,模拟百亿补贴参加活动商品
try {
TimeUnit.MINUTES.sleep(2);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
},"a1").start();
}
}
@RestController
@Slf4j
@Api(tags = "百亿补贴活动模拟")
public class BYBTTaskController {
public static final String BYBT_KEY = "bybt";
public static final String BYBT_KEY_A = "bybt:a";
public static final String BYBT_KEY_B = "bybt:b";
@Autowired
private RedisTemplate redisTemplate;
/**
* 分页查询,查询redis
* @return
*/
@RequestMapping(value = "/findPage",method = RequestMethod.GET)
@ApiOperation("查询商品,每次1页每页5条显示")
public List<Product> findPage(int page,int size){
List<Product> list =null;
long start = (page - 1) * size;
long end = start + size - 1;
try {
//使用redis的list里的lrange查询分页
list = redisTemplate.opsForList().range(BYBT_KEY,start,end);
if (CollectionUtils.isEmpty(list)){
//查询为空就去mysql中查询
return null;
}
log.info("参加活动商品:{}",list);
} catch (Exception e) {
//出现异常,一般redis出现事故
log.error("bybt异常:{}",e);
}
return list;
}
@RequestMapping(value = "/findPageAB",method = RequestMethod.GET)
@ApiOperation("双缓存查询商品,每次1页每页5条显示")
public List<Product> findPageAB(int page,int size){
List<Product> list =null;
long start = (page - 1) * size;
long end = start + size - 1;
try {
//先去缓存A中查找
list = redisTemplate.opsForList().range(BYBT_KEY_A,start,end);
if (CollectionUtils.isEmpty(list)){
log.info("缓存A已失效,暂时采用缓存B");
//若缓存A没有就去缓存B中查找
list = redisTemplate.opsForList().range(BYBT_KEY_B,start,end);
if (CollectionUtils.isEmpty(list)){
//TODO 去MySQL查找
return null;
}
}
log.info("参加活动商品:{}",list);
} catch (Exception e) {
//出现异常,一般redis出现事故
log.error("bybt异常:{}",e);
e.printStackTrace();
}
return list;
}
}