缓存数据一致性实践

Posted Alleria Windrunner

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了缓存数据一致性实践相关的知识,希望对你有一定的参考价值。

缓存作为互联网应用系统不可或缺的一部分,作为固态存储的“辅助”,本篇我们就来讨论一下缓存数据的一致性实践。


缓存的作用

一般来说缓存的作用无非就是以下两点:
  • 降低请求的响应时间,提升用户体验

  • 减少对固化存储的读压力


缓存使用场合

并不是所有的场合都适合使用缓存的,那么对于静态资源、较少更改资源、读多写少的场景适合使用缓存,而对于频繁更新、读少写多的场景就不适合使用缓存了。


缓存的类别

缓存可以分为:
  • 本地缓存

  • 分布式缓存


Google Guava Cache作为本地缓存的不二选择我相信并没有啥微词。本地缓存一般存储静态不变的数据,减少网络I/O的交互,例如:电商网站的分类类名。而分布式缓存一般则缓存相对静态的数据,例如,电商网站的商品信息、电商购买商品列表。由于缓存的数据量较大,单机无法存放。业界内分布式缓存一般有以下选择:
  • Memcached

  • Redis

    • Master-Slave

    • Redis Cluster

  • Codis

    • 分布式Redis解决方案

    • 豌豆荚开源

  • Twemproxy

    • Redis的一种代理分片机制

    • Twitter开源


Memcached作为一款古老的缓存产品早期还是撑起了缓存的一片天,其特点如下:
  • 分布式的缓存服务

  • 仅支持Key-Value

  • Value可以是字符串、也可以是Binary

  • Server不是分布式

  • 分布式支持靠客户端

  • 性能较高(W+吞吐量)

  • 不可持久化,仅仅能缓存

  • 使用简单


而Redis作为后起之秀也是目前业界内使用最广泛的缓存产品,其特点也是有很多的:
  • Key-Value

  • Key是字符串

  • Value可以是字符串、Set、Map、List等,支持类型丰富

  • Server不是分布式的

  • 分布式支持靠客户端

  • 性能较高(5W+吞吐量,官方号称10W+)

  • Master-Slave方式,具有高可靠性、高可用性

  • 具备持久化功能

    • RDB快照

    • AOF配置刷盘


那么我们如何选择缓存产品呢?Memcached已经为历史长河做出了巨大的贡献,所以Redis是首选,如果是单机容量范围内选择Redis Master-Slave模式即可,至于分布式则有两种方案:
  • Redis Cluster

  • Codis


那么,接下来我们就好好说一说Redis Cluster和Codis。


Redis Cluster

Redis Cluster是一个去中心化并且高可用的缓存集群,集群示意图如下:

但是Redis Cluster是Smart Client模式,即首先client必须按key的哈希将请求直接发送到对应的节点,mget的支持需要client先拆分定位到对应节点然后对返回结果进行合并。然后官方的cluster必须要等对应语言的Redis driver对cluster支持的开发和不断成熟。最后client不能直接像单机一样使用pipeline来提高效率,想同时执行多个请求来提速必须在client端自行实现异步逻辑。


CAP模型

在CAP模型这一块,Redis Cluster属于AP模型。数据分布上预分配了16384个slot槽,根据crc16(key)mod 16384的值,决定key存放在哪个slot里。


Redis Cluster Master-Slave模式

集群模式的话,Redis Cluster采用一主多从,client写数据到master,master告知client ok,master同步数据到slave。但是如果在master告知client ok之后,master crash会导致主从数据不一致。主节点负责存储键值对数据,而从节点则负责复制主节点。从节点不提供任何读写操作。


命令执行

命令的执行分为槽位正确与槽位不正确。当槽位正确的时候,命令处理的键所在的槽,正好由接收命令的节点负责,就直接执行命令。当槽位不正确的时候,接受命令的节点并不包含键所在的槽位,这个时候接受命令的节点会向客户端返回Redirection,客户端根据Redirection的指引,转向正确的节点发送命令,正确的节点接受到命令后执行。


Gossip协议通信

去中心化这一块Redis Cluster采用Gossip最终一致性算法实现。Gossip不要求节点知道所有的其他节点,具有去中心化的特点,节点之间完全对等,不需要任何的中心节点。Gossip算法可用于众多能接受“最终一致性”的领域:失败检测、路由同步、Pub/Sub、动态负载均衡,通过Gossip,每个节点都能知道集群中包含哪些节点,以及这些节点状态。


故障转移

如果集群中的节点发生故障,那么集群会自动从宕机主节点的所有从节点选中一个节点,被选中的从节点会执行SLAVEOF no one命令,成为新的主节点。新的主节点会撤销所有对宕机节点的槽指派,并将这些槽全部指派给自己。新的主节点还会向集群广播一条Gossip PONG消息,这条PONG消息可以让集群中的其他节点即知道这个节点已经由从节点变成了主节点,并且这个节点已经接管了原本由宕机节点负责处理的槽。新的主节点开始接受和自己负责处理的槽有关的命令请求,故障转移完成。


环境搭建

Redis Cluster环境搭建很简单,并且迁移slot也很简单,redis-cli可以直接连接上集群模式,所有命令天然支持。
关于Slot的设置问题,slot对Redis cluster是一个逻辑概念,不限制空间大小,如果所有的key值全落在某一个slot也是可以的,但是问题在于,slot一定是和某个单节点挂钩的,无法水平扩展,如果尝试使用mget跨节点,redis会报错“error CROSSLOT Keys in request don't hash to the same slot”。跨节点的mget对Redis十分不友好,目前Java世界里最流行的客户端Jedis不支持在client先计算再归并mget结果。
关于slot迁移过程中服务可用性的问题。slot迁移过程中,先动slot位置,所有新key和更新操作都会写到新的slot里,旧有节点,会检查key。如果没有会让客户端重定向到新节点去,旧有slot以原子性命令migrate慢慢迁移到新slot里。理论上来说,迁移过程始终可用。


Codis

由于原生的Redis Cluster的实现是Smart Client,所以豌豆荚开源了一个分布式的Redis解决方案Codis,对于上层的应用来说,连接到Codis Proxy和连接原生的Redis Server没有显著区别,上层应用可以像使用单机的Redis一样使用,Codis底层会处理请求的转发、不停机的数据迁移等工作。所有后边的一切事情,对于前面的客户端来说是透明的,可以简单的认为后边连接的是一个内存无限大的Redis服务。简单来说就是从Smart Client模式编程了Smart Server模式。


环境搭建

Codis的环境搭建稍显复杂,并且是一套,需要go、Zookeeper、proxy、Redis。


Slot概念

Codis也是切分slot,slotId = crc32(key)%1024。切分到1024个slot里。slot是先迁,再改集群状态,有一致性要求,先迁但是不能写。Codis支持水平扩容/缩容,扩容可以直接界面的"Auto Rebalance"按钮,缩容只需要将要下线的实例拥有的slot迁移到其它实例,然后在界面上删除下线的group 即可。


故障转移

Redis主从使用Redis的Sentinel,类似轮训监控,发现问题后,触发更改metadata,从而做主从的切换。


负载均衡

负载均衡通过Zookeeper实现,后端的proxy节点失败后会有通知,之后更新完成,保证客户端调用活的proxy节点。
总的来说,Codis的依赖比较多,但是相对完善,包括运维工具(自动rebalance slot),以及图形监控。推荐使用!


缓存高可用

关于缓存是否需要做高可用,一般有两种声音:
  • 高可用

  • 不高可用


我们分情况来讨论,首先是高可用,既然要做高可用那么肯定就是7*24小时,高可用的目的无非就是防止请求穿透到数据库,避免数据库冲击。那么缓存的冗余怎么做?无非就是一份数据写入到2份缓存同时两份缓存都提供读写服务。对于写操作来说要求2份缓存都写成功了才返回成功。对于读操作来说,先到其中一台,读到直接返回,读不到读另外一台,读到返回,若读不到,读数据库,并把读到数据回填到2个缓存中。
另一种情况就是不做高可用,当缓存不可用的时候,查询数据库。其实这个时候只要评估缓存宕机后对数据库造成的压力可控就行了,所以缓存高可用意义不大。


缓存数据一致性

最后来说一下缓存数据的一致性,至于为什么会产生数据不一致?也就是数据一致的根源本质上是因为存在多份数据存储,比如数据库、缓存。那么如何来解决这个问题呢?比如强一致性、最终一致性。但是这些都有具体的限制,对于强一致性来说,吞吐量会成为瓶颈,而最终一致性来说,对于数据敏感的金融行业来说业务又不满足。那么通过时序控制是否可行呢?比如先更新数据库,再更新缓存或者先更新缓存再更新数据库?其实都是不能保证的,因为只要不是原子操作都是无法绝对不出问题的。
所以对于缓存来说我们也只能是追求数据最终一致性。有更新操作时,更新数据库,同时delete缓存项,X秒后,再次delete缓存项,采取这种双重失效的方式尽可能的保证时效的成功率。除此之外还要设置缓存项Expired Time,最后的最后我们还要记录缓存失败的日志,脚本定期修正。
ok,关于缓存这一块就聊这些。

以上是关于缓存数据一致性实践的主要内容,如果未能解决你的问题,请参考以下文章

memcached 缓存数据库应用实践

java程序设计与实践教程第二版课后答案,成功入职阿里

如何保障缓存和数据库的一致性(超详细案例)

如何保障缓存和数据库的一致性(超详细案例)

如何保障缓存和数据库的一致性(超详细案例)

如何保障缓存和数据库的一致性(超详细案例)