专栏名称: 石杉的架构笔记
专注原创、用心雕琢!十余年BAT一线大厂架构经验倾囊相授
目录
相关文章推荐
江西省邮政管理局  ·  江西卫视:快递进村 打通物流“最后一公里” ·  2 天前  
中央网信办举报中心  ·  中央网信办发布2025年“清朗”系列专项行动 ... ·  2 天前  
中央网信办举报中心  ·  中央网信办发布2025年“清朗”系列专项行动 ... ·  2 天前  
申妈的妹子圈  ·  阿里第三季度营收2801亿,超市场预期 ·  3 天前  
申妈的妹子圈  ·  阿里第三季度营收2801亿,超市场预期 ·  3 天前  
青岛新闻网  ·  本科毕业6年半!他已任985高校博导 ·  3 天前  
青岛新闻网  ·  本科毕业6年半!他已任985高校博导 ·  3 天前  
51好读  ›  专栏  ›  石杉的架构笔记

亿级流量高并发场景下,如何保证缓存与数据库的双写一致性?

石杉的架构笔记  · 公众号  ·  · 2020-11-11 08:30

正文


我的新课 《C2C 电商系统微服务架构120天实战训练营》 在公众号 儒猿技术窝 上线了,感兴趣的同学,可以长按扫描下方二维码了解课程详情:

课程大纲请参见文末


本文来自中华石杉架构班学员魏武归心的投稿

感谢魏武归心同学的技术分享


相信只要是个稍微像样点的互联网公司,或多或少都有自己的一套缓存体系。 只要用缓存,就可能会涉及到缓存与数据库双存储双写,你只要是双写,就一定会有数据一致性的问题,遂笔者想在这想和大家聊一聊: 如何解决一致性问题?


如何保证缓存与数据库双写一致性,也是现在Java面试中面试官非常喜欢问的一个问题!


一般来说,如果允许缓存可以稍微跟数据库偶尔有不一致,也就是说如果你的系统不是严格要求 缓存 + 数据库 必须保持一致性的话,最好不要做这个方案。


即:读请求和写请求串行化,串到一个内存队列里去,从而达到防止并发请求导致数据错乱的问题,场景如图所示:

值得注意的是, 串行化可以保证一定不会出现不一致的情况,但是它也会导致系统的吞吐量大幅度降低,用比正常情况下多几倍的机器去支撑线上的一个请求(土豪请自觉无视此提醒)


解决思路如下图:

代码实现大致如下:


  1. /**

  2. * 请求异步处理的service实现

  3. * @author Administrator

  4. *

  5. */

  6. @Service("requestAsyncProcessService")

  7. publicclassRequestAsyncProcessServiceImplimplementsRequestAsyncProcessService {


  8. @Override

  9. publicvoid process(Request request) {

  10. try {

  11. // 先做读请求的去重

  12. RequestQueue requestQueue = RequestQueue.getInstance();

  13. Map<Integer, Boolean> flagMap = requestQueue.getFlagMap();


  14. if(request instanceofProductInventoryDBUpdateRequest) {

  15. // 如果是一个更新数据库的请求,那么就将那个productId对应的标识设置为true

  16. flagMap.put(request.getProductId(), true);

  17. } elseif(request instanceofProductInventoryCacheRefreshRequest) {

  18. Boolean flag = flagMap.get(request.getProductId());


  19. // 如果flag是null

  20. if(flag == null) {

  21. flagMap.put(request.getProductId(), false);

  22. }


  23. // 如果是缓存刷新的请求,那么就判断,如果标识不为空,而且是true,就说明之前有一个这个商品的数据库更新请求

  24. if(flag != null && flag) {

  25. flagMap.put(request.getProductId(), false);

  26. }


  27. // 如果是缓存刷新的请求,而且发现标识不为空,但是标识是false

  28. // 说明前面已经有一个数据库更新请求+一个缓存刷新请求了,大家想一想

  29. if(flag != null && !flag) {

  30. // 对于这种读请求,直接就过滤掉,不要放到后面的内存队列里面去了

  31. return;

  32. }

  33. }


  34. // 做请求的路由,根据每个请求的商品id,路由到对应的内存队列中去

  35. ArrayBlockingQueue<Request> queue = getRoutingQueue(request.getProductId());

  36. // 将请求放入对应的队列中,完成路由操作

  37. queue.put(request);

  38. } catch (Exception e) {

  39. e.printStackTrace();

  40. }

  41. }


  42. /**

  43. * 获取路由到的内存队列

  44. * @param productId 商品id

  45. * @return 内存队列

  46. */

  47. privateArrayBlockingQueue<Request> getRoutingQueue(Integer productId) {

  48. RequestQueue requestQueue = RequestQueue.getInstance();


  49. // 先获取productId的hash值

  50. String key = String.valueOf(productId);

  51. int h;

  52. int hash = (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);


  53. // 对hash值取模,将hash值路由到指定的内存队列中,比如内存队列大小8

  54. // 用内存队列的数量对hash值取模之后,结果一定是在0~7之间

  55. // 所以任何一个商品id都会被固定路由到同样的一个内存队列中去的

  56. int index = (requestQueue.queueSize() - 1) & hash;


  57. System.out.println("===========日志===========: 路由内存队列,商品id=" + productId + ", 队列索引=" + index);


  58. return requestQueue.getQueue(index);

  59. }


  60. }



Cache Aside Pattern

下面我们来聊聊最经典的缓存+数据库读写的模式,就是 Cache Aside Pattern。


读的时候,先读缓存,缓存没有的话,就读数据库,然后取出数据后放入缓存,同时返回响应。 更新的时候,先更新数据库,然后再删除缓存。


为什么是删除缓存,而不是更新缓存? 原因很简单,很多时候,在复杂点的缓存场景,缓存不单单是数据库中直接取出来的值。


比如可能更新了某个表的一个字段,然后其对应的缓存,是需要查询另外两个表的数据并进行运算,才能计算出缓存最新的值的。


另外更新缓存的代价有时候是很高的,是不是每次修改数据库的时候,都一定要将其对应的缓存更新一份?


也许有的场景是这样,但是对于比较复杂的缓存数据计算的场景,就不是这样了。


如果你频繁修改一个缓存涉及的多个表,缓存也频繁更新。但是问题在于,这个缓存到底会不会被频繁访问到?


举个栗子,一个缓存涉及的表的字段,在 1 分钟内就修改了 20 次,或者是 100 次,那么缓存更新 20 次、100 次;


但是这个缓存在 1 分钟内只被读取了 1 次,有大量的冷数据。


实际上,如果你只是删除缓存的话,那么在 1 分钟内,这个缓存不过就重新计算一次而已,开销大幅度降低,用到缓存才去算缓存。


其实删除缓存,而不是更新缓存,就是一个 lazy 计算 的思想,不要每次都重新做复杂的计算,不管它会不会用到,而是让它到需要被使用的时候再重新计算。


像 mybatis,hibernate,都有懒加载思想,查询一个部门,部门带了一个员工的 list,没有必要说每次查询部门,都把里面的 1000 个员工的数据也同时查出来。


80% 的情况,查这个部门,就只是要访问这个部门的信息就可以了,先查部门,同时要访问里面的员工,那么这时只有在你要访问里面的员工的时候,才会去数据库里面查询 1000 个员工。


最初级的缓存不一致问题及解决方案

问题:先修改数据库,再删除缓存。如果删除缓存失败了,那么会导致数据库中是新数据,缓存中是旧数据,数据就出现了不一致。


解决思路 :先删除缓存,再修改数据库。如果数据库修改失败了,那么数据库中是旧数据,缓存中是空的,那么数据不会不一致。


因为读的时候缓存没有,则读数据库中旧数据,然后更新到缓存中。


比较复杂的数据不一致问题分析

数据发生了变更,先删除了缓存,然后要去修改数据库。 但是还没来得及修改,一个请求过来,去读缓存,发现缓存空了,去查询数据库,查到了修改前的旧数据,放到了缓存中。 随后数据变更的程序完成了数据库的修改。


完了,数据库和缓存中的数据不一样了。。。


为什么上亿流量高并发场景下,缓存会出现这个问题?


只有在对一个数据在并发的进行读写的时候,才可能会出现这种问题。 如果说你的并发量很低的话,特别是读并发很低,每天访问量就 1 万次,那么很少的情况下,会出现刚才描述的那种不一致的场景。


但是问题是,如果每天的是上亿的流量,每秒并发读是几万,每秒只要有数据更新的请求,就可能会出现上述的数据库+缓存不一致的情况。


解决方案如下:







请到「今天看啥」查看全文