缓存和数据库的数据一致性解决方案
有3种情况:
程序先从缓存中读取数据,如果命中,则直接返回
程序先从缓存中读取数据,如果没有命中,则从数据库中读取,成功后将数据再存放到缓存中
程序先更新数据库,再删除缓存 (国外经典的解决方案,高并发也可能出现缓存数据不一致的情况)
不足:
最典型的就是更新缓存成功,更新数据库异常。那么就需要回滚缓存,如果缓存数据存的是精密计算后的结果,回滚缓存会造成前面的计算都白算了。
数据存储是以数据库为中心或是最终目标
不足:
1.并发情况下:线程A更新了数据库a=1,此时线程B抢占CPU资源,将a=2更新到数据库,并且将a=2更新到缓存,B线程操作完后,A线程将a=1再更新到缓存。会出现脏读
2.非并发情况下:比如10w个线程更新操作,只有1次读操作,那么就多更新了10w次缓存,造成了系统资源浪费
不足:
并发情况下:旧值a=1
A线程做更新,B线程读:
A首先删除缓存,此时B线程来读取a,发现缓存没有值,那么将旧值a=1添加到缓存,此时A线程更新数据库a=2,那么就会出现数据不一致的情况
不足:
普通的这种策略也是有比较小的概率出现数据不一致的问题:但概率极小
并发情况:a=1
线程A查询,B线程更新
线程A查询,刚好缓存失效,查询到a=1(还未写入缓存),此时请求B更新a=2,并删除缓存,此时A线程将a=1写入缓存。
由于读的线程往往比写的线程快,所以出现这种概率是极小的。
为了避免这种问题,可以将B删除缓存时,延迟一点删除。
延时双删方案执行步骤:
1.删除redis
2.更新数据库
3.延时500毫秒
4.删除redis (为防止更新数据库时,另外线程又将数据取出存入redis缓存)
此架构下,这种情况也会导致数据不一致,如下:
改进方案,就不用延时删除缓存的策略了:
方案一:更新完数据库后,由binlog订阅系统去更新redis缓存,写入binlog后,数据库事务可能还未提交,此时数据库的值依然是旧值。如果B线程来访问redis,也会访问到redis的旧值。
方案二:如果对数据一致性特别高的场景,也可以直接操作主库,高并发的情况需要再考虑。
通过订阅binlog日志方式更新缓存数据,也会有不足:
不足:比如redis挂了/更新缓存失败,那将如何处理?
改进如下:
通过消息队列的单线程顺序订阅和顺序消费来保证缓存数据与数据库数据的一致性。消费失败可以重复消费或也可以持久化
mysql中4种日志文件:ACID
原子性:undolog,回滚日志。记录了与binlog相反的操作
一致性:
隔离性:MVCC+锁实现
持久性:redolog,预写日志,也类似于中转日志文件(两阶段提交操作的也是redolog)
中继日志:relaylog,主从同步时的中转文件
系统级别日志:binlog
Innodb存储引擎级别日志:undolog,redolog
redolog的prepare阶段和commit阶段:
在恢复数据时,如果redolog日志都处于commit阶段,可以直接恢复
如果redolog日志部分处于prepare状态,然后去binlog日志中查找,是否有相同日志,如果有则将redolog中的日志改为commit状态,如果没有则会删掉无效数据
读读:不会存在任何问题,也不需要并发控制
读写:会有线程安全问题,需要进行并发控制(锁)或MVCC
写写:会有线程安全问题,更新丢失问题
如图是当前读和快照读的理解和触发场景:
记录之前数据的历史版本,新增操作不涉及MVCC的实现
readview 生成时机:
通过可见性算法来实现:具体通过如下3个字段来进行实现
按不同维度的锁可以分为如下:
mysql innodb的锁是通过锁索引来实现的
当我们用范围条件而不是相等条件检索数据,并请求共享或排他锁时,InnoDB会给符合条件的已有数据记录的索引项加锁;对于键值在条件范围内但并不存在的记录,叫做“间隙(GAP)”,InnoDB也会对这个“间隙”加锁,这种锁机制就是所谓的间隙锁。