预热雪崩穿透击穿
缓存预热
缓存雪崩
有这两种原因
- redis key 永不过期or过期时间错开
- redis 缓存集群实现高可用
- 主从哨兵
- Redis Cluster
- 开启redis持久化aof,rdb,尽快恢复集群
- 多缓存结合预防雪崩:本地缓存 ehcache + redis 缓存
- 服务降级:Hystrix 或者 sentinel 限流降级
- 人民币玩家:阿里云给了你多少广告?笑
缓存穿透
恶意请求不存在的数据
- guava BloomFilter
- 误判问题,但是概率小可以接受,不能从布隆过滤器删除 -> 布隆过滤器可能会错误地判断某个元素存在于集合中(称为误报),但不会错误地判断一个存在的元素不存在
- 全部合法的key都需要放入 Guava布隆过滤器+redis里面,不然数据就是返回null
案例
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>31.1-jre</version>
</dependency>
@Service
@Slf4j
public class GuavaBloomFilterService {
// 1.定义一个常量
public static final int _1W = 10000;
// 2.定义我们guava布隆过滤器,初始容量
public static final int SIZE = 100 * _1W;
// 3.误判率,它越小误判的个数也越少(思考:是否可以无限小? 没有误判岂不是更好)
public static double fpp = 0.0000000000003; // 这个数越小所用的hash函数越多,bitmap占用的位越多 默认的就是0.03,5个hash函数 0.01,7个函数
// 4.创建guava布隆过滤器
private static BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), SIZE, fpp);
public void guavaBloomFilter() {
// 1. 往 bloomFilter 中添加数据
for (int i = 0; i < SIZE; i++) {
bloomFilter.put(i);
}
// 2. 故意取10w个不在范围内的数据进行测试,来进行误判率演示
List<Integer> list = new ArrayList<>(10 * _1W);
// 3. 验证
for (int i = SIZE; i < SIZE + (10 * _1W); i++) {
if (bloomFilter.mightContain(i)) {
// log.info("被误判了:{}", i);
list.add(i);
}
}
log.info("误判总数量:{}", list.size());
log.info("误判率:{}", list.size() / (10 * _1W));
}
}
@SpringBootTest
public class GuavaTest {
@Resource
GuavaBloomFilterService guavaBloomFilterService;
/**
* guava版本布隆过滤器,helloworld 入门级演示
*/
@Test
public void testGuavaWithBloomFilter() {
System.out.println("testGuavaWithBloomFilter");
// 1. 创建 guava版布隆过滤器
BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), 100);
//2. 判断指定的元素是否存在
System.out.println(bloomFilter.mightContain(1));
System.out.println(bloomFilter.mightContain(2));
// 2. 添加数据
bloomFilter.put(1);
bloomFilter.put(2);
System.out.println(bloomFilter.mightContain(1));
System.out.println(bloomFilter.mightContain(2));
}
@Test
public void testGuavaWithBloomFilter2() {
guavaBloomFilterService.guavaBloomFilter();
}
}
fpp 默认 0.03
fpp要求越高,bit位数越多,hash函数越多
guava 黑名单使用
缓存击穿
对比穿透和击穿
互斥更新->对于更新的方法
聚划算案例
功能分析
数据结构使用 list
代码
@ApiModel(value = "聚划算活动product信息")
@AllArgsConstructor
@NoArgsConstructor
@Data
public class Product {
// 产品id
private Long id;
// 产品名称
private String name;
// 产品价格
private Integer price;
// 产品详情
private String detail;
}
@Service
@Slf4j
public class JHSTaskService {
private static final String JHS_KEY = "jhs";
private static final String JHS_KEY_A = "jhs:a";
private static final String JHS_KEY_B = "jhs:b";
@Autowired
RedisTemplate redisTemplate;
/**
* 模拟从数据库读取20件特价商品
* @return 商品列表
*/
private List<Product> getProductsFromMysql() {
List<Product> list = new ArrayList<>();
for (int i = 0; i < 20; i++) {
Random random = new Random();
int id = random.nextInt(1000);
Product product = new Product((long) id, "product" + i, i, "detail");
list.add(product);
}
log.info("模拟从数据库读取20件特价商品完成{}", list);
return list;
}
@PostConstruct
public void initJHSAB() {
log.info("启动AB的定时器 天猫聚划算模拟开始===========");
new Thread(() -> {
while (true) {
// 2.模拟从mysql查到数据,加到 redis 并返回给页面
List<Product> list = getProductsFromMysql();
redisTemplate.delete(JHS_KEY);
redisTemplate.opsForList().leftPushAll(JHS_KEY, list);
redisTemplate.expire(JHS_KEY, 86410L, TimeUnit.SECONDS);
// 5.暂停一分钟,间隔1分钟执行一次,模拟聚划算一天执行的参加活动的品牌
try {
Thread.sleep(1000* 60);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "t1").start();
}
}
测试类
@SpringBootTest
@Slf4j
public class JhsTest {
private static final String JHS_KEY = "jhs";
private static final String JHS_KEY_A = "jhs:a";
private static final String JHS_KEY_B = "jhs:b";
@Autowired
private RedisTemplate redisTemplate;
@Test
public void find() {
int page = 1;
int size = 10;
List<Product> list = null;
long start = (page - 1) * size;
long end = start + size - 1;
try {
list = redisTemplate.opsForList().range(JHS_KEY, start, end);
if (CollectionUtils.isEmpty(list)) {
// TODO 走 mysql 查询
}
log.info("参加活动的商家={}", list);
} catch (Exception e) {
// 出异常了,一般 redis 宕机了或者redis网络抖动导致timeout
log.error("jhs exception{}", e);
e.printStackTrace();
// ..... 重试机制 再次查询 mysql
}
log.info(list.toString());
}
}
测试方法,先跑主启动类(后台更新聚划算商品信息),然后手动执行测试类测试查询
问题分析
delete 执行间隙,这一瞬间缓存击穿,打到mysql
解决
@PostConstruct
public void initJHSAB() {
log.info("启动AB的定时器 天猫聚划算模拟开始===========");
new Thread(() -> {
while (true) {
// 2.模拟从mysql查到数据,加到 redis 并返回给页面
List<Product> list = getProductsFromMysql();
// redisTemplate.delete(JHS_KEY);
// redisTemplate.opsForList().leftPushAll(JHS_KEY, list);
// redisTemplate.expire(JHS_KEY, 86410L, TimeUnit.SECONDS);
// 3.先更新B缓存并且让B缓存过期时间超过A时间,如果A突然失效了还有B兜底,防止击穿
redisTemplate.delete(JHS_KEY_B);
redisTemplate.opsForList().leftPushAll(JHS_KEY_B, list);
redisTemplate.expire(JHS_KEY_B, 86410L, TimeUnit.SECONDS);
// 4.再更新A缓存
redisTemplate.delete(JHS_KEY_A);
redisTemplate.opsForList().leftPushAll(JHS_KEY_A, list);
redisTemplate.expire(JHS_KEY_A, 86400L, TimeUnit.SECONDS);
// 5.暂停一分钟,间隔1分钟执行一次,模拟聚划算一天执行的参加活动的品牌
try {
Thread.sleep(1000* 60);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "t1").start();
}
@Test
public void findAB() {
int page = 1;
int size = 10;
List<Product> list = null;
long start = (page - 1) * size;
long end = start + size - 1;
try {
list = redisTemplate.opsForList().range(JHS_KEY_A, start, end);
if (CollectionUtils.isEmpty(list)) {
log.info("---------A缓存已经过期或活动结束了,记得人工修补,B缓存继续顶着");
// A 没有来找 B
list = redisTemplate.opsForList().range(JHS_KEY_B, start, end);
if (CollectionUtils.isEmpty(list)) {
// TODO 走 mysql 查询
}
}
log.info("参加活动的商家={}", list);
} catch (Exception e) {
// 出异常了,一般 redis 宕机了或者redis网络抖动导致timeout
log.error("jhs exception{}", e);
e.printStackTrace();
// ..... 重试机制 再次查询 mysql
}
log.info(list.toString());
}