生产级Redis 高并发分布式锁实战2:缓存架构设计问题优化
https://www.cnblogs.com/yizhiamumu/p/16556667.html
总结篇3:redis 典型缓存架构设计问题及性能优化
https://www.cnblogs.com/yizhiamumu/p/16557996.html
总结篇4:redis 核心数据存储结构及核心业务模型实现应用场景
https://www.cnblogs.com/yizhiamumu/p/16566540.html
DB\redis\zookeeper分布式锁设计
https://www.cnblogs.com/yizhiamumu/p/16663243.html
在缓存和数据库双写场景下,一致性是如何保证的
https://www.cnblogs.com/yizhiamumu/p/16686751.html
如何保证 Redis 的高并发和高可用?讨论redis的单点,高可用,集群
https://www.cnblogs.com/yizhiamumu/p/16586968.html
分布式缓存应用场景与redis持久化机制
https://www.cnblogs.com/yizhiamumu/p/16702154.html
Redisson 源码分析及实际应用场景介绍
https://www.cnblogs.com/yizhiamumu/p/16706048.html
Redis 高可用方案原理初探
https://www.cnblogs.com/yizhiamumu/p/16709290.html
RedisCluster集群架构原理与通信原理
https://www.cnblogs.com/yizhiamumu/p/16704556.html
缓存一般是直接将数据放到离计算最近的地方(目前大部分放在内存中),解决 CPU 和 I/O 的速度不匹配的问题,用来加快计算处理速度,通常会对热点数据进行缓存,保证较高的命中率。在互联网的架构设计中,数据库及缓存一般相互配合使用来满足不同的场景需求,比如在大流量的请求中会使用缓存来加速。
Redis 在互联网行业中使用最为广泛。Redis 在很多时候也被称为“内存数据库”,它集合了缓存和数据库的优势,但并非开启持久化和主备同步机制就可以高枕无忧。从架构设计的角度思考:
缓存就是缓存,缓存数据会随时丢失,缓存存在的目的是拦截到数据库的请求,相比数据的可靠性、一致性,还是
吞吐量
、稳定性优先。
缓存有三大矛盾:
缓存
实时性和一致性
问题:当有了写入后咋办?
缓存的
穿透
问题:当没有读到咋办?
缓存对数据库
高并发
访问:都来访问数据库咋办?
第一个也就是本问题。而解决这三大矛盾的刷新策略包括:
实时策略
——用户体验好,是默认应该使用的策略;
异步策略
——适用于并发量大,但是数据没有那么关键的情况,好处是实时性好;
定时策略
——并发量实在太大,数据量也大的情况,异步都难以满足的场景;
写入数据库成功,即让缓存失效,下一次读取时再缓存。
这是缓存的实时策略。当然,并不适用于所有的场景。
实时策略是最常用的策略,也是保持实时性最好的策略:
读取的过程,应用程序先从 cache 取数据,没有得到,则从数据库中取数据,成功后,放到缓存中。如果命中,应用程序从 cache 中取数据,取到后返回。
写入的过程,把数据存到数据库中,成功后,再让缓存失效,失效后下次读取的时候,会被写入缓存。
从用户体验的角度,应该数据库有了写入,就马上废弃缓存,触发一次数据库的读取,从而更新缓存。
然而,这和高并发就矛盾了——如果所有的都实时从数据库里面读取,高并发场景下,数据库往往受不了。
一台MySQL,一台Redis,两台应用服务器,用户的数据存储持久化在MySQL中,缓存在Redis,有请求的时候从Redis中获取缓存的用户数据,有修改则同时修改MySQL和Redis中的数据。现在问题是:
1. 先保存到MySQL和先保存到Redis都面临着一个保存成功而另外一个保存失败的情况,这样,如何保证MySQL与Redis中的数据同步?
2. 两台应用服务器的并发访问,如何保证数据的安全性?
一些用户请求在某些情况下是可能重复发送的,如果是查询类操作并无大碍,但其中有些涉及写入操作,一旦重复了,可能会导致很严重的后果。例如交易接口如果重复请求,可能会重复下单。
要想达到数据一致性,需要保证两点:
无并发请求下,保证A和B步骤都能成功执行。
并发请求下,在A和B步骤的间隔中,避免或消除其他线程的影响。
一致性就是数据保持一致,在分布式系统中,可以理解为多个节点中数据的值是一致的。
强一致性
:这种一致性级别是最符合用户直觉的,它要求系统写入什么,读出来的也会是什么,用户体验好,但实现起来往往对系统的性能影响大
弱一致性
:这种一致性级别约束了系统在写入成功后,不承诺立即可以读到写入的值,也不承诺多久之后数据能够达到一致,但会尽可能地保证到某个时间级别(比如秒级别)后,数据能够达到一致状态
最终一致性
:最终一致性是弱一致性的一个特例,系统会保证在一定时间内,能够达到一个数据一致的状态。这里之所以将最终一致性单独提出来,是因为它是弱一致性中非常推崇的一种一致性模型,也是业界在大型分布式系统的数据一致性上比较推崇的模型
缓存可以提升性能、缓解数据库压力,但是使用缓存也会导致数据
不一致性
的问题。一般我们是如何使用缓存呢?有三种经典的缓存模式:
旁路缓存模式
Cache-Aside Pattern
读写穿透
Read-Through/Write through
异步缓存写入
Write behind
1
旁路缓存模式
Cache-Aside Pattern
Cache-Aside Pattern,即
旁路缓存模式
,它的提出是为了尽可能地解决缓存与数据库的数据不一致问题。
1.1 Cache-Aside读流程
Cache-Aside Pattern
的读请求流程如下:
更新的时候,先
更新数据库,然后再删除缓存
。
2 读写穿透 Read-Through/Write-Through
Read/Write Through
模式中,服务端把缓存作为主要数据存储。应用程序跟数据库缓存交互,都是通过
抽象缓存层
完成的。
2.1 Read-Through
Read-Through
的简要流程如下
这种方式下,缓存和数据库的一致性不强,
对一致性要求高的系统要谨慎使用
。但是它适合频繁写的场景,MySQL的
InnoDB Buffer Pool机制
就使用到这种模式。
操作缓存的时候,删除缓存呢,还是更新缓存?
一般业务场景,我们使用的就是
Cache-Aside
模式。 有些小伙伴可能会问,
Cache-Aside
在写入请求的时候,为什么是
删除缓存而不是更新缓存
呢?
这时候,缓存保存的是A的数据(老数据),数据库保存的是B的数据(新数据),数据
不一致
了,脏数据出现啦。如果是
删除缓存取代更新缓存
则不会出现这个脏数据问题。
更新缓存相对于删除缓存
,还有两点劣势:
如果你写入的缓存值,是经过复杂计算才得到的话。更新缓存频率高的话,就浪费性能啦。
在写数据库场景多,读数据场景少的情况下,数据很多时候还没被读取到,又被更新了,这也浪费了性能呢(实际上,写多的场景,用缓存也不是很划算了)
总结起来就是,从服务器读取主服务器Bin log中的数据,从而同步到自己的数据库中。
canal是阿里巴巴旗下的一款开源项目,纯Java开发。基于数据库增量日志解析,提供增量数据订阅&消费,目前主要支持了MySQL(也支持mariaDB)
server代表一个canal运行实例,对应于一个jvm
instance对应于一个数据队列 (1个server对应1..n个instance)
instance模块:
eventParser (数据源接入,模拟slave协议和master进行交互,协议解析)
eventSink (Parser和Store链接器,进行数据过滤,加工,分发的工作)
eventStore (数据存储)
metaManager (增量订阅&消费信息管理器)
canal模拟mysql slave的交互协议,伪装自己为mysql slave,向mysql master发送dump协议
mysql master收到dump请求,开始推送binary log给slave(也就是canal)
canal解析binary log对象(原始为byte流)
但如果只是进行删除缓存,只删除了一次,也可能会失败。
就需要加上重试机制了。如果删除缓存失败,写入重试表,使用定时任务重试。或者写入mq,让mq自动重试。
推荐使用mq自动重试机制。
在binlog订阅者中如果删除缓存失败,则发送一条mq消息到mq服务器,在mq消费者中自动重试3次。如果有任意一次成功,则直接返回成功。如果重试3次后还是失败,则该消息自动被放入
死信
队列,后面可能需要人工介入。
用
binlog
同步的方式相对比较优雅。
1、mysql发生变更产生一条binlog
2、binlog写进消息队列(MQ)
3、程序监听消息队列,得到binlog消息
4、解析binlog,得到变更的内容
5、将变更的内容更新至redis
由于借助了MQ消息队列,那无须担心有漏变更的情况(MQ一般都能确保
至少一次性
)。两个
数据源
的更新只能
保证最终一致性,
无法保证强一致性。
修改服务Service
连接池
,id取模选取服务连接,能够保证同一个数据的读写都落在同一个后端服务上。
修改数据库DB连接池,id取模选取DB连接,能够保证同一个数据的读写在数据库层面是串行的。
使用Redis分布式读写锁
读读并发解决方案:
a. 延迟消息凭借经验发送「延迟消息」到队列中,延迟删除缓存,同时也要控制主从库延迟,尽可能降低不一致发生的概率。
b. 订阅binlog,异步删除。通过数据库的binlog来异步淘汰key,利用工具(canal)将binlog日志采集发送到MQ中,然后通过ACK机制确认处理删除缓存。
c. 删除消息写入数据库通过比对数据库中的数据,进行删除确认 先更新数据库再删除缓存,有可能导致请求因缓存缺失而访问数据库,给数据库带来压力,也就是缓存穿透的问题。针对缓存穿透问题,可以用缓存空结果、布隆过滤器进行解决。
d. 加锁更新数据时,加写锁;查询数据时,加读锁。
其中,分布式锁的实现可以使用以下策略:
- 乐观锁:使用版本号, updatetime;缓存中,只允许高版本覆盖低版本
- watch 实现 redis 乐观锁:watch 监控redisKey 状态值,创建redis 事务,key+1, 执行事务,key 被修改过则回滚
- setnx : 获取锁,set/setnx;释放锁:del 命令/ lua 脚本
- redisson 分布式锁:利用redis 的hash 结构作为储存单元,将业务指定的名称作为key, 将随机uuid 和 线程id 作为 field, 最后将加乐的次数作为 value 来储存。线程安全。
讨论版:多级缓存设计的流程图