分布式缓存和分布式事务

分布式缓存

缓存选型

Memcache

memcache 提供了简单的kv cache存储,value大小为1mb
memcache 使用slab方式来做内存管理,这种方式存在一定的浪费,如果大量接近的item,建议调整memcache参数来优化每一个slab增长的ratio,可以通过设置slab_automove 和slab_reassion 开启memcache的动态/手动move slab,防止某些slab热点导致内存不足的情况下引发LRU。
每个slab包含若干个大小为1M的内存页,这些内存又被分割成多个chunk,每个chunk存储一个item。memcache在启动初始化的时候,每个slab都会分配一个1M的内存页由slabs_preallocate 完成,chunk的增长因子由-f指定,默认是1.25,初始大小为48字节

Redis

redis 有丰富的数据类型,支持增量方式的修改部分数据,比如排行榜,集合,数组等
比较常用的方式是使用 redis 作为数据索引,比如评论的列表 ID,播放历史的列表 ID 集合,我们的关系链列表 ID。
因为redis是没有内存池的,所以是存在一定的内存碎片,一般会使用jemalloc来优化内存分配,需要编译使用jemalloc库来代替glib的malloc使用。

分布式缓存集群方案

当数据量逐步增加,需要搭建多个缓存实例来支撑业务,需要将数据进行分片缓存。

Proxy

通过Proxy的方式对缓存集群进行封装,当需要插入数据/查询数据,通过Proxy的方式对缓存集群进行访问。
技术方案

  • twemproxy
    开源的缓存代理框架,可以满足需求,但有如下缺点
    • 单进程模型和redis类似,在处理一些大Key的时候,可能出现IO瓶颈
    • 二次开发成本高
    • 不支持自动伸缩,不支持autorebalance,增删节点需要重启才能生效
    • 运维不友好,没有控制面板
  • 其他开源的代理工具
    • codis:只支持redis协议,且需要使用patch版本的redis
    • mcrouter:只支持memcache协议,C开发,同运维集成开发难度高

一致性Hash

一致性hash是将数据按照特征值映射到一个首尾相接的hash环上,同时将节点按照IP地址或是机器名hash,映射到这个环上。
对于数据,从数据在环上的位置开始,顺时针找到第一个节点就是数据测存储节点。
余数分布式算法由于保存建的服务器发生巨大变化二影响缓存的命中率。但Consistent Hashing中,只有在环上增加服务器的地点逆时针方向的第一台服务器上的键会收到影响。

特点
  • 平衡性:尽可能分布到所有的缓存
  • 单调性:单调性是指如果已经有一些内容通过哈希分派到了相应的缓冲中,又有新的缓冲区加入到系统中,那么哈希的结果应能够保证原有已分配的内容可以被映射到新的缓冲区中去,而不会被映射到旧的缓冲集合中的其他缓存区。
  • 分散性:相同的内容被存储到不同的缓冲中去,降低了系统存储的效率,降低分散性。
  • 负载:哈希算法应该能够尽量降低缓存的负荷。
  • 平滑性:缓存服务器的数目平滑改变和缓存对象的平滑改变是一致的。

Slot

redis-cluster把16384槽按照节点数量进行平均分配,由节点进行管理。对于每个key按照CRC16规则进行hash运算,把hash结果对16383进行取余,把余数发送给Redis节点。
Redis Cluster的节点之间会共享消息,每个节点都会知道哪个节点负责哪个范围内的数据槽。
当新增加节点的时候,只需要之前每个节点余处一部分槽给到新的节点即可,保证在新增加节点的时候,只有少部分数据会发生迁移。

缓存模式

数据一致性

Storage和Cache同步更新比较容易出现数据的不一致性

  • 同步操作DB
  • 同步操作Cache
  • 利用Job消费消息,来进行缓存写入
图片.png
不一致的原因

在Cache Aside模型中,读缓存Miss的回填操作和修改数据同步更新缓存,包括消息队列的异步补偿缓存,都无法满足"Happens Before", 会存在相互覆盖的情况

图片.png
解决方案

读写同时操作

  1. 读操作,读缓冲,缓存miss
  2. 读操作,读DB,读到数据
  3. 写操作,更新DB数据
  4. 读操作,ADD操作数据回写缓存(可以异步JOB操作,Redis可以使用SETEX操作)
  5. 写操作使用SET操作命令,覆盖写缓存,读操作,使用ADD操作回写MISS数据,从而保证写操作最新的数据不会被读操作的回写数据覆盖。

多级缓存

微服务拆分细粒度原子业务下的整合服务(聚合服务),用以提供粗粒度的接口,以及二级缓存加速,减少扇出的rpc的网络请求,减少延迟。
因此在过程中,最重要的是要保证多级缓存的数据一致性

  • 清理优先级:优先清理下游缓存
  • 下游缓存的过期时间要大于上游,里面穿透回源

热点缓存

对于热点缓存key,按照如下思路处理

  • 小表广播:从RemoteCache 升级为LocalCache 定时更新,
  • 主动监控防御预热
  • 支持热点发现
  • 多Key设计:使用多个副本,减少节点热点问题
  • 建立多cluster和微服务,存储一起组成一个Region
    • 同一个key在每一个fronted cluter 都可能有一个copy,这样会有性能问题,但是这样可以有效提高稳定性,利用Mysql的binlog消息anycast到不同集群的某个节点清理或是更新缓存
    • 如果应用程序可以忍受稍微过期的数据,针对这点可以进一步降低系统负载。当一个key 被删除的时候(delete 请求或者 cache 爆棚清空间了),它被放倒一个临时的数据结构里,会再续上比较短的一段时间。当有请求进来的时候会返回这个数据并标记为“Stale”。对于大部分应用场景而言,Stale Value 是可以忍受的

穿透缓存

对于缓存穿透,可以参考下面的的方式进行处理

  • Singlefly:对关键字进行一致性hash,使其某个维度的key一定命中某个节点,然后在节点内使用互斥锁,保证归并回源,但是对与批量查询不行
  • 分布式锁:设置一个lock key,有且只有一个人成功,并且返回,交由这个人来执行回源操作,其他候选者轮训cache这个lock key ,如果不存在去读数据缓存,hit就返回,miss继续抢锁
  • 队列:如果cache miss 由队列聚合一个key,来load数据回缓存,对于miss当前请求可以使用singlefly保证回源
  • Lease:lease 是 64-bit 的 token,与客户端请求的 key 绑定,对于过时设置,在写入时验证 lease,可以解决这个问题;对于 thundering herd,每个key 10s 分配一次,当 client 在没有获取到 lease 时,可以稍微等一下再访问 cache,这时往往cache 中已有数据。(基础库支持 & 修改 cache 源码);

缓存技巧

Incast Congestion

如果网络中包太多,会发生Incast Congestion的问题,可以理解为,network上有很多swtich,router等,一旦一次性发一堆包,这些包会同时到达switch,这些switch会无法处理这些数据,针对这样的场景可以通过下面几个点进行考虑

  • 客户端实现发送队列,限制每次发出去的包的数量,具体这个值是多少,需要根据实际业务场景去调整,如果这个值太小,发送太慢,会导致延迟较高,如果太大则会导致network switch等崩溃,造成丢包的场景。

其他小技巧

  • 易读性前提下,key的设置尽可能的小,减少资源占用,redis value.可以用int就不要用string, 对于小于N的value, redis 内部有 shared_object 缓存
  • 拆分 key。主要是用在 redis 使用 hashes 情况下。同一个 hashes key 会落到同一个 redis 节点,hashes 过大的情况下会导致内存及请求分布的不均匀。考虑对 hash 进行拆分为小的hash,使得节点内存均匀及避免单节点请求热点。
  • 空缓存设置,可能数据库始终为空,这时应该设置空缓存,避免每次请求都缓存 miss 直接打到 DB。
  • 读失败后写缓存策略
  • 序列化使用protobuf,尽可能减少size

memcache 小技巧

  • flag使用:标识compress,encoding,large value等
  • memcache 支持 gets,尽可能的pipeline, 减少网络开销
  • 使用二进制协议,支持pipeline delete, UDP读取,TCP更新

redis小技巧

  • 增量更新一致性:EXPIRE ZADD/HEST 保证索引结构体务必存在的情况下去操作数据
  • BITSET: 存储每日登陆用户,单个标记位置(boolean),为了避免单个 BITSET 过大或者热点,需要使用 region sharding,比如按照mid求余 %和/ 10000,商为 KEY、余数作为offset;
  • LIst:用于类似Stack PUSH/POP操作
  • Sortedset:排序有序集合,
  • Hashs:过小的时候会使用压缩列表、过大的情况容易导致 rehash 内存浪费
  • String:SET 的 EX/NX 等 KV 扩展指令,SETNX 可以用于分布式锁、SETEX 聚合了SET + EXPIRE;
  • Sets:类似hashes 无value
  • 尽可能的 PIPELINE 指令,但是避免集合过大

分布式事务

事务在单一的数据库上,是可以保证ACID,但是在多服务相互调用的时候,如果保证数据的一致性。

事务消息

如何可靠的保存消息凭证。
要解决消息的可靠存储,实际上需要解决的问题是,本地MySQL存储和message存储的一致性问题。

  1. Transactinal outbox
  2. Polling publisher
  3. Transaction log tailing
  4. 2PC Message Queue

事务消息一旦被可靠持久化,我们整个分布式事务,变为了最终的一致性。消息的消费才能保证整体业务的完整性,所以需要尽最大努力把消息送达到下游的业务消费方,只有消息被消费,整个交易才能算是完成。

Transactinal outbox

Transactinal outbox, 服务A在完成服务同时,同时记录消息数据,这个消息数据同业务数据保存在同一个数据库实例

Begin Transaction
 update a set amount= amount-1000 where user_id = 1;
 insert into msg(user_id,amount,status) values (1,1000,1);
 end transaction
commit

上面的事务保证只要支付宝被扣钱了钱,消息一定会被保存下来,当上述事务提交成功后,会通知下游服务,当下游服务完成的时候,则会删除该条消息。

Polling publisher

Polling publisher,我们可以定时轮询msg表,把status =1的消息通通拿出来消费,可以按照自增ID排序,保证顺序消费。在这里我们可以独立出一个task服务,把拖出来消息publish给消息队列,Balance服务自己来消费队列,或者直接rpc 发送给balance服务

Transaction log tailing

Transaction log tailing,上述保存消息的方式使得消息数据和业务数据紧耦合在一起,从架构上看并不优雅,容易诱发其他问题。
在一些业务场景中,可以通过主表被canal订阅的方式来进行处理。使用canal订阅后,是实时流式消费数据,在消费者balance或者balance-job必须努力送达到。

幂等

在分布式事务中,需要重点处理的是消息重复投递的问题,如果相同的消息被投递了两次,那么会增加数据重复的问题,可以考虑下面两个方式

  • 全局唯一ID + 去重表
  • 版本号

2PC Message Queue

两阶段提交协议(Two Phase Commitment Protocol

  • 一个事务协调者(coordinator):负责协调多个参与者进行事务投票及提交(回滚)
  • 多个事务参与者(participants):即本地事务执行者
    总共处理步骤有两个
    (1)投票阶段(voting phase):协调者将通知事务参与者准备提交或取消事务,然后进入表决过程。参与者将告知协调者自己的决策:同意(事务参与者本地事务执行成功,但未提交)或取消(本地事务执行故障);
    (2)提交阶段(commit phase):收到参与者的通知后,协调者再向参与者发出通知,根据反馈情况决定各参与者是否要提交还是回滚
    在消息的发送中,加入rocketMQ用以生产者和消费者模型,通过双向同 rocketMQ 来保证分布式事务一致性。

Seata 2PC

  • 架构层面:传统的2PC方案的RM实际上是在数据库层,RM本质上就是数据库自身,通过XA协议实现,而Seata的RM是以jar包的形式作为中间层部署在应用程序的一册
  • 两阶段提交:传统2PC无论是第二阶段的决议是commit还是rollback,事务性资源的锁事要保持到Phase2完成才能释放,而Seata的做法是在Phase1就将本地事务提交,减少Phase2持有锁的时间,提高整体效率。
图片.png

TCC

TCC 是 Try、Confirm、Cancel 三个词语的缩写,TCC 要求每个分支事务实现三个操作:预处理 Try、确认 Confirm、撤销 Cancel。

  • Try 操作做业务检查及资源预留,Confirm 做业务确认操作,Cancel 实现一个与 Try 相反的操作即回滚操作。
  • 首先发起所有的分支事务的 Try 操作,任何一个分支事务的 Try 操作执行失败,TM 将会发起所有分支事务的 Cancel 操作,若 Try 操作全部成功,TM 将会发起所有分支事务的 Confirm 操作,其中 Confirm/Cancel 操作若执行失败,会进行重试。

你可能感兴趣的:(分布式缓存和分布式事务)