Java互联网架构-高并发缓存架构你那些年遇到的踩坑故事
今天讲的这个话题,我相信是众多工程师和团队的痛。从我刚开始工作,那时候构建本地缓存,到后续memcache, Redis的出现,到现在各种分布式集群的缓存,例如redis Cluster等产品的出现,缓存越来越发达和复杂了,缓存对我们的系统也越发重要,现在很难相信一个后端服务里没有缓存的存在。在这篇文章里,我会和大家分享一下过去踩到的缓存踩坑故事,然后试图给出一些解决方案,大家可以一起讨论,最终拿出更好的方法。由于篇幅有限,所以这里的缓存讨论,只局限于后端服务的缓存,并且不涉及具体的框架。
从两个角度看
一、客户端:
html缓存:
<meta http-equiv="Cache-Control"content="no-cache"/> 手机页面通常在第一次加载后会进行缓存,然后每次刷新会使用缓存而不是去重新向服务器发送请求。 如果不希望使用缓存可以设置no-cache。
<meta http-equiv="pragma" content="no-cache"><meta http-equiv="cache-control" content="no-cache"><meta http-equiv="expires" content="0">
ajax缓存:
cache:
要求为Boolean类型的参数,默认为true(当dataType为script时,默认为false),设置为false将不会从浏览器缓存中加载请求信息
二、服务端
服务端肯定是有缓存的,涉及CDN,redis。
案例1,缓存和DB的同步更新不在同一个事务里并且没有重试补偿机制
为了减少系统间的依赖,不同系统的数据更新往往不放在同一个事务里,采用MQ来进行通信。大家可以看下图,后台系统CRM更新产品数据到DB,Product系统收到异步消息通知后,更新最新数据到缓存。这是一个最常见的缓存应用场景,我相信很多团队都是这样用的。在这个Case里容易出现的问题在于,如果批处理任务收到消息后服务crash掉了,缓存没有正常更新,就出现了与DB的数据不同步,前端系统一直不能读到最新数据,导致业务异常。
解决方案:1. 失败消息一定要建立一定时间间隔的重试机制
2. 系统要有缓存更新的报警机制,方便更新失败或者重试超时后,可以人工介入进行补偿。
案例2, 同一数据被1个以上的服务执行写操作,其中一个服务的缓存数据没有版本控制
这也是两个不同服务更新数据过程中很常见的情况,见下图,CRM系统更新了某个用户的Profile, 保存更新数据库后,通过MQ通知用户系统更新缓存,由于是异步更新延迟,在缓存更新前,用户系统收到前端的指令,读取了当前缓存里的用户数据,做了修改,并更新到DB中。出现的结果就是数据库里的CRM的更新被错误覆盖。
解决方案:缓存里的数据有一个标志位可以作为更新数据库数据的依据(Update_time or Version), 如果缓存里数据时间与数据库时间不能匹配,意味着另外一个服务更新了该数据,那么就先从DB里读取最新数据版本,然后在新版本上提交数据。
案例3, 并发查询缓存中同一数据,如果缓存没命中,导致DB瞬时被打爆
做促销活动的时候,存在大量用户的并发访问某一个特定商品,该商品数据缓存失效,或者做了数据更改,但是对应缓存还没有更新,那么所有这些访问将同时直接被作用到DB上。
解决方案:做一个计数器或者锁(没有特别复杂逻辑的话,可以直接用HashMap),如果发现某个KEY缓存没有命中,那么在计数器+1, 然后访问数据库,拿到结果更新缓存,清理掉计数器中的key。 在这个过程中,如果有第二个线程或者更多的线程需要访问这个KEY时,发现计数器的值>1 或者被加锁, 那么wait, 直到计数器清理掉,当然,这个技术器阈值是可以在配置文件里配置的,不一定是1。
案例4, 缓存没有设置默认值,被攻击,缓存一直保持在被“穿透”状态
这个情况,和案例3比较类似,都是缓存无法命中,但不一样的地方在于,数据的KEY值是无法控制的,所以没法简单的用计数器和锁来处理, 比方,被人为攻击,制造的大量的无效userID访问。
解决方案:所有没有在缓存的KEY,全部分配一个默认VALUE “UNKOWN-KEY” ,具体是什么情况下,将默认值分配给没有命中的KEY, 这个可以根据自己的业务系统来定,比方说,可以根据特定的IP段,或者没有命中的总次数等,然后我们就可以决定是否继续访问DB还是直接返回默认值给前端,拒绝本次数据访问。这种做法的核心在于,每次数据访问,都会有缓存结果返回,根据系统的情况来决定是否要进一步访问DB。
总结,今天列举的这几个案例,归纳起来,可以总结为以下几点:
1. 保证缓存同步
2. 减少缓存并发
3. 杜绝缓存穿透
缓存与背后的DB是相互依存的关系,缓存系统的设计原则,就是将访问的异常处理或者压力尽可能的前置处理掉,将DB还原成它最初本来的存储功能。
总结
到这里,高并发缓存架构你那些年遇到的踩坑故事就结束了,,不足之处还望大家多多包涵!!觉得收获的话可以点个关注收藏转发一波喔,谢谢大佬们支持。(吹一波,233~~)
下面和大家交流几点编程的经验:
1、多写多敲代码,好的代码与扎实的基础知识一定是实践出来的
2丶 测试、测试再测试,如果你不彻底测试自己的代码,那恐怕你开发的就不只是代码,可能还会声名狼藉。
3丶 简化编程,加快速度,代码风骚,在你完成编码后,应回头并且优化它。从长远来看,这里或那里一些的改进,会让后来的支持人员更加轻松。
最后,每一位读到这里的网友,感谢你们能耐心地看完。希望在成为一名更优秀的Java程序员的道路上,我们可以一起学习、一起进步。
内部交流群617912068 欢迎各位前来交流和分享, )