Local Cache & Distributed Cache

总结下缓存更新思路。

缓存

缓存是提高系统性能的关键,也必然会带来数据一致性的问题。

常见的缓存大概有以下几种,

客户端(浏览器)缓存

使用用户本地缓存来减少服务端的请求压力,会造成客户端与服务端数据不一致的问题。

客户端缓存可以有效的降低服务端的压力,例如,抢购以及电商大促时常常先把一些静态数据缓存到客户端本地。

cdn缓存

利用分布式的边缘服务器对服务端的数据进行缓存,客户端就近获取数据

cdn缓存可以理解为利用中间件(分布式缓存)对服务端整体缓存,同样会造成数据不一致的问题。

cdn缓存一般用于静态数据,例如,前端相关的css、js,商品详情页的静态数据。

nginx缓存

nginx缓存一般仅用于服务端的静态数据的缓存,一般常用于前端相关的css/js等。

本地缓存

本地缓存是解决高并发的终极手段,但一定会存在数据不一致的问题。

大量的本地缓存会占用大量的业务内存,合理的设置LRU以及热点数据的评估是使用本地缓存的关键问题。

为了实现高并发,往往会采用数据的最终一致性。

分布式缓存

分布式缓存是目前微服务下最常用的缓存手段。

在一些QPS不高的业务下,并不需要本地缓存,仅仅使用分布式缓存就能实现很好的性能。

由于分布式缓存引入了新的中间件,会增加整个链路的网络IO开销。

常见的分布式缓存有Redis、Memcached。

缓存更新

缓存更新的方式大体可以分为两种:同步更新异步更新

由于更新的异步化,写入数据的同时不能保证数据的缓存刷新,因此,很难保证数据的强一致性。

同步更新大多适用于数据强一致性,而异步更新大多适用于数据的最终一致性。


以下几种实例中,均使用Memcached作为分布式缓存,MySQL作为持久化存储。

基于分布式缓存的同步更新

此方案是最基础的分布式缓存方案,关键在于同步的DB写入与缓存刷新。

read

优先读取缓存,如果不存在,则回源到DB,重新加载到缓存。

write

优先写DB,写成功后同步清理缓存。

基于分布式缓存的同步更新

基于分布式缓存的线程池异步更新

read

同上,优先读取缓存,如果不存在,则回源到DB,重新加载到缓存。

write

仅写DB,但写入成功后会直接返回,触发异步线程来做缓存的更新,存在缓存的不一致问题。

基于分布式缓存的线程池异步更新

如图所示,与同步更新相比,异步线程不能保证数据更新的时效性,如果线程出现异常会导致长时间的缓存不一致的问题。

此外,如何设置合理的异步线程池也是一个关键问题。

基于分布式缓存的消息异步更新

read

同上,优先读取缓存,如果不存在,则回源到DB,重新加载到缓存。

write

仅写DB,但写入成功后会直接返回,使用mq作为异步操作持久化的媒介,从而解决异步线程池的问题,但仍然无法解决性能上的问题。

基于分布式缓存的消息异步更新

如图所示,使用RocketMQ中间件来作为异步化的媒介,通过Consumer来触发缓存的刷新。

基于分布式缓存的binlog异步更新

read

同上,优先读取缓存,如果不存在,则回源到DB,重新加载到缓存。

write

仅写DB,但写入成功后会直接返回,使用binlog来避免对于mq中间件的依赖,订阅binlog日志来实现缓存的生成。

基于分布式缓存的binlog异步更新

这种方案引入了binlog日志,从复杂度上降低了直接的依赖,但也引入了间接的依赖。

Instagram才用了这种方式来实现高性能的服务,具体可参考Scaling Instagram Infrastructure

基于本地缓存的binlog异步更新

read

优先读本地缓存,如果不存在,则回源到DB,重新加载到本地缓存。

此方案不需要两次网络IO(分布式缓存的Request与Response),性能上一定是更好的,但一致性很难保证。

write

优先写DB,写入成功并更新本地缓存后会直接返回。

为了解决分布式场景下本地缓存的同步更新,引入了Redis的发布订阅机制。

此方案仅更新了本地缓存,如果可以保证该用户请求均路由到同一机器,是可以解决当前用户可见性问题。

基于本地缓存的binlog异步更新1

考虑到中间件的可靠性与时延问题,增加了SyncScheduler来实现定时同步机制。

基于本地缓存的binlog异步更新2

基于分布式缓存的异步持久化

read

仅读取缓存,缓存数据的加载是通过缓存中间件来实现,服务不会直接与DB交互。

write

仅写缓存,缓存需要持久化策略,通过binlog通过到MySQL。

此方案使用分布式缓存作为数据库的前置缓存,通过实现缓存的持久化机制来同步数据到DB。

此方案需要具有缓存中间件定制化开发的能力,缓存异常恢复也是实现高可用服务的关键。

基于分布式缓存的异步持久化

基于本地缓存的分治缓存策略

read

需要路由的支持,实现用户的特定路由,从而达到某个用户的所有操作均打到同一台机器上。

由于每台机器仅接受特定用户的数据,可以通过引入Zookeeper来获取一定的标识来预加载特定的数据。

仅读取本地缓存,通过SyncSheduler模块主动或定时拉取缓存数据。

write

仅写本地缓存,需要同步写入到数据库,从而保证数据的持久化。

此方案是基于路由能力,实现一套特定路由方案的缓存一致性策略。

通过保证用户请求路由的一致性,使用服务内存缓存特定数据,从而利用分布式服务来代替分布式缓存,从而满足数据的一致性。

基于本地缓存的分治缓存策略

这种方案没有具体实施过,特定的路由规则必然带来额外的复杂度。

这种方案在大型网站的库存系统中是否有过实现也未可知。

总结

缓存更新是分布式场景下无法避免的问题,大体可以分为同步异步

高性能与数据一致性往往不可兼得,强一致性与最终一致性之间如何取舍是架构设计的必经之路。

项目设计之初并不需要复杂的缓存设计,在满足性能的前提下架构的设计越简单越好。

如果DB的性能足以满足当前的需求,就不必引入分布式缓存。

如果分布式缓存的性能足以满足当前的需求,就不必引入本地缓存。

如果同步同步更新的性能满足当前的需求,就不必引入异步更新策略。

如果对数据的实时性要求不高,就没必要一定要同步调用,可以通过各种异步策略来实现。

如果线程池的方式满足当前的需求,就没必要引入为的中间件。

总之,缓存设计简单就好。

参考

基于 Canal 的 MySql RabbitMQ Redis/memcached/mongodb 的nosql同步