共计 2225 个字符,预计需要花费 6 分钟才能阅读完成。
数据库与缓存双写问题
计算机领域任何一个问题都可以通过增加一个抽象“层”来解决。
业务中为了减少热点数据不必要的 db 查询,往往会增加一层缓存来解决 I / O 性能。可是 I / O 多了一层也就多了一层的更新维护与容错保障,当修改 db 中某些数据时,往往会面临缓存更新的问题,在这里简单介绍 数据库与缓存双写问题以及在业务场景如何使用双写策略。
缓存更新时机
缓存在以下情况下需要更新:
不存在缓存,回源至 db 后添加缓存
缓存超时,重复上个步骤
修改 db,更新缓存
缓存更新策略
若不存在缓存或者缓存超时:
查询 db
设置缓存
若缓存存在,且需要更新 db,则有多种缓存更新策略:
先更新 db,然后更新缓存
先删除缓存,然后更新 db
先更新 db,在删除缓存
本节主要讨论更新 db 时如何更新缓存的问题,且暂时不考虑缓存操作失败的情况(如网络原因、redis 服务不可用等)。
如果业务场景中不会出现修改相同数据字段竞争的问题,那么这三种更新策略毫无疑问都可以使用。如果出现缓存竞争态的情况,那么第一种策略是最先排除的:](https://si.geilicdn.com/viewm… 上图所示,如果 A、B 先后修改 db,会出现最终缓存与 db 不一致的现象,导致随后至缓存超时或下次更新的时间段内使用脏数据的现象。而且业务方需要考虑的是,是否每次更新 db,都需要立即刷新缓存。如果在“写频繁,而读频率远小于写的情况下,频繁的刷新缓存是否有必要?”
第二种策略,先删除缓存再更新数据库旨在牺牲性能下尽可能降低使用脏缓存的情况,可是此种情况下仍有可能出现脏缓存的情况:](https://si.geilicdn.com/viewm… 如上图,A 先删除缓存,同时开始更新 db;与此同时 B 查询缓存为空,进而查询 db,由于 db 的读性能高于写且数据库隔离级别默认为提交读,因此 B 查询 db 的数据往往为旧数据,此后 B 查询完毕更新缓存,导致缓存在超时时间或者下次修改 db 的范围内为脏数据。如果 db 底层做了读写分离的情况下,这种现象更容易出现,B 查询 db 是读库,而 A 修改主库后需要一定时间的同步才能保障从库的数据最新,因此在此种情况下,缓存肯定仍是脏数据。
为了避免这种情况,A 可以在更新 db 后延时一定间隔(往往是查询 db 时间 + 设置缓存的时间)删除缓存,尽量缩短脏缓存的时段,新的请求回源 db 并设置新的缓存数据。如下图所示。](https://si.geilicdn.com/viewm…
第三种策略先更新数据库再删除缓存,此种策略较为安全,几乎不会出现脏缓存的情况,就算出现也是会在极不合理的情况下导致脏缓存:](https://si.geilicdn.com/viewm… 如上图,缓存出现脏数据的前提是第 2 步骤耗时大于第 3、4 步骤,即读耗时大于写耗时,这几乎不可能发生。就算发生,也可以通过 A 再次延迟删除缓存(两次删除)解决。
缓存操作问题
在上一节中提到的所有缓存更新策略都是在暂时不考虑缓存操作失败的情况(如网络原因、redis 服务不可用等)前提下讨论的,如果缓存操作失败,则必须通过业务代码重试、消息队列或者设置缓存超时解决。
业务代码重试,设置合理的重试次数与间隔,如果超时后缓存仍然无法操作则需要等待缓存超时或者人为介入;
消息队列则在缓存操作失败后投递对应消息,在非业务代码中进行重试;
缓存超时则是兜底方案,这是允许最长的缓存不一致的时间。
分布式事务
比较遗憾的是,在 node 领域还没有类似 JAVA 的 JTA 规范及其实现,JTA 规范中的核心“事务管理器 TM”大都由容器来实现,如常见的 jboss 和 websphere;TM 接收业务层的事务请求,同时协同参与事务的各个资源管理器 RM 如 dbms、mq 等,实现分布式事务的提交与回滚;同时也提供分布式事务在不同自治系统的传递。
分布式事务的集中解决方案有如下几种:
1. 两阶段提交
2. 三阶段提交
3. 异步确保
4. TCC
在 JAVA 和其他生态已经证明了,两阶段提交的低效以及无法抗住高并发且存在单点的问题;三阶段提交虽然解决了两阶段的单点和减少协调者阻塞等待参与者的问题,但仍存在数据不一致的情况,因此这两种理论上的模型其实并不符合实际业务中的场景,在工程领域需要追求的是最优化,可见理论与现实仍然有不少差距。
那么在 node 场景中,处理分布式事务的方式也就只剩下两种工程上的解决方案。
node 中使用异步确保模型可以使用相比较简单的基于消息队列的异步确保模型(也可基于本地数据库表)。将分布式长事务切分为多个本地事务,通过保障本地事务的可靠性实现分布式长事务的最终提交。如果参与分布式事务的某个本地事务执行出错进行回滚,则通过消息队列实现业务主动方的补偿,实现最终的数据一致性。如下图:
TCC 模型相比较异步确保而言则比较重,需要开发一个 TCC 的 TM 协调各个服务参与方,同时对参与事务的各个从服务侵入性比较大,必须提供 try、confirm 和 cancel 三个接口。其中 try 接口预留相关资源,并确保数据一致性,confirm 接口和 cancel 接口保证幂等性,执行或回滚 try 阶段预留的资源。其中,在业务中主动调用所有参与分布式事务的从服务的 try 接口,并汇报给 TM 执行情况,由 TM 根据 try 阶段的结果完成后续的执行或回滚操作,同时记录分布式事务状态传递以及各个从服务的执行阶段等信息,便于追踪。
因此用 node 实现分布式事务时,在没有自研 TCC 中间件的前提下,可根据业务特性自行扩展异步确保型方案。