共计 3498 个字符,预计需要花费 9 分钟才能阅读完成。
明天来说一个陈词滥调的问题,来看一个理论案例:
现有业务中往往都会通过缓存来进步查问效率,升高数据库的压力,尤其是在分布式高并发场景下,大量的申请间接拜访 Mysql 很容易造成性能问题。
有一天老板找到了你 ……
老板:据说你会缓存?
你:来看我操作。
你设计了一个最常见的缓存计划,基于这种计划,开始对用户积分性能进行优化,但当你睡的正酣时,零碎轻轻进行了上面操作:
1、线程 A 依据业务会把用户 id 为 1 的积分更新成 100
2、线程 B 依据业务会把用户 id 为 1 的积分更新成 200
3、在数据库层面,因为数据库用锁来保障了 ACID,线程 A 和线程 B 不存在并发状况,,无论数据库中最终的值是 100 还是 200,咱们都假如正确
4、假如线程 B 在 A 之后更新数据库,则数据库中的值为 200
5、线程 A 和线程 B 在回写缓存过程中,很可能会产生线程 A 在线程 B 之后操作缓存的状况(因为网络调用存在不确定性),这个时候缓存内的值会被更新成 100,产生了缓存和数据库不统一的状况。
第二天早上你收到了用户投诉,怎么办?人工批改积分值还是删库跑路?
但凡处于不同物理地位的两个操作,如果操作的是雷同数据,都会遇到一致性问题,这是分布式系统不可避免的一个痛点。
1 什么是数据一致性?
数据一致性通常讲的次要是数据存储系统,主从 mysql、分布式存储系统等,如何保证数据一致性,
比如说主从一致性,正本一致性,保障不同的工夫或者雷同的申请拜访这种主从数据库时拜访的数据是一致性的,不会这次拜访是后果 A 下次是后果 B。
2 CAP 定理
说到数据一致性,就必须说 CAP 定理。
CAP 定理是 2000 年由 Brewer 提出的,他认为分布式系统在设计和部署时,面临 3 个外围问题:
Consistency:一致性。数据库 ACID 操作是在一个事务中对数据加以束缚,使得执行后仍处于统一状态,而分布式系统在进行更新操作时所有的用户都应该读到最新值。
Availability:可用性。每一个操作总是可能在肯定工夫内返回后果。后果能够是胜利或失败,肯定工夫是给定的工夫。
Partition Tolerance:分区容忍性。思考零碎效力和可伸缩性,是否可进行数据分区。
CAP 定理认为,一个提供数据服务的存储系统无奈同时满足数据一致性、数据可用性、分区容忍性。
为什么?如果采纳分区,分布式节点之间就须要进行通信,波及到通信,就会存在某一时刻这一节点只实现一部分业务操作,在通信实现的这一段时间内,数据就是不统一的。如果要保障一致性,就要 在通信实现的这段时间内爱护数据,使得对拜访这些数据的操作都不可用。
反过来思考,如果想保障一致性和可用性,那么数据就不可能分区。一个简略的了解就是所有的数据就必须寄存在一个数据库外面,不能进行数据库拆分。这个对于大数据量、高并发的互联网利用来说,是不可承受的。
3 数据一致性模型
基于 CAP 定理,一些分布式系统通过复制数据来进步零碎的可靠性和容错性,也就是将数据的不同正本寄存在不同的机器。罕用的一致性模型有:
强一致性:数据更新实现后,任何后续拜访将会返回最新的数据。这在分布式网络环境简直不可能实现。
弱一致性:零碎不保证数据更新后的拜访会失去最新的数据。客户端获取最新的数据之前须要满足一些非凡条件。
最终一致性:是弱一致性的一种特例,保障用户最终可能读取到某操作对系统特定数据的更新。
4 如何保证数据一致性?
针对刚开始的问题,如果加以思考,你可能会发现不论是先写 MySQL 数据库,再删除 Redis 缓存;还是先删除缓存,再写库,都有可能呈现数据不统一的状况。
(1)先删除缓存
1、如果先删除 Redis 缓存数据,然而还没有来得及写入 MySQL,另一个线程就来读取;
2、这个时候发现缓存为空,则去 Mysql 数据库中读取旧数据写入缓存,此时缓存中为脏数据;
3、而后数据库更新后发现 Redis 和 Mysql 呈现了数据不统一的问题。
(2)后删除缓存
1、如果先写了库,而后再删除缓存,可怜的写库的线程挂了,导致了缓存没有删除;
2、这个时候就会间接读取旧缓存,最终也导致了数据不统一状况;
3、因为写和读是并发的,没法保障程序, 就会呈现缓存和数据库的数据不统一的问题。
解决方案 1:分布式锁
在平时开发中,利用分布式锁可能算是比拟常见的解决方案了。利用分布式锁把缓存操作和数据库操作封装为逻辑上的一个操作能够保证数据的一致性,具体流程为:
1、每个想要操作缓存和数据库的线程都必须先申请分布式锁;
2、如果胜利取得锁,则进行数据库和缓存操作,操作结束开释锁;
3、如果没有取得锁,依据不同业务能够抉择阻塞期待或者轮训,或者间接返回的策略。
流程见下图:
利用分布式锁是解决分布式事务的一种计划,然而在肯定水平上会升高零碎的性能,而且分布式锁的设计要思考到 down 机和死锁的意外状况。
解决方案 2:提早双删
在写库前后都进行 redis.del(key) 操作,并且设定正当的超时工夫。
伪代码如下:
public void write(String key, Object data){redis.delKey( key);
db.updateData(data);
Thread.sleep(500);
redis.delKey(key);
}
具体步骤:
1、先删除缓存
2、再写数据库
3、休眠 500 毫秒(这个依据读取的业务工夫来定)
4、再次删除缓存
来看之前的案例在这种计划下的情景:
T1 线程线删除缓存再更新 db , T1 线程更新 db 实现之前 T2 线程如果读取到 db 旧的数据, 会再把旧的数据写入 Redis 缓存。
此时 T1 线程提早一段时间后再删除 Redis 缓存操作. 当其余线程再读取缓存为 null 时会查问 db 最新数据从新进行缓存, 保障了 Mysql 和 Redis 缓存的数据一致性。
在此基础上,缓存也要设置过期工夫,来保障最终数据的一致性。只有缓存过期,就去读数据库而后从新缓存。
这种双删 + 缓存超时的策略,最差的状况是在缓存过期工夫内产生数据存在不统一,而且写的时候减少了耗时。
然而这种计划还会呈现一个问题,如何保障写入库后,再次删除缓存胜利?
如果删除失败,还有可能呈现数据不统一的状况。这时候须要提供一个重试计划。
解决方案 3:异步更新缓存(基于 Mysql binlog 的同步机制)
1、波及到更新的数据操作,利用 Mysql binlog 进行增量订阅生产;
2、将音讯发送到音讯队列;
3、通过音讯队列生产将增量数据更新到 Redis 上。
这样的成果是:
读取 Redis 缓存:热数据都在 Redis 上;
写 Mysql:增删改都是在 Mysql 进行操作;
更新 Redis 数据:Mysql 的数据操作都记录到 binlog,通过音讯队列及时更新到 Redis 上。
这样一旦 MySQL 中产生了新的写入、更新、删除等操作,就能够把 binlog 相干的音讯推送至 Redis,Redis 再依据 binlog 中的记录,对 Redis 进行更新。
其实这种机制,很相似 MySQL 的主从备份机制,因为 MySQL 的主备也是通过 binlog 来实现的数据一致性。
计划 2 中的重试计划就能够借助计划 3,启动一个订阅程序订阅数据库的 binlog,提取所须要的数据和 key,另起代码获取这些信息。如果尝试删除缓存失败,就发送音讯给音讯队列,从新从音讯队列获取数据,重试删除操作。
参考文档:
- https://mp.weixin.qq.com/s/k38MZRAGmZ8EhDB5DcVhhQ
- https://baijiahao.baidu.com/s?id=1678826754388688520&wfr=spider&for=pc
- https://www.php.cn/faq/415782.html
- https://blog.csdn.net/u013256816/article/details/50698167
- https://blog.csdn.net/My\_Best\_bala/article/details/121977033?spm=1001.2101.3001.6661.1&utm\_medium=distribute.pc\_relevant\_t0.none-task-blog-2~default~CTRLIST~Rate-1.pc\_relevant\_antiscan&depth\_1-utm\_source=distribute.pc\_relevant\_t0.none-task-blog-2~default~CTRLIST~Rate-1.pc\_relevant\_antiscan&utm\_relevant_index=1
感激浏览~
作者:京东批发 李泽阳
起源:京东云开发者社区 转载请注明起源