走进缓存的世界(二) - 缓存设计
系列文章
如何设计缓存
主要考虑三个问题:
- 缓存哪些数据
- 如何缓存
- 如何保证数据一致性
缓存哪些数据
系统优化时有一句话必须切记:“优化无止境”,所以如果缓存不是必须的,请果断去掉,要知道越是业务上复杂的系统,对Cache的使用反而越简单,因为对于一个复杂、多变、历史悠久的系统,在Cache方面做过度设计会让人深陷其中;缓存的数据越多,系统的维护成本就越高,所以找准需要缓存的点尤为重要。一般情况下,我们只会缓存给系统带来巨大瓶颈的IO操作,在普通应用里尤其指由top SQL或者慢 SQL所带来的DAO查询;找准需要优化的sql,你可以找DBA帮忙。
如何缓存
存储介质的选择: 你可以直接缓存在JVM内存里,也可以采用阿里云专门的缓存服务器,如tair、memcache等;
DB、文件其实也可以做缓存,他们一般缓存复杂计算的中间结果,一般很少用到;如果你的缓存是存放在jvm本地,那么通常是用map实现,如果缓存数据更新比较频繁且对数据正确性比较高,那么你需要考虑为其添加并发控制和失效策略。还有一点比较重要的就是,在集群环境下想要做到数据一致性比较困难,主动更新比较麻烦而且达不到其降低数据库IO操作的效果,所以本地缓存适用场景一般是在读访问非常高,而写操作极少,对数据一致性要求不是特别高的场景;如果采用专门的缓存服务器则避免了很多麻烦,阿里云的缓存系统tair,是我们经常使用的缓存中间件,它提供了很好的并发控制和失效机制,另外还提供了不同存储引擎可以供我们选择,如mdb,rdb,ldb;普通的缓存可以选择mdb和rdb,两者分别有memcache和redis的影子,其响应时间和高QPS的表现都非常好,但没有提供持久化,如果要确保数据不丢失可以采用ldb引擎存储,它提供了对数据持久化的支持,相反牺牲了一点点性能。
数据一致性
缓存意味着同样的数据可能有多份并存,如果你的代码没有考虑某种情况导致了两份数据不一致就会有问题发生。解决方法很简单,把你的业务逻辑、代码触发情况都考虑清楚,不要遗留没有触底的地方。
多处使用缓存会导致你的代码逻辑变得异常复杂,这也是为何说在非必要的时候,建议你不要用缓存的原因。
缓存一致性协议就是为了解决数据一致性问题而发明的。缓存一致性协议有多种,大多数计算机设备使用的都属于“窥探(snooping)”协议。
“窥探”的基本思想是,内存是共享资源,所有内存I/O传输都发生在一条共享的总线上,所有的处理器都能看到这条总线,所有处理器对内存的访问请求都要经过仲裁(arbitrate):同一个指令周期中,只有一个处理器可以读写内存中的被缓存的数据。窥探协议的思想是,缓存不仅仅在做内存传输的时候才和总线打交道,而是不停地在窥探总线上发生的数据交换,跟踪其他缓存在做什么。所以当一个缓存代表它所属的处理器去读写内存时,其他处理器都会得到通知,以此来使自己的缓存保持同步。只要某个处理器执行写操作,其他处理器马上就知道这块内存在它们自己的缓存中对应的段已经失效。
在直写模式下,这是很直接的,因为写操作一旦发生,它的效果马上会被“公布”出去。但是如果混着回写模式就有问题了。因为有可能在写指令执行过后很久,数据才会被真正回写到物理内存中。在这段时间内,其他处理器的缓存可能会去写同一块内存地址导致冲突。在回写模型中,简单把内存写操作的信息广播给其他处理器是不够的,我们需要做的是,在修改本地缓存之前,就要告知其他处理器。
搞懂了细节,就找到了处理回写模式这个问题的最简单方案。当处理器想写某个缓存段时,如果它没有独占权,它必须先发送一条“我要独占权”的请求给总线,这会通知其他处理器,把它们拥有的同一缓存段的拷贝失效(如果它们有的话)。只有在获得独占权后,处理器才能开始修改数据——并且此时,这个处理器知道,这个缓存段只有一份拷贝,在我自己的缓存里,这样一来就可以巧妙地避免了冲突。