怎样包管Redis与MySQL双写划一性

[复制链接]
发表于 2025-12-27 16:24:24 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

×
什么是双写划一性标题?
双写划一性紧张指在一个数据同时存在于缓存(如Redis)和恒久化存储(如MySQL)的环境下,任何一方的数据更新都必须确保另一方数据的同步更新,以保持两边数据的划一状态。这一标题的核心在于怎样在高并发环境下精确处置处罚缓存与数据库的读写交互,防止数据出现不划一的环境。
划一性通常可以分为以下几个种别:
强划一性:
全部节点在任何时间都看到雷同的数据。任何更新操纵都会立刻对全部节点可见,包管了数据的强划一性。这意味着,假如一个节点完成了写操纵,那么全部其他节点读取雷同的数据之后,都将看到最新的效果。强划一性通常必要付出更高的代价,比方增长通讯开销和低落体系的可用性。
弱划一性:
体系中的数据在某些环境下大概会出现不划一的状态,但终极会收敛到划一状态。弱划一性下的体系答应在一段时间内,差别节点之间看到差别的数据状态。弱划一性通常用于必要在性能和划一性之间举行衡量的场景,比方缓存体系等。
终极划一性:
是弱划一性的一种特例,它包管了在颠末一段时间后,体系中的全部节点终极都会到达划一状态。只管在数据更新时大概会出现一段时间的不划一,但终极数据会收敛到划一状态。终极划一性通常通过一些技能本领来实现,比方基于版本向量或时间戳的数据复制和同步机制。
1、缓存常见读取数据、写数据用法

2、缓存不划一产生的缘故原由
假如数据不绝没有变更,那么就不会出现Redis和MySQL数据不划一性的标题。
两者之间数据不划一是由于一者发生了数据的变更,另一者怎样在短时间内同步数据的标题。由于每次数据变更必要同时操纵数据库和缓存,而他们又属于差别的体系,无法做到同时操纵乐成或失败,总会有一个时间差。在并发读写的时间大概就会出现缓存不划一的标题。
 
包管数据划一性通常涉及5种战略
1、先更新数据库,再更新缓存
  1. @Transactional
  2. public void updateUser(User user) {
  3.     // 1. 更新数据库
  4.     userMapper.updateUser(user);
  5.    
  6.     // 2. 更新Redis缓存
  7.     // 方式1:更新缓存
  8.     redisTemplate.opsForValue().set("user:" + user.getId(), user);
  9.    
  10.     // 方式2:删除缓存(推荐)
  11.     redisTemplate.delete("user:" + user.getId());
  12. }
复制代码

如上图所示,其大概实行的流程序次为:
1.客户端1 触发更新数据A的逻辑
2.客户端2 触发查询数据A的逻辑
3.客户端3 触发查询数据A的逻辑
4.客户端1 更新数据库中数据A
5.客户端2 查询缓存中数据A,掷中返回(旧数据)
6.客户端1 让缓存中数据A失效
7.客户端3 查询缓存中数据A,未掷中
8.客户端3 查询数据库中数据A,并更新到缓存中
可见,末了缓存中的数据A和数据库中的数据A是划一的,理论上大概会出现一小段时间数据不划一,不外这种概率也比力低,大部分的业务也不会有太大的标题。

为什么操纵缓存的时间是删除旧缓存而不是直接更新缓存?
举个例子:
线程A先发起一个写操纵,第一步先更新数据库,然后更新缓存
线程B再发起一个写操纵,第二步更新了数据库,然后更新缓存
当以上两个线程的实行,假如严酷先后序次实行,那么对于更新缓存照旧删除缓存去操纵缓存都可以,但是假如两个线程同时实行时,由于网络大概其他缘故原由,导致线程B先实行完更新缓存,然后线程A才会更新缓存。这时间缓存中生存的就是线程A的数据,而数据库中生存的是线程B的数据。这时间假如读取到的缓存就是脏数据。但是假如使用删除缓存取代更新缓存,那么就不会出现这个脏数据。
2、先更新缓存,再更新数据库
  1. @Transactional
  2. public void updateUser(User user) {
  3.     // 1. 删除Redis缓存
  4.     redisTemplate.delete("user:" + user.getId());
  5.    
  6.     // 2. 更新MySQL
  7.     userMapper.updateUser(user);
  8. }
复制代码

如上图所示,其大概实行的流程序次为:
1.客户端1,发起一个写操纵,第一步删除缓存
2.客户端2,发起一个读操纵,缓存中没有,则继承读数据库,读出来一个老数据,然后客户端2把老数据放入缓存中
3.客户端1更新数据库数据
如许就会出现缓存中存储的是旧数据,而数据库中存储的是新数据,如许就出现脏数据,以是我们一样寻常都接纳先操纵数据库,再操纵缓存。如许后续的读哀求从数据库获取最新数据并重新添补缓存。如许的操持低落了数据不划一的风险,提拔了体系的可靠性。同时,这也符合CAP定理中对于划一性(Consistency)和可用性(Availability)衡量的要求,在许多场景下,数据划一性被优先思量。因此一样寻常不发起使用这种方式。
3、延时双删战略
  1. @Transactional
  2. public void updateUser(User user) {
  3.     // 1. 删除Redis缓存
  4.     redisTemplate.delete("user:" + user.getId());
  5.    
  6.     // 2. 更新MySQL
  7.     userMapper.updateUser(user);
  8.    
  9.     // 3. 延迟一段时间后再次删除缓存
  10.     CompletableFuture.runAsync(() -> {
  11.         try {
  12.             Thread.sleep(500); // 延迟500毫秒
  13.             redisTemplate.delete("user:" + user.getId());
  14.         } catch (InterruptedException e) {
  15.             // 处理异常
  16.         }
  17.     });
  18. }
复制代码
延时双删战略紧张用于办理在高并发场景下,由于网络耽误、并发控制等缘故原由造成的数据库与缓存数据不划一的标题。
当更新数据库时,起首删除对应的缓存项,以确保后续的读哀求会从数据库加载最新数据。
但是由于网络耽误或其他不确定性因素,删除缓存与数据库更新之间大概存在时间窗口,导致在这段时间内的读哀求从数据库读取数据后写回缓存,新写入的缓存数据大概还未反映出数据库的最新变更。
以是为了办理这个标题,延时双删战略在第一次删除缓存后,设定一段短暂的耽误时间,如几百毫秒,然后在这段耽误时间竣事后再次实行删除缓存。如许做的目的是确保在数据库更新流传到全部节点,而且在缓存中的旧数据彻底逾期失效之前,第二次删除操纵可以消除缓存中大概存在的旧数据,从而进步数据划一性。
4、使用消息队列
  1. @Transactional
  2. public void updateUser(User user) {
  3.     // 1. 更新MySQL
  4.     userMapper.updateUser(user);
  5.    
  6.     // 2. 发送消息到消息队列
  7.     kafkaTemplate.send("user-update-topic", JSON.toJSONString(user));
  8. }
  9. // 3. 在消费者服务中更新缓存
  10. @KafkaListener(topics = "user-update-topic")
  11. public void consumeUserUpdate(String message) {
  12.     User user = JSON.parseObject(message, User.class);
  13.     // 更新Redis缓存
  14.     redisTemplate.opsForValue().set("user:" + user.getId(), user);
  15. }
复制代码
在高并发的业务场景中,消息队列是必不可少的技能之一。它不但可以异步解耦,还能削峰填谷。对包管体系的稳固性好坏常故意义的。
1.更新数据库
2.通过指定的topic发送到消息队列服务
3.然后斲丧者订阅该topic的消息,读取消息数据之后,再更新redis缓存。
5、使用 Canal 举行 MySQL binlog 同步
  1. @Component
  2. public class CanalClient {
  3.     @Autowired
  4.     private RedisTemplate<String, String> redisTemplate;
  5.     @PostConstruct
  6.     public void init() {
  7.         CanalConnector connector = CanalConnectors.newSingleConnector(
  8.             new InetSocketAddress("127.0.0.1", 11111),
  9.             "example", "", "");
  10.         
  11.         try {
  12.             connector.connect();
  13.             connector.subscribe(".*\\..*");
  14.             
  15.             while (true) {
  16.                 Message message = connector.getWithoutAck(100);
  17.                 long batchId = message.getId();
  18.                 List<CanalEntry.Entry> entries = message.getEntries();
  19.                
  20.                 if (batchId != -1 && entries.size() > 0) {
  21.                     for (CanalEntry.Entry entry : entries) {
  22.                         if (entry.getEntryType() == CanalEntry.EntryType.ROWDATA) {
  23.                             CanalEntry.RowChange rowChange = CanalEntry.RowChange.parseFrom(entry.getStoreValue());
  24.                            
  25.                             if (rowChange.getEventType() == CanalEntry.EventType.UPDATE) {
  26.                                 for (CanalEntry.RowData rowData : rowChange.getRowDatasList()) {
  27.                                     // 处理更新操作,更新Redis缓存
  28.                                     updateRedisCache(rowData);
  29.                                 }
  30.                             }
  31.                         }
  32.                     }
  33.                 }
  34.                 connector.ack(batchId);
  35.             }
  36.         } finally {
  37.             connector.disconnect();
  38.         }
  39.     }
  40.     private void updateRedisCache(CanalEntry.RowData rowData) {
  41.         // 根据rowData更新Redis缓存
  42.         // 这里需要根据具体的数据结构来实现
  43.     }
  44. }
复制代码
在数据库发生写操纵时,将变更记载在binlog或雷同的变乱日记中,然后使用一个专门的异步服务大概监听器订阅binlog的厘革(比如Canal),一旦检测到有数据更新,便根据binlog中的操纵信息定位到受影响的缓存项,删除或更新缓存中的对应数据,确保缓存与数据库保持划一。
 
回复

使用道具 举报

登录后关闭弹窗

登录参与点评抽奖  加入IT实名职场社区
去登录
快速回复 返回顶部 返回列表