相关文章推荐
强悍的蘑菇  ·  vue ...·  1 年前    · 

生产级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 来储存。线程安全。

    讨论版:多级缓存设计的流程图