Post

分布式缓存实践

分布式缓存主要用于查询场景,缓解DB压力,针对不同的一致性要求分为两种缓存场景:

  • 最终一致性分布式缓存
  • 强一致分布式缓存

CAP中的C表示的是多副本一致性 ACID中的C表示的是动作一致性,比如A给B打钱

最终一致性分布式缓存

最终一致性对一致性要求不高,可以采用异步更新的方式避免业务系统阻塞。 采用旁路缓存模式,先去缓存获取数据,存在则直接返回,不存在去读DB,然后根据DB值设置缓存;同时,更新DB数据后需要主动删除缓存,避免脏数据。 更新DB数据后如何触发缓存删除动作呢?一般有两种方式:

  1. domain层发送领域事件触发
  2. 监听数据库数据变化(如binlog)触发

(这里使用直接更新方式,也可以通过mq异步更新缓存)

当然,两种方式可以同时存在:

缓存删除后在查缓存不存在,则会去DB取数据,而当某一时刻查询并发大时,则会导致缓存穿透,为避免该问题,可在读取DB数据设置缓存前获取分布式锁:

在该最终一致性场景下,如第一张图所示,更新DB和缓存删除操作并非原子操作,高查询并发或更新时会存在2种缓存脏数据的情况:

那么在强一致场景下,这种脏数据如何避免呢?

强一致分布式缓存

加锁

在强一致场景下,需要舍弃性能保证(锁的粒度较大)来保证数据一致性,两处lock指向同一个分布式锁

缓存删除失败怎么办

缓存失败的原因可能是缓存服务不可用、网络波动,其中网络波动可以通过重试补偿,而服务不可用时要及时告警且人员第一时间介入,同时,增加降级开关和熔断机制保证业务的可用性。这里只针对网络波动提供通用解决方案: 首先,缓存key需要添加超时时间,避免key长期存在,当然针对某些热点key可设置永不过期,网络波动场景下(或服务不可用重启后),数据库更新后的缓存删除动作必须保证成功,通过重试机制来保证“必须”,可以添加一个本地重试表(类似本地消息表)来记录缓存删除状态,后续可通过定时任务异步执行重试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
create table gaotu_express.express_retry
(
    id bigint auto_increment comment '自增主键' primary key,
    number bigint not null comment '雪花id',
    type tinyint not null comment 'biz_number对应类型 0-发货单,1-订单',
    key bigint not null comment '缓存key',
    retry_status tinyint  default 0 not null comment '重试状态,0-待重试,1-重试成功',
    retry_times tinyint  default 0 not null comment '重试次数',
    ext_info text null comment 'json格式额外信息',
    next_retry_time datetime not null comment '下一次执行重试时间',
    create_time datetime default CURRENT_TIMESTAMP not null comment '创建时间',
    update_time datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '修改时间',
    constraint unq_number unique (number)
)
    comment '物流重试表';

create index idx_update_time_status
    on gaotu_express.express_retry (next_retry_time, retry_status, type);

参考 : 携程分布式缓存实践:最终一致和强一致性通吃! 业界标杆:分布式缓存与DB秒级一致的灵活设计实践

This post is licensed under CC BY 4.0 by the author.