酒店 深圳 网站制作,小型网站设计及建设论文范本,网站备案审核状态查询,北京市网上服务平台登录Redis缓存使用问题 
数据一致性 
只要使用到缓存#xff0c;无论是本地内存做缓存还是使用 redis 做缓存#xff0c;那么就会存在数据同步的问题。 
我以 Tomcat 向 MySQL 中写入和删改数据为例#xff0c;来给你解释一下#xff0c;数据的增删改操作具体是如何进行的。 我…Redis缓存使用问题 
数据一致性 
只要使用到缓存无论是本地内存做缓存还是使用 redis 做缓存那么就会存在数据同步的问题。 
我以 Tomcat 向 MySQL 中写入和删改数据为例来给你解释一下数据的增删改操作具体是如何进行的。 我们分析一下几种解决方案 
1、先更新缓存再更新数据库 
2、先更新数据库再更新缓存 
3、先删除缓存后更新数据库 
4、先更新数据库后删除缓存 
新增数据类 
如果是新增数据数据会直接写到数据库中不用对缓存做任何操作此时缓存中本身就没有新增数据而数据库中是最新值此时缓存和数据库的数据是一致的。 
更新缓存类 
1、先更新缓存再更新DB 
这个方案我们一般不考虑。原因是更新缓存成功更新数据库出现异常了导致缓存数据与数据库数据完全不一致而且很难察觉因为缓存中的数据一直都存在。 2、先更新DB再更新缓存 
这个方案也我们一般不考虑原因跟第一个一样数据库更新成功了缓存更新失败同样会出现数据不一致问题。同时还有以下问题 
1 并发问题 
同时有请求A和请求B进行更新操作那么会出现 
1 线程A更新了数据库 
2 线程B更新了数据库 
3 线程B更新了缓存 
4 线程A更新了缓存 
这就出现请求A更新缓存应该比请求B更新缓存早才对但是因为网络等原因B却比A更早更新了缓存。这就导致了脏数据因此不考虑。 
2 业务场景问题 
如果你是一个写数据库场景比较多而读数据场景比较少的业务需求采用这种方案就会导致数据压根还没读到缓存就被频繁的更新浪费性能。 
除了更新缓存之外我们还有一种就是删除缓存。 
到底是选择更新缓存还是淘汰缓存呢 
主要取决于“更新缓存的复杂度”更新缓存的代价很小此时我们应该更倾向于更新缓存以保证更高的缓存命中率更新缓存的代价很大此时我们应该更倾向于淘汰缓存。 
删除缓存类 
3、先删除缓存后更新DB 
该方案也会出问题具体出现的原因如下。 
1、此时来了两个请求请求 A更新操作 和请求 B查询操作 
2、请求 A 会先删除 Redis 中的数据然后去数据库进行更新操作 
3、此时请求 B 看到 Redis 中的数据时空的会去数据库中查询该值补录到 Redis 中 
4、但是此时请求 A 并没有更新成功或者事务还未提交请求B去数据库查询得到旧值 
5、那么这时候就会产生数据库和 Redis 数据不一致的问题。 
如何解决呢其实最简单的解决办法就是延时双删的策略。就是 
1先淘汰缓存 
2再写数据库 
3休眠1秒再次淘汰缓存 
这段伪代码就是“延迟双删” 
redis.delKey(X)
db.update(X)
Thread.sleep(N)
redis.delKey(X) 
这么做可以将1秒内所造成的缓存脏数据再次删除。 
那么这个1秒怎么确定的具体该休眠多久呢 
针对上面的情形读该自行评估自己的项目的读数据业务逻辑的耗时。然后写数据的休眠时间则在读数据业务逻辑的耗时基础上加几百ms即可。这么做的目的就是确保读请求结束写请求可以删除读请求造成的缓存脏数据。 
但是上述的保证事务提交完以后再进行删除缓存还有一个问题就是如果你使用的是** Mysql 的读写分离的架构**的话那么其实主从同步之间也会有时间差。 
此时来了两个请求请求 A更新操作 和请求 B查询操作 
请求 A 更新操作删除了 Redis 
请求主库进行更新操作主库与从库进行同步数据的操作 
请 B 查询操作发现 Redis 中没有数据 
去从库中拿去数据此时同步数据还未完成拿到的数据是旧数据。 
此时的解决办法有两个 
1、还是使用双删延时策略。只是睡眠时间修改为在主从同步的延时时间基础上加几百ms。 
2、就是如果是对 Redis 进行填充数据的查询数据库操作那么就强制将其指向主库进行查询。 
继续深入采用这种同步淘汰策略吞吐量降低怎么办 
那就将第二次删除作为异步的。自己起一个线程异步删除。这样写的请求就不用沉睡一段时间后了再返回。这么做加大吞吐量。 
继续深入第二次删除,如果删除失败怎么办 
所以我们引出了下面的第四种策略先更新数据库再删缓存。 
4、先更新DB后删除缓存 
这种方式被称为Cache Aside Pattern读的时候先读缓存缓存没有的话就读数据库然后取出数据后放入缓存同时返回响应。更新的时候先更新数据库然后再删除缓存。 
如何选择问题 
一般在线上更多的偏向与使用删除缓存类操作因为这种方式的话会更容易避免一些问题。 
因为删除缓存更新缓存的速度比在DB中要快一些所以一般情况下我们可能会先用先更新DB后删除缓存的操作。因为这种情况下缓存不一致性的情况只有可能是查询比删除慢的情况而这种情况相对来说会少很多。同时结合延时双删的处理可以有效的避免缓存不一致的情况。 缓存穿透、击穿、雪崩 
缓存穿透 
是指查询一个根本不存在的数据缓存层和存储层都不会命中于是这个请求就可以随意访问数据库这个就是缓存穿透缓存穿透将导致不存在的数据每次请求都要到存储层去查询失去了缓存保护后端存储的意义。 
缓存穿透问题可能会使后端存储负载加大由于很多后端存储不具备高并发性甚至可能造成后端存储宕掉。通常可以在程序中分别统计总调用数、缓存层命中数、存储层命中数如果发现大量存储层空命中可能就是出现了缓存穿透问题。 
造成缓存穿透的基本原因有两个。 
第一自身业务代码或者数据出现问题比如我们数据库的 id 都是1开始自增上去的如发起为id值为 -1 的数据或 id 为特别大不存在的数据。如果不对参数做校验数据库id都是大于0的我一直用小于0的参数去请求你每次都能绕开Redis直接打到数据库数据库也查不到每次都这样并发高点就容易崩掉了。 
第二,一些恶意攻击、爬虫等造成大量空命中。下面我们来看一下如何解决缓存穿透问题。 
1.缓存空对象 
当存储层不命中到数据库查发现也没有命中那么仍然将空对象保留到缓存层中之后再访问这个数据将会从缓存中获取,这样就保护了后端数据源。 
缓存空对象会有两个问题: 
第一空值做了缓存意味着缓存层中存了更多的键需要更多的内存空间(如果是攻击问题更严重),比较有效的方法是针对这类数据设置一个较短的过期时间让其自动剔除。 
第二缓存层和存储层的数据会有一段时间窗口的不一致可能会对业务有一定影响。例如过期时间设置为5分钟如果此时存储层添加了这个数据那此段时间就会出现缓存层和存储层数据的不一致此时可以利用消前面所说的数据一致性方案处理。 
2.布隆过滤器拦截 
在访问缓存层和存储层之前,将存在的key用布隆过滤器提前保存起来,做第一层拦截。例如:一个推荐系统有4亿个用户id每个小时算法工程师会根据每个用户之前历史行为计算出推荐数据放到存储层中,但是最新的用户由于没有历史行为,就会发生缓存穿透的行为,为此可以将所有推荐数据的用户做成布隆过滤器。如果布隆过滤器认为该用户id不存在,那么就不会访问存储层,在一定程度保护了存储层。 这种方法适用于数据命中不高、数据相对固定、实时性低(通常是数据集较大)的应用场景,代码维护较为复杂,但是缓存空间占用少。 
缓存击穿 
缓存击穿是指一个Key非常热点在不停的扛着大并发大并发集中对这一个点进行访问当这个Key在失效的瞬间持续的大并发就穿破缓存直接请求数据库就像在一个完好无损的桶上凿开了一个洞。 
缓存击穿的话设置热点数据永远不过期。或者加上互斥锁就能搞定了。 
使用互斥锁(mutex key) 
业界比较常用的做法是使用mutex。简单地来说就是在缓存失效的时候判断拿出来的值为空不是立即去load db而是先使用缓存工具的某些带成功操作返回值的操作比如Redis的SETNX或者Memcache的ADD去set一个mutex key当操作返回成功时再进行load db的操作并回设缓存否则就重试整个get缓存的方法。 
伪代码如下图 永远不过期 
这里的“永远不过期”包含两层意思 
(1) 从redis上看确实没有设置过期时间这就保证了不会出现热点key过期问题也就是“物理”不过期。 
(2) 从功能上看如果不过期那不就成静态的了吗所以我们把过期时间存在key对应的value里如果发现要过期了通过一个后台的异步线程进行缓存的构建也就是“逻辑”过期 
从实战看这种方法对于性能非常友好唯一不足的就是构建缓存时候其余线程(非构建缓存的线程)可能访问的是老数据但是对于一般的互联网功能来说这个还是可以忍受。 
缓存雪崩 
缓存雪崩:由于缓存层承载着大量请求,有效地保护了存储层,但是如果缓存层由于某些原因不能提供服务比如同一时间缓存数据大面积失效那一瞬间Redis跟没有一样于是所有的请求都会达到存储层存储层的调用量会暴增造成存储层也会级联宕机的情况。 
缓存雪崩的英文原意是stampeding herd(奔逃的野牛)指的是缓存层宕掉后流量会像奔逃的野牛一样,打向后端存储。 
预防和解决缓存雪崩问题,可以从以下三个方面进行着手。 
1保证缓存层服务高可用性。和飞机都有多个引擎一样如果缓存层设计成高可用的,即使个别节点、个别机器、甚至是机房宕掉依然可以提供服务例如前面介绍过的Redis 
Sentinel和 Redis Cluster都实现了高可用。 
2依赖隔离组件为后端限流并降级。无论是缓存层还是存储层都会有出错的概率可以将它们视同为资源。作为并发量较大的系统假如有一个资源不可用可能会造成线程全部阻塞(hang)在这个资源上造成整个系统不可用。 
3提前演练。在项目上线前演练缓存层宕掉后应用以及后端的负载情况以及可能出现的问题,在此基础上做一些预案设定。 
4将缓存失效时间分散开比如我们可以在原有的失效时间基础上增加一个随机值比如1-5分钟随机这样每一个缓存的过期时间的重复率就会降低就很难引发集体失效的事件。 
热点Key 
在Redis中访问频率高的key称为热点key。 
产生原因和危害 
原因 
热点问题产生的原因大致有以下两种 
用户消费的数据远大于生产的数据热卖商品、热点新闻、热点评论、明星直播。 
在日常工作生活中一些突发的事件例如双十一期间某些热门商品的降价促销当这其中的某一件商品被数万次点击浏览或者购买时会形成一个较大的需求量这种情况下就会造成热点问题。同理被大量刊发、浏览的热点新闻、热点评论、明星直播等这些典型的读多写少的场景也会产生热点问题。 
请求分片集中超过单Server的性能极限。在服务端读数据进行访问时往往会对数据进行分片切分此过程中会在某一主机Server上对相应的Key进行访问当访问超过Server极限时就会导致热点Key问题的产生。 
危害 
1、流量集中达到物理网卡上限。 
2、请求过多缓存分片服务被打垮。 
3、DB击穿引起业务雪崩。 
发现热点key 
预估发现 
针对业务提前预估出访问频繁的热点key例如秒杀商品业务中秒杀的商品都是热点key。 
当然并非所有的业务都容易预估出热点key可能出现漏掉或者预估错误的情况。 
客户端发现 
客户端其实是距离key最近的地方因为Redis命令就是从客户端发出的以Jedis为例可以在核心命令入口使用这个Google Guava中的AtomicLongMap进行记录如下所示。 
使用客户端进行热点key的统计非常容易实现但是同时问题也非常多 
(1) 无法预知key的个数存在内存泄露的危险。 
(2) 对于客户端代码有侵入各个语言的客户端都需要维护此逻辑维护成本较高。 
(3) 规模化汇总实现比较复杂。 
Redis发现 
monitor命令 
monitor命令可以监控到Redis执行的所有命令利用monitor的结果就可以统计出一段时间内的热点key排行榜命令排行榜客户端分布等数据。 Facebook开源的redis-faina正是利用上述原理使用Python语言实现的例如下面获取最近10万条命令的热点key、热点命令、耗时分布等数据。为了减少网络开销以及加快输出缓冲区的消费速度monitor尽可能在本机执行。 
此种方法会有两个问题 
1、monitor命令在高并发条件下内存暴增同时会影响Redis的性能所以此种方法适合在短时间内使用。 
2、只能统计一个Redis节点的热点key对于Redis集群需要进行汇总统计。 
可以参考的框架Facebook开源的redis-faina正是利用上述原理使用Python语言实现的 
hotkeys 
Redis在4.0.3中为redis-cli提供了--hotkeys用于找到热点key。 如果有错误需要先把内存逐出策略设置为allkeys-lfu或者volatile-lfu否则会返回错误。 但是如果键值较多执行较慢和热点的概念的有点背道而驰同时热度定义的不够准确。 
抓取TCP包发现 
Redis客户端使用TCP协议与服务端进行交互通信协议采用的是RESP。如果站在机器的角度可以通过对机器上所有Redis端口的TCP数据包进行抓取完成热点key的统计 
此种方法对于Redis客户端和服务端来说毫无侵入是比较完美的方案但是依然存在3个问题 
(1) 需要一定的开发成本 
(2) 对于高流量的机器抓包对机器网络可能会有干扰同时抓包时候会有丢包的可能性。 
(3) 维护成本过高。 
对于成本问题有一些开源方案实现了该功能例如ELK(ElasticSearch Logstash Kibana)体系下的packetbeat[2] 插件可以实现对Redis、MySQL等众多主流服务的数据包抓取、分析、报表展示 
解决热点key 
发现热点key之后需要对热点key进行处理。 
使用二级缓存 
可以使用 guava-cache或hcache发现热点key之后将这些热点key加载到JVM中作为本地缓存。访问这些key时直接从本地缓存获取即可不会直接访问到redis层了有效的保护了缓存服务器。 
key分散 
将热点key分散为多个子key然后存储到缓存集群的不同机器上这些子key对应的value都和热点key是一样的。当通过热点key去查询数据时通过某种hash算法随机选择一个子key然后再去访问缓存机器将热点分散到了多个子key上。 
BigKey 
什么是bigkey 
bigkey是指key对应的value所占的内存空间比较大例如一个字符串类型的value可以最大存到512MB一个列表类型的value最多可以存储23-1个元素。 
如果按照数据结构来细分的话一般分为字符串类型bigkey和非字符串类型bigkey。 
字符串类型体现在单个value值很大一般认为超过10KB就是bigkey但这个值和具体的OPS相关。 
非字符串类型哈希、列表、集合、有序集合,体现在元素个数过多。 
bigkey无论是空间复杂度和时间复杂度都不太友好下面我们将介绍它的危害。 
bigkey的危害 
bigkey的危害体现在三个方面: 
1、内存空间不均匀.(平衡):例如在Redis Cluster中bigkey 会造成节点的内存空间使用不均匀。 
2、超时阻塞:由于Redis单线程的特性操作bigkey比较耗时也就意味着阻塞Redis可能性增大。 
3、网络拥塞:每次获取bigkey产生的网络流量较大 
假设一个bigkey为1MB每秒访问量为1000那么每秒产生1000MB 的流量,对于普通的千兆网卡(按照字节算是128MB/s)的服务器来说简直是灭顶之灾而且一般服务器会采用单机多实例的方式来部署,也就是说一个bigkey可能会对其他实例造成影响,其后果不堪设想。 
bigkey的存在并不是完全致命的 
如果这个bigkey存在但是几乎不被访问,那么只有内存空间不均匀的问题存在,相对于另外两个问题没有那么重要紧急,但是如果bigkey是一个热点key(频繁访问)那么其带来的危害不可想象,所以在实际开发和运维时一定要密切关注bigkey的存在。 
发现bigkey 
redis-cli --bigkeys可以命令统计bigkey的分布。 但是在生产环境中开发和运维人员更希望自己可以定义bigkey的大小而且更希望找到真正的bigkey都有哪些,这样才可以去定位、解决、优化问题。 
判断一个key是否为bigkey只需要执行debug object key查看serializedlength属性即可它表示 key对应的value序列化之后的字节数。 如果是要遍历多个则尽量不要使用keys的命令可以使用scan的命令来减少压力。 
scan 
Redis 从2.8版本后提供了一个新的命令scan它能有效的解决keys命令存在的问题。和keys命令执行时会遍历所有键不同,scan采用渐进式遍历的方式来解决 keys命令可能带来的阻塞问题但是要真正实现keys的功能,需要执行多次scan。可以想象成只扫描一个字典中的一部分键直到将字典中的所有键遍历完毕。scan的使用方法如下: 
scan cursor [match pattern] [count number] 
cursor 是必需参数实际上cursor是一个游标第一次遍历从0开始每次scan遍历完都会返回当前游标的值,直到游标值为0,表示遍历结束。 
Match pattern 是可选参数,它的作用的是做模式的匹配,这点和keys的模式匹配很像。 
Count number 是可选参数,它的作用是表明每次要遍历的键个数,默认值是10,此参数可以适当增大。 可以看到第一次执行scan 0返回结果分为两个部分: 
第一个部分9就是下次scan需要的cursor 
第二个部分是10个键。接下来继续 
直到得到结果cursor变为0说明所有的键已经被遍历过了。 
除了scan 以外Redis提供了面向哈希类型、集合类型、有序集合的扫描遍历命令解决诸如hgetall、smembers、zrange可能产生的阻塞问题对应的命令分别是hscan、sscan、zscan它们的用法和scan基本类似请自行参考Redis官网。 渐进式遍历可以有效的解决keys命令可能产生的阻塞问题但是scan并非完美无瑕如果在scan 的过程中如果有键的变化(增加、删除、修改)那么遍历效果可能会碰到如下问题:新增的键可能没有遍历到遍历出了重复的键等情况也就是说scan并不能保证完整的遍历出来所有的键这些是我们在开发时需要考虑的。 
如果键值个数比较多scan  debug object会比较慢可以利用Pipeline机制完成。对于元素个数较多的数据结构debug object执行速度比较慢存在阻塞Redis的可能所以如果有从节点,可以考虑在从节点上执行。 
解决bigkey 
主要思路为拆分对 big key 存储的数据 big value进行拆分变成value1value2… valueN等等。 
例如big value 是个大json 通过 mset 的方式将这个 key 的内容打散到各个实例中或者一个hash每个field代表一个具体属性通过hget、hmget获取部分valuehset、hmset来更新部分属性。 
例如big value 是个大list可以拆成将list拆成。 list_1 list_2, list3, ...listN 
其他数据类型同理。 
数据倾斜 
数据倾斜其实分为访问量倾斜或者数据量倾斜: 
1、hotkey出现造成集群访问量倾斜 
2、bigkey 造成集群数据量倾斜 
解决方案前面已经说过了这里不再赘述。 
Redis脑裂 
所谓的脑裂就是指在有主从集群中同时有两个主节点它们都能接收写请求。而脑裂最直接的影响就是客户端不知道应该往哪个主节点写入数据结果就是不同的客户端会往不同的主节点上写入数据。而且严重的话脑裂会进一步导致数据丢失。 
哨兵主从集群脑裂 
现在假设有三台服务器一台主服务器两台从服务器还有一个哨兵。 
基于上边的环境这时候网络环境发生了波动导致了sentinel没有能够心跳感知到master但是哨兵与slave之间通讯正常。所以通过选举的方式提升了一个salve为新master。如果恰好此时server1仍然连接的是旧的master而server2连接到了新的master上。数据就不一致了哨兵恢复对老master节点的感知后会将其降级为slave节点然后从新maste同步数据full resynchronization导致脑裂期间老master写入的数据丢失。 
而且基于setNX指令的分布式锁可能会拿到相同的锁基于incr生成的全局唯一id也可能出现重复。通过配置参数 min-replicas-to-write 2
min-replicas-max-lag 10 
第一个参数表示最少的salve节点为2个 
第二个参数表示数据复制和同步的延迟不能超过10秒 
配置了这两个参数如果发生脑裂原master会在客户端写入操作的时候拒绝请求。这样可以避免大量数据丢失。 
集群脑裂 
Redis集群的脑裂一般是不存在的因为Redis集群中存在着过半选举机制而且当集群16384个槽任何一个没有指派到节点时整个集群不可用。所以我们在构建Redis集群时应该让集群 Master 节点个数最少为 3 个且集群可用节点个数为奇数。 
不过脑裂问题不是是可以完全避免只要是分布式系统必然就会一定的几率出现这个问题CAP的理论就决定了。 
多级缓存实例 
一个使用了Redis集群和其他多种缓存技术的应用系统架构如图 首先用户的请求被负载均衡服务分发到Nginx上此处常用的负载均衡算法是轮询或者一致性哈希轮询可以使服务器的请求更加均衡而一致性哈希可以提升Nginx应用的缓存命中率。 
接着Nginx应用服务器读取本地缓存实现本地缓存的方式可以是Lua Shared Dict,或者面向磁盘或内存的Nginx Proxy Cache以及本地的Redis实现等如果本地缓存命中则直接返回。Nginx应用服务器使用本地缓存可以提升整体的吞吐量降低后端的压力尤其应对热点数据的反复读取问题非常有效。 
如果Nginx应用服务器的本地缓存没有命中就会进一步读取相应的分布式缓存——Redis分布式缓存的集群可以考虑使用主从架构来提升性能和吞吐量如果分布式缓存命中则直接返回相应数据并回写到Nginx应用服务器的本地缓存中。 
如果Redis分布式缓存也没有命中则会回源到Tomcat集群在回源到Tomcat集群时也可以使用轮询和一致性哈希作为负载均衡算法。当然如果Redis分布式缓存没有命中的话Nginx应用服务器还可以再尝试一次读主Redis集群操作目的是防止当从 Redis集群有问题时可能发生的流量冲击。 
在Tomcat集群应用中首先读取本地平台级缓存如果平台级缓存命中则直接返回数据并会同步写到主Redis集群然后再同步到从Redis集群。此处可能存在多个Tomcat实例同时写主Redis集群的情况可能会造成数据错乱需要注意缓存的更新机制和原子化操作。 
如果所有缓存都没有命中系统就只能查询数据库或其他相关服务获取相关数据并返回当然,我们已经知道数据库也是有缓存的。 
整体来看这是一个使用了多级缓存的系统。Nginx应用服务器的本地缓存解决了热点数据的缓存问题Redis分布式缓存集群减少了访问回源率Tomcat应用集群使用的平台级缓存防止了相关缓存失效崩溃之后的冲击数据库缓存提升数据库查询时的效率。正是多级缓存的使用才能保障系统具备优良的性能。 
互联网大厂中的Redis 
经过几年演进携程金融形成了自顶向下的多层次系统架构如业务层、平台层、基础服务层等其中用户信息、产品信息、订单信息等基础数据由基础平台等底层系统产生服务于所有的金融系统对这部分基础数据我们引入了统一的缓存服务系统名utag。 缓存数据有三大特点全量、准实时、永久有效在数据实时性要求不高的场景下业务系统可直接调用统一的缓存查询接口。 
在构建此统一缓存服务时候有三个关键目标 
数据准确性DB中单条数据的更新一定要准确同步到缓存服务。 
数据完整性将对应DB表的全量数据进行缓存且永久有效从而可以替代对应的DB查询。 
系统可用性我们多个产品线的多个核心服务都已经接入utag的高可用性显得尤为关键。 
整体方案 系统在多地都有部署故缓存服务也做了相应的异地多机房部署一来可以让不同地区的服务调用本地区服务无需跨越网络专线二来也可以作为一种灾备方案增加可用性。 
对于缓存的写入由于缓存服务是独立部署的因此需要感知业务DB数据变更然后触发缓存的更新本着“可以多次更新但不能漏更新”的原则设计了多种数据更新触发源定时任务扫描业务系统MQ、binlog变更MQ相互之间作为互补来保证数据不会漏更新。 
对于MQ使用携程开源消息中间件QMQ 和 Kafka在公司内部QMQ和Kafka也做了异地机房的互通。 
使用MQ来驱动多地多机房的缓存更新在不同的触发源触发后会查询最新的DB数据然后发出一个缓存更新的MQ消息不同地区机房的缓存系统同时监听该主题并各自进行缓存的更新。 
对于缓存的读取utag系统提供dubbo协议的缓存查询接口业务系统可调用本地区的接口省去了网络专线的耗时50ms延迟。在utag内部查询redis数据并反序列化为对应的业务model再通过接口返回给业务方。 
数据准确性 
不同的触发源对缓存更新过程是一样的整个更新步骤可抽象为4步 
step1触发更新查询DB中的新数据并发送统一的MQ 
step2接收MQ查询缓存中的老数据 
step3新老数据对比判断是否需要更新 
step4若需要则更新缓存 
并发控制 
若一条DB数据出现了多次更新且刚好被不同的触发源触发更新缓存时候若未加控制可能出现数据更新错乱如下图所示 故需要将第2、3、4步加锁使得缓存刷新操作全部串行化。由于utag本身就依赖了redis此处我们的分布式锁就基于redis实现。 
基于updateTime的更新顺序控制 
即使加了锁也需要进一步判断当前db数据与缓存数据的新老因为到达缓存更新流程的顺序并不代表数据的真正更新顺序。我们通过对比新老数据的更新时间来实现数据更新顺序的控制。若新数据的更新时间大于老数据的更新时间则认为当前数据可以直接写入缓存。 
我们系统从建立之初就有自己的MySQL规范每张表都必须有update_time字段且设置为ON UPDATE CURRENT_TIMESTAMP但是并没有约束时间字段的精度大部分都是秒级别的因此在同一秒内的多次更新操作就无法识别出数据的新老。 
针对同一秒数据的更新策略我们采用的方案是先进行数据对比若当前数据与缓存数据不相等则直接更新并且发送一条延迟消息延迟1秒后再次触发更新流程。 
举个例子假设同一秒内同一条数据出现了两次更新value1和value2期望最终缓存中的数据是value2。若这两次更新后的数据被先后触发分两种情况 
case1若value1先更新value2后更新两者都可更新到缓存中因为虽然是同一秒但是值不相等则缓存中最终数据为value2。 
case2若value2先更新value1后更新则第一轮更新后缓存数据为value1不是期望数据之后对比发现是同一秒数据后会通过消息触发二次更新重新查询DB数据为value2可以更新到缓存中。如下图所示 数据完整性设计 
上述数据准确性是从单条数据更新角度的设计而我们构建缓存服务的目的是替代对应DB表的查询因此需要缓存对应DB表的全量数据而数据的完整性从以下三个方面得到保证 
1“把鸡蛋放到多个篮子里”使用多种触发源定时任务业务MQbinglog MQ来最大限度降低单条数据更新缺失的可能性。 
单一触发源有可能出现问题比如消息类的触发依赖业务系统、中间件canel、中间件QMQ和Kafka扫表任务依赖分布式调度平台、MySQL等。中间任何一环都可能出现问题而这些中间服务同时出概率的可能相对来说就极小了相互之间可以作为互补。 
2全量数据刷新任务全表扫描定时任务每周执行一次来进行兜底确保缓存数据的全量准确同步。 
3数据校验任务监控Redis和DB数据是否同步并进行补偿。