开课吧开课吧锤锤2021-02-22 15:13
80、90后是这个时代最焦虑的一代人,大部分人都是,上有老,下有小,有房贷,车贷,卡贷。身上的大山很重,很多人的工资是不足以负担着这些的,所以想要给家人,给自己一个好的生活,就要从事一份薪资高的工作,在众多工种中,程序员的工资最高,很多人都在向往着这份工作,但是普通的程序员,也不会有非常可观的薪资,只有那种技术尖端人才,才可以,事业名利双丰收,那怎么样才能做到行业的顶尖呢?,那就呀不断的提升自己,今天就为大家带来分析数据库与缓存的双写问题,希望对您攀登事业顶峰有所帮助。
缓存和数据库一致性实战
实战:先删除缓存,再更新数据库
终于到了实战,我们在秒杀项目的代码上增加接口:先删除缓存,再更新数据库
OrderController中新增:
/**
* 下单接口:先删除缓存,再更新数据库
* @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);
}
stockService中新增:
@Override
public void delStockCountCache(int id) {
String hashKey = CacheKey.STOCK_COUNT.getKey() + "_" + id;
stringRedisTemplate.delete(hashKey);
LOGGER.info("删除商品id:[{}] 缓存", id);
}
其他涉及的代码都在之前三篇文章中有介绍,并且可以直接去Github拿到项目源码,就不在这里重复贴了。
实战:先更新数据库,再删缓存
如果是先更新数据库,再删缓存,那么代码只是在业务顺序上颠倒了一下,这里就只贴OrderController中新增:
/**
* 下单接口:先更新数据库,再删缓存
* @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);
}
实战:缓存延时双删
如何做延时双删呢,最好的方法是开设一个线程池,在线程中删除key,而不是使用Thread.sleep进行等待,这样会阻塞用户的请求。
更新前先删除缓存,然后更新数据,再延时删除缓存。
OrderController中新增接口:
// 延时时间:预估读数据库数据业务逻辑的耗时,用来做缓存再删除
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);
}
OrderController中新增线程池:
// 延时双删线程池
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);
}
}
}
来试验一下,请求接口createOrderWithCacheV3:
日志中,做到了两次删除:
实战:删除缓存重试机制
上文提到了,要解决删除失败的问题,需要用到消息队列,进行删除操作的重试。这里我们为了达到效果,接入了RabbitMq,并且需要在接口中写发送消息,并且需要消费者常驻来消费消息。Spring整合RabbitMq还是比较简单的,我把简单的整合代码也贴出来。
pom.xml新增RabbitMq的依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
写一个RabbitMqConfig:
@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));
}
}
OrderController中新增接口:
/**
* 下单接口:先更新数据库,再删缓存,删除缓存重试机制
* @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);
}
访问createOrderWithCacheV4:
可以看到,我们先完成了下单,然后删除了缓存,并且假设延迟删除缓存失败了,发送给消息队列重试的消息,消息队列收到消息后再去删除缓存。
实战:读取binlog异步删除缓存
我们需要用到阿里开源的canal来读取binlog进行缓存的异步删除。
扩展阅读
更新缓存的的DesignPattern有四种:
Cacheaside
Readthrough
Writethrough
Writebehindcaching,
小结
引用陈浩《缓存更新的套路》最后的总结语作为小结:
分布式系统里要么通过2PC或是Paxos协议保证一致性,要么就是拼命的降低并发时脏数据的概率
缓存系统适用的场景就是非强一致性的场景,所以它属于CAP中的AP,BASE理论。
异构数据库本来就没办法强一致,只是尽可能减少时间窗口,达到最终一致性。
还有别忘了设置过期时间,这是个兜底方案
结束语
本文总结并探讨了缓存数据库双写一致性问题。
文章内容大致可以总结为如下几点:
对于读多写少的数据,请使用缓存。
为了保持数据库和缓存的一致性,会导致系统吞吐量的下降。
为了保持数据库和缓存的一致性,会导致业务代码逻辑复杂。
缓存做不到绝对一致性,但可以做到最终一致性。
对于需要保证缓存数据库数据一致的情况,请尽量考虑对一致性到底有多高要求,选定合适的方案,避免过度设计。
以上内容由开课吧老师Java中文社群提供,更多Java教程尽在开课吧广场Java教程频道。更多免费课程可以关注公众号“码农集散地”

最新文章

如何在大规模 Kubernetes 集群上实现高 SLO?
Pod创建成功率:这是一个非常重要的指标,蚂蚁集团一周的Pod创建量在百万级别,如果成功率波动会造成大量Pod失败,同时Pod成功率下跌也是集群异常的最直观反映;
2021-03-05 16:20:04

MySQL不建议delete删除数据的原因是什么?(二)
MySQL内部不会真正删除空间,而且做标记删除,即将delflag:N修改为delflag:Y,commit之后会会被purge进入删除链表,如果下一次insert更大的记录,delete之后的空间不会被重用,如果插入的记录小于等于delete的记录空会被重用,这块内容可以通过知数堂的innbloc
2021-03-05 11:45:44

MySQL不建议delete删除数据的原因是什么?(一)
物理上主要由系统用户数据文件,日志文件组成,数据文件主要存储MySQL字典数据和用户数据,日志文件记录的是datapage的变更记录,用于MySQLCrash时的恢复。
2021-03-05 11:36:22

Java教程:MySQL如何设计索引更高效?(五)
同时也介绍了如何更好做MySQL索引设计,包括前缀索引,复合索引的顺序问题以及MySQL8.0推出的索引跳跃扫描,我们都知道,索引可以加快数据的检索,减少IO开销,会占用磁盘空间,是一种用空间换时间的优化手段,同时更新操作会导致索引频繁的合并分裂,影响索引性能,在实际的业务开发中,如何根据业务场景去
2021-03-05 11:29:12

Java教程:MySQL如何设计索引更高效?(四)
在单列索引不能很好的过滤数据的时候,可以结合where条件中其他字段来创建复合索引,更好的去过滤数据,减少IO的扫描次数,举个例子:业务需要按照时间段来查询交易记录,有如下的SQL:
2021-03-05 11:25:02