这篇文章的主要思路是先介绍大家了解MySQL和Redis的数据一致性,然后对不一致性进行逆向,从而探究单线程中的不一致性。同时,探索多线程中的不一致性,制定数据一致性策略。1、什么是数据一致性“数据一致性”一般是指:缓存中有数据,缓存数据值=数据库中的值。但是,基于缓存中有数据,“一致性”可以包括两种情况:缓存中有数据,缓存数据值=数据库中的值,缓存中没有数据,以及数据库中的值=最新值(有请求查询数据库时,会将数据写入缓存,就会变成上面的“一致”状态)“数据不一致”:缓存的数据值≠该值在数据库中;缓存或数据库中有旧值,导致其他线程读取旧值数据。2.数据不一致及对策根据是否接收到写请求,缓存可以分为读写缓存和只读缓存。只读缓存:只在缓存中查找数据,即使用“更新数据库+删除缓存”的策略。读写缓存:需要对缓存中的数据进行增删改查,即使用“更新数据库+更新缓存”的策略。(1)对于只读缓存(updatedatabase+deletecache)read-onlycache:新增数据时,直接写入数据库;当数据更新(修改/删除)时,首先删除缓存。以后访问这些新增、删除、修改的数据时,缓存会丢失,然后查询数据库更新缓存。添加数据时,写入数据库;访问数据时,缓存缺失,检查数据库,更新缓存(始终处于“数据一致”状态,不会出现数据不一致)更新(修改/删除)数据时,会出现时序问题:更新数据库和删除缓存的顺序(这个过程会造成数据不一致的问题)在更新数据的过程中,可能会出现以下问题:没有并发请求,其中一个操作失败。在并发请求下,其他线程可能会读取旧值。因此,为了实现数据的一致性,需要保证两点:在没有并发请求的情况下,保证步骤A和B能够成功执行。并发请求下,在步骤A和B之间的间隔内,避免或消除其他线程的影响。接下来,我们针对有/没有并发的场景分析和使用不同的策略。无并发无并发请求下,在更新数据库和删除缓存值的过程中,由于操作被拆分成了两步,所以很可能会出现“第一步成功,第二步失败”的情况(由于单线程Step1和Step2是串行执行的,不太可能出现“step2成功,step1失败”)。(1)先删除缓存,再更新数据库(2)先更新数据库,再删除缓存解决策略:a.消息队列+异步重试无论采用哪种执行顺序,都可以将步骤1和步骤2的请求写入消息队列。当步骤2失败时,可以使用重试策略来“补偿”失败的操作。具体步骤如下:将待删除的缓存值或待更新的数据库值暂存在消息队列中(如使用Kafka消息队列)当缓存值被删除或数据库值更新成功后,移除这些值从消息队列中取出,以免重复操作。当删除缓存值或更新数据库值失败时,执行失败策略,重试服务从消息队列中重新读取这些值,然后再次删除或更新。当删除或更新失败时,需要重试,重试超过一定次数。向业务层发送错误消息。b.订阅Binlog变更日志创建一个更新缓存服务,接收数据变更的MQ消息,然后消费该消息,更新/删除Redis中的缓存数据。使用Binlog实时更新/删除Redis缓存。使用Canal,负责更新缓存的服务伪装成MySQL从节点,从MySQL接收Binlog,解析Binlog,获取实时数据变化信息,然后根据变化信息更新/删除Redis缓存。MQ+Canal策略将CanalServer接收到的Binlog数据直接交付给MQ进行解耦,利用MQ异步消费Binlog日志进行数据同步。无论采用MQ/Canal还是MQ+Canal策略异步更新缓存,对整个更新服务的数据可靠性和实时性要求都比较高。如果出现数据丢失或更新延迟,会导致MySQL和Redis中的数据不一致。因此,在使用该策略时,需要考虑在出现不同步问题时的降级或补偿方案。高并发采用上述策略后,单线程/非并发场景下的数据一致性可以得到保证。但是在高并发场景下,由于数据库层面的读写并发,会造成数据库与缓存数据不一致的问题(本质是后发生的读请求先返回)(1)先删除缓存,假设线程再更新数据库A删除缓存值后,由于网络延迟等原因,数据库没有更新。这时候线程B开始读取数据的时候,会发现缓存丢失了,然后去数据库查询。而当线程B从数据库中读取数据并更新缓存时,线程A开始更新数据库。此时缓存中的数据是旧值,而数据库中是最新值,造成“数据不一致”。本质是本应晚发生的“B线程读请求”在“A线程写请求”之前执行并返回。或者解决策略:设置缓存过期时间+延迟双删除通过设置缓存过期时间,如果出现上述清除缓存失败的情况,缓存过期后,读请求仍然可以从DB中读取最新的数据并更新缓存。减少数据不一致的影响范围。虽然一定时间范围内的数据存在差异,但可以保证数据的最终一致性。另外,也可以通过延迟双删来保证:线程A更新数据库值后,让它休眠一小段时间,保证线程B可以先从数据库中读取数据,然后将缺失的数据写入缓存,然后,线程A再次删除它。后面其他线程读取数据的时候,发现缓存不见了,就从数据库中读取最新的值。redis.delKey(X)db.update(X)Thread.sleep(N)redis.delKey(X)sleep时间:业务程序运行时,统计线程读取数据和写入缓存的运行时间,基于此估计。注意:如果难以接受sleep的写法,可以改用延迟队列。先删除缓存值再更新数据库,这样可能会导致请求访问数据库时因为缺少缓存,给数据库造成压力,这就是缓存穿透的问题。对于缓存穿透的问题,可以使用缓存空结果和布隆过滤器来解决。(2)先更新数据库,再删除缓存如果线程A更新了数据库中的值,但是在删除缓存值之前,线程B开始读取数据,那么此时,线程B查询缓存时,它发现缓存命中,会直接从缓存中读取旧值。本质也是,本应晚发生的“B线程-读取请求”在“A线程-删除缓存”之前执行完毕并返回。或者,在“先更新数据库,再删除缓存”的方案下,“读写分离+主从库延时”也会导致不一致:延迟消息根据经验向队列发送“延迟消息”,延迟删除Cache,同时也控制主从库的延迟,将不一致的概率降到最低。b.订阅binlog并异步删除key通过数据库的binlog异步淘汰key,使用工具(canal)将binlog日志收集发送给MQ,然后通过ACK机制确认删除缓存的过程。C。将删除消息写入数据库,通过比对数据库中的数据确认删除。先更新数据库,再删除缓存,这样可能会因为缺少缓存而导致访问数据库的请求,给数据库造成压力,也就是缓存穿透的问题。对于缓存穿透的问题,可以使用缓存空结果和布隆过滤器来解决。d.加锁和更新数据时,加写锁;查询数据时,加读锁。建议:优先考虑“先更新数据库,再删除缓存”的执行顺序。主要有两个原因:先删除缓存值再更新数据库,可能会因为缺少缓存而请求访问数据库,对数据库造成压力。业务应用中有时难以预估读取数据库和写入缓存的时间,导致延迟双删的休眠时间设置不当。(2)对于读写缓存(更新数据库+更新缓存)读写缓存:在缓存中进行增删改查,采用相应的回写策略将数据同步到数据库。同步直写:使用事务保证缓存和数据更新的原子性,失败重试(如果Redis自身出现故障,会降低服务的性能和可用性)异步回写:不同步写入数据库写入缓存时,等到数据从缓存中淘汰时,再写回数据库(在写回数据库之前,缓存失效,会造成数据丢失)。这种策略在秒杀领域已经看到过,业务层直接对缓存中的秒杀产品库存信息进行操作。,一段时间后写回数据库。一致性:同步直写>异步回写,因此,对于读写缓存,保持数据强一致性的主要思路是:使用同步直写,同步直写也有两个时序问题:更新数据库和更新缓存。无并发和高并发有四种场景会导致数据不一致:场景一和场景二的解决方案是:将请求的读取记录保存到缓存中,对比延迟消息,发现不一致后进行业务补偿。对于场景3和24的解决方案是:对于写请求,需要配合分布式锁使用。当一个写请求进来时,对于同一个资源的修改操作,先加一个分布式锁,保证只有一个线程同时更新数据库和缓存;未获得锁的线程将操作放入队列,延迟处理。这样就保证了多个线程对同一个资源的操作顺序,保证了一致性。其中,分布式锁的实现可以采用以下策略:(3)强一致性策略上述策略只能保证数据的最终一致性。要实现强一致性,最常见的方案是2PC、3PC、Paxos、Raft等共识协议,但它们的性能往往比较差,而且这些方案都比较复杂,需要考虑各种容错问题。如果业务层需要读数据的强一致性,可以采用如下策略:更新数据库时暂存并发读请求时,先将并发读请求暂存到Redis缓存客户端,等到数据库更新完毕,缓存值被删除。再次读取数据,确保数据一致性。序列化读写请求进入队列,工作线程从队列中取出任务顺序执行。修改service服务连接池,通过取模id选择服务连接,可以保证相同数据的读写落在同一个后端服务上。修改数据库DB连接池,通过取模id选择DB连接,可以保证同一个数据的读写在数据库层面是串行的。使用Redis分布式读写锁,将淘汰缓存和更新库表放入同一个写锁中,与其他读请求互斥,防止其间产生旧数据。读写互斥、写写互斥、读写共享,可以满足读多写少场景的数据一致性,也能保证并发。而过期时间是根据逻辑平均运行时间和响应超时时间来确定的。publicvoidwrite(){LockwriteLock=redis.getWriteLock(lockKey);writeLock.lock();try{redis.delete(key);db.update(record);}finally{writeLock.unlock();}}publicvoidread(){if(caching){return;}//nocacheLockreadLock=redis.getReadLock(lockKey);readLock.lock();try{record=db.get();}finally{readLock.unlock();}redis.set(key,record);}(4)总结对于读写缓存:同步直写,更新数据库+更新缓存对于只读缓存:更新数据库+删除缓存更通用的一致性策略制定:在并发场景下,使用“”更新数据库+更新缓存”需要分布式锁来保证缓存和数据的一致性,可能会存在“缓存资源浪费”和“机器性能浪费”;一般推荐使用“更新数据库+删除缓存”的方案。如果需要的热点数据较多,可以采用“更新数据库+更新缓存”的策略,在“更新数据库+删除缓存”方案中,推荐使用推荐的“先更新数据库,再删除缓存”的策略,因为先删除缓存可能会导致大量请求落到数据库,双删的延迟时间很难评估。在“先更新数据库,再删除缓存”的策略中,可以使用“消息队列+重试机制”的方案来保证缓存的删除。并通过“订阅binlog”进行缓存比对,再加上一层保护。此外,还需要通过缓存初始预热、多数据源触发、延迟消息比较等策略进行辅助和补偿。【多种数据更新触发源:定时任务扫描、业务系统MQ、binlog变更MQ,取长补短,保证数据不漏更新】3、数据不一致需要注意的其他问题(1)合理设置k-vsizeRediskey大小设计:由于一次网络传输最大MTU为1500字节,为保证高效性能,建议单次k-v大小不超过1KB,完成一次网络传输即可避免多种网络交互;k-v越小性能越好Redishotkey:当业务遇到单读hotkey时,通过增加副本增加读取能力或者使用hashtag将key的多份副本存储在多个分片中。当业务遇到单个写入热键时,业务需要拆分这个键的功能,这是一种不合理的设计。当业务遇到热分片时,即同一个分片上有多个热键导致单个分片CPU高。主题标签方法崩溃了。(2)避免其他导致缓存服务器崩溃的问题,这几乎会导致数据一致性策略失效。缓存穿透、缓存击穿、缓存雪崩、机器故障等问题阅读)确定一致性级别确定同步/异步模式选择缓存过程补充说明参考资料:1.如何保证Redis和MySQL的双写一致性2.干货|携程的最终一致性和强一致性缓存实践3.大厂都是MySQL到Redis同步怎么做4.缓存和数据库一致性策略5.缓存和数据库一致性保证6.缓存和数据库数据不一致的问题怎么解决7.Redis经典问题、缓存(穿透、雪崩、击穿、数据不一致、数据并发竞争、HotKey、BigKey)、分布式锁(watch乐观锁、setnx、Redisson)八、Redisson分布式锁场景及使用
