数据库、缓存数据一致性的问题一直是生产中一直存在的,有一些程序直接忽略这个问题,有些自己编写程序去处理。但是还是存在一下问题:
- 先更新数据库,更新缓存
- 先删除缓存,在更新数据库
- 先更新数据库,在删除缓存
一、中间件Canal
其实在工作中不论遇到什么问题,尽量要想到是否有成熟的中间件可以处理,这要比自己造轮子强很多,毕竟经过大部分人验证没问题的中间件是很香的。
1、canal介绍
canal是阿里开发并开源的用于Mysql增量日志数据的订阅、消费和解析的。是要原理是模拟一个Mysql的备机去监听binlog,当Mysql数据发生变化的时候消息回同步给canal组件。可以做:
- 数据库的镜像
- 数据库的实时备份
- 索引构建和实时为何(拆分异构索引、倒排索引等)
- 业务cache刷新
- 带业务逻辑的增量数据处理
官网:https://github.com/alibaba/canalhttps://github.com/alibaba/canal
2、canal工作原理
mysql的主从复制步骤:
- 当master数据发生改变,会写入二进制文件
- salve会在一定时间内对master上的二进制文件进行检测,如果发生改变则开启一个线程请求master的二进制事件日志
- master为每个请求线程启动一个dump线程,向其发送二进制事件日志
- slave将接受到的二机制事件日志保存到本地中继日志文件中,并启动线程读取在本地重放,保证和master数据一直
- 各个线程都会进入休眠状态,等待下次同步
canal模拟的就是Mysql slave的交互协议,把自己伪装成一个slave,向master发送dump协议,master将binlog推送给canal。
3、canal的安装
1、配置mysql
-
当前的 canal 支持源端 MySQL 版本包括 5.1.x , 5.5.x , 5.6.x , 5.7.x , 8.0.x。
-
查看mysql是否开启binlog,因为canal就是监听binlog的,所以必须开启
-
log-bin=mysql-bin #开启 binlog
-
binlog-format=ROW #选择 ROW 模式
-
ROW:记录每个字段的变化,空间占用多
-
STATEMENT:只记录执行的sql语句
-
MIX模式:表结构改变使用STATEMENT模式,行数据改变使用ROW
-
-
server_id=1 #配置MySQL replaction需要定义,不要和canal的 slaveId重复
-
-
创建canal授权账号
2、配置canal服务端
在官网上下载相关工具:
- canal.admin-1.1.5.tar.gz:用于canal软件的管理,配置Server和Instance
- canal.deployer-1.1.5.tar.gz:用于mysql数据同步
- canal.adapter-1.1.5.tar.gz:canal.deployer的增强版,更多的数据源
这里感谢 Canal高可用架构部署 - 简书,写的很全面
3、canal客户端编写
二、缓存双写一致性讨论
1、一致性的理解
- 缓存中有数据:和DB值相同
- 缓存中无数据:DB中值要最新的
2、缓存分类
- 只读缓存
- 读写缓存:想要保证缓存和数据库一致,需要采用同步直写策略
- 同步直写策略:写缓存的时候同时写数据库(或者写数据库的时候同时写缓存),缓存和数据库中的数据一致
缓存和数据库的数据同步和异步,需要根据数据的类型,热点数据、实时数据都不需要同步写的,当数据的实时性没有那么高的,就可以异步。但是如果当同步直写失败的时候,就需要使用异步写入作为补偿方案。
3、数据库和缓存一致性的集中更新策略
不论什么方式,最终目的就是期望缓存和数据库数据一致性。
- 先更新数据库,更新缓存
- 可能存在数据不一致性,例如:数据库更新完成,缓存更新失败,最大的问题是可能读取到旧数据
- 先删除缓存,在更新数据库(推荐1)
- 缺点
- 缓存击穿
- mysql还没更新完毕,另外的线程读取到了mysql的旧数据,把旧值写回缓存
- 数据是主从同步,master没写完,读取线程去slave读取
- 解决办法
- 延时双删策略:更新线程操作流程 删除->更新数据库,修改为删除->更新数据库->休眠->删除,这样就保证了在更新过程中读取线程误写入旧数据问题(但是还是在一段时间内读到旧数据)也就是最终保证写线程能把在修改期间读线程写的脏数据删除。
- 休眠时间的确定:需要根据具体业务场景进行确定,其实就是修改过程中会存在多少读取的进程,这些进程会用多长时间更新了旧数据
- 吞吐量降低了:可以开启另外一个线程处理第二次删除
- 延时双删策略:更新线程操作流程 删除->更新数据库,修改为删除->更新数据库->休眠->删除,这样就保证了在更新过程中读取线程误写入旧数据问题(但是还是在一段时间内读到旧数据)也就是最终保证写线程能把在修改期间读线程写的脏数据删除。
- 缺点
- 先更新数据库,在删除缓存(推荐2)
- 缺点
- 缓存删除失败或者来不及删除,就会导致读取线程读取到缓存中的旧值
- 解决办法:充实机制+MQ
- 缺点
方案2和方案3,在业务场景怎么选,其实只要符合当前业务场景,哪个都可以,甚至方案1也是可以的,但是对比而言,更推荐方案3,方案3首先是避免了缓存击穿的问题,同时避免了延时双删需要确定休眠时间的问题,何乐而不为呢 。
方案2和方案3对比: