前言
微笑挖坑, 努力填坑.
---- 已经拥有黑眼圈, 但还没学会小猪老师时间管理学的蛮三刀同学
我们来讨论秒杀系统中缓存热点数据的问题, 进一步延伸到数据库和缓存的双写一致性问题, 并且给出了实现代码.
本篇文章主要内容
缓存热点数据
为何要使用缓存
哪类数据适合缓存
缓存的利与弊
缓存和数据库双写一致性
不使用更新缓存而是删除缓存
先删除缓存, 还是先操作数据库?
我一定要数据库和缓存数据一致怎么办
实战: 先删除缓存, 再更新数据库
实战: 先更新数据库, 再删缓存
实战: 删除缓存重试机制
实战: 删除缓存重试机制
实战: 读取 binlog 异步删除缓存
- /**
- * 查询库存: 通过数据库查询库存
- * @param sid
- * @return
- */
- @RequestMapping("/getStockByDB/{sid}")
- @ResponseBody
- public String getStockByDB(@PathVariable int sid) {
- int count;
- try {
- count = stockService.getStockCountByDB(sid);
- } catch (Exception e) {
- LOGGER.error("查询库存失败:[{}]", e.getMessage());
- return "查询库存失败";
- }
- LOGGER.info("商品 Id: [{}] 剩余库存为: [{}]", sid, count);
- return String.format("商品 Id: %d 剩余库存为:%d", sid, count);
- }
- /**
- * 查询库存: 通过缓存查询库存
- * 缓存命中: 返回库存
- * 缓存未命中: 查询数据库写入缓存并返回
- * @param sid
- * @return
- */
- @RequestMapping("/getStockByCache/{sid}")
- @ResponseBody
- public String getStockByCache(@PathVariable int sid) {
- Integer count;
- try {
- count = stockService.getStockCountByCache(sid);
- if (count == null) {
- count = stockService.getStockCountByDB(sid);
- LOGGER.info("缓存未命中, 查询数据库, 并写入缓存");
- stockService.setStockCountToCache(sid, count);
- }
- } catch (Exception e) {
- LOGGER.error("查询库存失败:[{}]", e.getMessage());
- return "查询库存失败";
- }
- LOGGER.info("商品 Id: [{}] 剩余库存为: [{}]", sid, count);
- return String.format("商品 Id: %d 剩余库存为:%d", sid, count);
- }
- server.tomcat.max-threads=10000
- server.tomcat.max-connections=10000
- /**
- * 下单接口: 先删除缓存, 再更新数据库
- * @param sid
- * @return
- */
- @RequestMapping("/createOrderWithCacheV1/{sid}")
- @ResponseBody
- public String createOrderWithCacheV1(@PathVariable int sid) {
- int count = 0;
- try {
- // 删除库存缓存
- stockService.delStockCountCache(sid);
- // 完成扣库存下单事务
- orderService.createPessimisticOrder(sid);
- } catch (Exception e) {
- LOGGER.error("购买失败:[{}]", e.getMessage());
- return "购买失败, 库存不足";
- }
- LOGGER.info("购买成功, 剩余库存为: [{}]", count);
- return String.format("购买成功, 剩余库存为:%d", count);
- }
- @Override
- public void delStockCountCache(int id) {
- String hashKey = CacheKey.STOCK_COUNT.getKey() + "_" + id;
- stringRedisTemplate.delete(hashKey);
- LOGGER.info("删除商品 id:[{}] 缓存", id);
- }
- /**
- * 下单接口: 先更新数据库, 再删缓存
- * @param sid
- * @return
- */
- @RequestMapping("/createOrderWithCacheV2/{sid}")
- @ResponseBody
- public String createOrderWithCacheV2(@PathVariable int sid) {
- int count = 0;
- try {
- // 完成扣库存下单事务
- orderService.createPessimisticOrder(sid);
- // 删除库存缓存
- stockService.delStockCountCache(sid);
- } catch (Exception e) {
- LOGGER.error("购买失败:[{}]", e.getMessage());
- return "购买失败, 库存不足";
- }
- LOGGER.info("购买成功, 剩余库存为: [{}]", count);
- return String.format("购买成功, 剩余库存为:%d", count);
- }
- // 延时时间: 预估读数据库数据业务逻辑的耗时, 用来做缓存再删除
- private static final int DELAY_MILLSECONDS = 1000;
- /**
- * 下单接口: 先删除缓存, 再更新数据库, 缓存延时双删
- * @param sid
- * @return
- */
- @RequestMapping("/createOrderWithCacheV3/{sid}")
- @ResponseBody
- public String createOrderWithCacheV3(@PathVariable int sid) {
- int count;
- try {
- // 删除库存缓存
- stockService.delStockCountCache(sid);
- // 完成扣库存下单事务
- count = orderService.createPessimisticOrder(sid);
- // 延时指定时间后再次删除缓存
- cachedThreadPool.execute(new delCacheByThread(sid));
- } catch (Exception e) {
- LOGGER.error("购买失败:[{}]", e.getMessage());
- return "购买失败, 库存不足";
- }
- LOGGER.info("购买成功, 剩余库存为: [{}]", count);
- return String.format("购买成功, 剩余库存为:%d", count);
- }
- // 延时双删线程池
- private static ExecutorService cachedThreadPool = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS,new SynchronousQueue<Runnable>());
- /**
- * 缓存再删除线程
- */
- private class delCacheByThread implements Runnable {
- private int sid;
- public delCacheByThread(int sid) {
- this.sid = sid;
- }
- public void run() {
- try {
- LOGGER.info("异步执行缓存再删除, 商品 id:[{}], 首先休眠:[{}] 毫秒", sid, DELAY_MILLSECONDS);
- Thread.sleep(DELAY_MILLSECONDS);
- stockService.delStockCountCache(sid);
- LOGGER.info("再次删除商品 id:[{}] 缓存", sid);
- } catch (Exception e) {
- LOGGER.error("delCacheByThread 执行出错", e);
- }
- }
- }
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-amqp</artifactId>
- </dependency>
- @Configuration
- public class RabbitMqConfig {
- @Bean
- public Queue delCacheQueue() {
- return new Queue("delCache");
- }
- }
- @Component
- @RabbitListener(queues = "delCache")
- public class DelCacheReceiver {
- private static final Logger LOGGER = LoggerFactory.getLogger(DelCacheReceiver.class);
- @Autowired
- private StockService stockService;
- @RabbitHandler
- public void process(String message) {
- LOGGER.info("DelCacheReceiver 收到消息:" + message);
- LOGGER.info("DelCacheReceiver 开始删除缓存:" + message);
- stockService.delStockCountCache(Integer.parseInt(message));
- }
- }
- /**
- * 下单接口: 先更新数据库, 再删缓存, 删除缓存重试机制
- * @param sid
- * @return
- */
- @RequestMapping("/createOrderWithCacheV4/{sid}")
- @ResponseBody
- public String createOrderWithCacheV4(@PathVariable int sid) {
- int count;
- try {
- // 完成扣库存下单事务
- count = orderService.createPessimisticOrder(sid);
- // 删除库存缓存
- stockService.delStockCountCache(sid);
- // 延时指定时间后再次删除缓存
- // cachedThreadPool.execute(new delCacheByThread(sid));
- // 假设上述再次删除缓存没成功, 通知消息队列进行删除缓存
- sendDelCache(String.valueOf(sid));
- } catch (Exception e) {
- LOGGER.error("购买失败:[{}]", e.getMessage());
- return "购买失败, 库存不足";
- }
- LOGGER.info("购买成功, 剩余库存为: [{}]", count);
- return String.format("购买成功, 剩余库存为:%d", count);
- }
- https://www.jianshu.com/p/2936a5c65e6b
- https://www.cnblogs.com/rjzheng/p/9041659.html
- https://www.cnblogs.com/codeon/p/8287563.html
- https://www.jianshu.com/p/0275ecca2438
- https://www.jianshu.com/p/dc1e5091a0d8
- https://coolshell.cn/articles/17416.html
- GitHub:@qqxx6661 https://github.com/qqxx6661
- CSDN:@Rude3knife http://blog.csdn.net/qqxx6661
来源: https://www.cnblogs.com/rude3knife/p/13408102.html