场景案例
实现某宝聚划算功能(热点访问功能),其实就是模仿一个页面的分页展示(每次只展示前5条),并且分页展示内容是在实时变化的一个功能。
这里我们使用 Redis 中 list 来模拟此功能,如下:
1、定义实体类:
@Setter
@Getter
@AllArgsConstructor
class Product implements Serializable {
private int id;
private String name;
}
2、定义查询接口:
@RequestMapping("/page")
public List<Product> queryByPage(int currentPage, int pageSize) {
List<Product> list = null;
int start = (currentPage - 1) * pageSize;
int end = start + pageSize - 1;
list = this.redisTemplate.opsForList().range("jhs", start, end);
if (CollectionUtils.isEmpty(list)) {
// TODO query db
}
log.info(">>>>>>>>查询结果:" + list);
return list;
}
3、定义后台刷新内容 Task:
@RequestMapping("/jhs")
public void testDemo() {
System.out.println(">>>>>>>启动计划算功能.....");
new Thread(() -> {
while (true) {
List<Product> list = products();
// 每次先删除缓存的值
redisTemplate.delete("jhs");
// 然后再重新把值存进到缓存
redisTemplate.opsForList().leftPushAll("jhs", list);
try {
TimeUnit.MINUTES.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
log.info(">>>>>>>定时刷新...................");
}
}).start();
}
private static List<Product> products() {
List<Product> list = new ArrayList<>();
for (int i = 0; i < 5; i++) {
Random random = new Random();
int id = random.nextInt(1000);
list.add(new Product(id, "product_name" + i));
}
return list;
}
但是这里有个问题,如果高并发场景下,由于
// 每次先删除缓存的值
redisTemplate.delete("jhs");
// 然后再重新把值存进到缓存
redisTemplate.opsForList().leftPushAll("jhs", list);
这两行代码不是原子性的,如果先删除,但是还没来得及在将数据存入到缓存中,导致缓存失效,那么就会导致大批量请求直接到 MySQL 数据上去,发生缓存击穿,如何优化呢?
可以在添加一个缓存,继续拦截一层,并且这两个换成的失效时间是不一样的(错开),查询和更新的缓存顺序相反,如果先查询 A 缓存,后查询 B 缓存,那么更新就是先更新 B 缓存,后更新 A 缓存。如下所示:
@Autowired
private RedisTemplate redisTemplate;
private static final String JHS_KEY_A ="jhs:a";
private static final String JHS_KEY_B ="jhs:b";
@PostConstruct
public void init() {
System.out.println(">>>>>>>启动计划算功能.....");
new Thread(() -> {
while (true) {
List<Product> list = products();
// 缓存 A、B
redisTemplate.delete(JHS_KEY_B);
redisTemplate.opsForList().leftPushAll(JHS_KEY_B, list);
redisTemplate.expire(JHS_KEY_B, 10, TimeUnit.SECONDS);
redisTemplate.delete(JHS_KEY_A);
redisTemplate.opsForList().leftPushAll(JHS_KEY_A, list);
redisTemplate.expire(JHS_KEY_A, 15, TimeUnit.SECONDS);
try {
TimeUnit.SECONDS.sleep(30);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
log.info(">>>>>>>定时刷新...................");
}
}).start();
}
@RequestMapping("/page2")
public List<Product> queryByPage(int currentPage, int pageSize) {
List<Product> list = null;
int start = (currentPage - 1) * pageSize;
int end = start + pageSize - 1;
list = this.redisTemplate.opsForList().range(JHS_KEY_A, start, end);
if (CollectionUtils.isEmpty(list)) {
list = this.redisTemplate.opsForList().range(JHS_KEY_B, start, end);
System.out.println(">>>>>>>>>>>>>>>>查询到缓存 B 中 list="+list);
if (CollectionUtils.isEmpty(list)) {
System.out.println(">>>>>>>>最后还是查询到了数据库中");
}
}
//log.info(">>>>>>>>查询结果:" + list);
return list;
}