ToB企服应用市场:ToB评测及商务社交产业平台

标题: 如何包管Redis缓存和数据库的数据一致性 [打印本页]

作者: 我爱普洱茶    时间: 2024-8-1 15:43
标题: 如何包管Redis缓存和数据库的数据一致性
前言

  如果项目业务处于起步阶段,流量非常小,那无论是读哀求照旧写哀求,直接操纵数据库即可,这时架构模型是这样的:

  但随着业务量的增长,项目业务哀求量越来越大,这时如果每次都从数据库中读数据,那肯定会有性能问题。这个阶段通常的做法是,引入缓存来提高读性能,架构模型就变成了这样:

  在实际开发过程中,缓存的使用频率是非常高的,只要使用缓存和数据库存储,就难免会出现双写时数据一致性的问题,就是 Redis 缓存的数据和数据库中生存的数据出现不雷同的现象。


  如上图所示,大多数人的许多业务操纵都是根据这个图来做缓存的,这样能有效减轻数据库压力。但是一旦设计到双写或者数据库和缓存更新等操纵,就很容易出现数据一致性的问题。无论是先写数据库,在删除缓存,照旧先删除缓存,在写入数据库,都会出现数据一致性的问题。例如:

  总的来说,写和读在多数情况下都是并发的,不能绝对包管先后顺序,就会很容易出现缓存和数据库数据不一致的情况,那我们又该如何办理呢?
一、谈谈一致性

  首先,我们先来看看有哪几种一致性的情况呢?


二、 景象分析

2.1 针对读场景

  A哀求查询数据,如果命中缓存,那么直接取缓存数据返回即可。如果哀求中不存在,数据库中存在,那么直接取数据库数据返回,然后将数据同步到Redis中。不会存在数据不一致的情况。


  在高并发的情况下,A哀求和B哀求一起访问某条数据,如果缓存中数据存在,直接返回即可,如果不存在,直接取数据库数据返回即可。无论A哀求B哀求谁先谁后,本质上没有对数据举行修改,数据本身没变,只是从缓存中取照旧从数据库中取的问题,因此不会存在数据不一致的情况。
  因此,单独的读场景是不会造成Redis与数据库缓存不一致的情况,因此我们不用关心这种情况。
2.2 针对写场景

  如果该数据在缓存中不存在,那么直接修改数据库中的数据即可,不会存在数据不一致的情况。
  如果该数据在缓存中和数据库中都存在,那么就必要既修改缓存中的数据又修改数据库中的数据。如果写数据库的值与更新到缓存值是一样的,可以立刻更新缓存;如果写数据库的值与更新缓存的值不一致,在高并发的场景下,还存在先后关系,这就会导致数据不一致的问题。例如:

三、同步计谋

  想要包管缓存与数据库的双写一致,一共有4种方式,即4种同步计谋:

  从这4种同步计谋中,我们必要作出比力的是:更新缓存与删除缓存哪种方式更合适?应该先操纵数据库照旧先操纵缓存?
3.1 先更新缓存,再更新数据库

  这个方案我们一般不考虑。缘故原由是当数据同步时,更新 Redis 缓存成功,但更新数据库出现异常时,会导致 Redis 缓存数据与数据库数据完全不一致,而且这很难察觉,因为 Redis 缓存中的数据一直都存在。
  只要缓存举行了更新,后续的读哀求根本上就不会出现缓存未命中的情况。但在某些业务场景下,更新数据的本钱较大,并不是单纯将数据的数据查询出来丢到缓存中即可,而是必要毗连许多张表组装对应数据存入缓存中,并且可能存在更新后,该数据并不会被使用到的情况。
3.2 先更新数据库,再更新缓存

  这个方案我们一般也是不考虑。缘故原由是当数据同步时,数据库更新成功,但 Redis 缓存更新失败,那么此时数据库中是最新值,Redis 缓存中是旧值。之后的应用体系的读哀求读到的都是 Redis 缓存中旧数据。只有当 Redis 缓存数据失效后,才气从数据库中重新获得精确的值。
  该方案还存在并发引发的一致性问题,假设同时有两个线程举行数据更新操纵,如下图所示:


  从上图可以看到,线程1固然先于线程2发生,但线程2操纵数据库和缓存的时间,却要比线程1的时间短,执行时序发生繁芜,终极这条数据效果是不符合预期的。如果是写多读少的场景,采用这种方案就会导致,数据压根还没读到,缓存就被频仍的更新,浪费性能。
3.3 先删除缓存,后更新数据库

  这种方案只是尽可能包管一致性而已,极端情况下,照旧有可能发生数据不一致问题,缘故原由是当数据同步时,如果删除 Redis 缓存失败,更新数据库成功,那么此时数据库中是最新值,Redis 缓存中是旧值。之后的应用体系的读哀求读到的都是 Redis 缓存中旧数据。只有当 Redis 缓存数据失效后,才气从数据库中重新获得精确的值。由于缓存被删除,下次查询无法命中缓存,必要在查询后将数据写入缓存,增加查询逻辑。同时在高并发的情况下,同一时间大量哀求访问该条数据,第一条查询哀求还未完成写入缓存操纵时,这种情况,大量查询哀求都会打到数据库,加大数据库压力。
  该方案还存在并发引发的一致性问题,假设同时有两个线程举行数据更新操纵,如下图所示。当缓存被线程一删除后,如果此时有新的读哀求(线程二)发生,由于缓存已经被删除,这个读哀求(线程二)将会去从数据库查询。如果此时线程一还没有修改完数据库,线程二从数据库读的数据仍然是旧值,同时线程二将读的旧值写入到缓存。线程一完成后,数据库变为新值,而缓存照旧旧值。


  从上图可见,先删除 Redis 缓存,后更新数据库,当发生读/写并发时,照旧存在数据不一致的情况。如何办理呢?最简单的办理办法就是延时双删计谋:先淘汰缓存、再写数据库、休眠后再次淘汰缓存。这样做的目的,就是确保读哀求竣事,写哀求可以删除读哀求造成的缓存脏数据。
  1. public void deleteRedisData(UserInfo userInfo){
  2.     // 删除Redis中的缓存数据
  3.     jedis.del(userInfo);
  4.     // 更新MySQL数据库数据
  5.     userInfoDao.update(userInfo);
  6.     try {
  7.         TimeUnit.SECONDS.sleep(2);
  8.     } catch(Exception exp){
  9.         exp.printStackTrace();
  10.     }
  11.     // 删除Redis中的缓存数据
  12.     jedis.del(userInfo);
  13. }
复制代码
  延时双删就能彻底办理不一致吗?固然不一定来。首先,我们评估的延时时间并不能完全代表实际运行过程中的耗时,运行过程如果因为体系压力过大,我们评估的耗时就是不正确,仍然会导致数据不一致的出现。其次,延时双删固然在包管事务提交完以后再举行删除缓存,但是如果使用的是MySQL的读写分离的机构,主从同步之间着实也会有时间差。
3.4 先更新数据库,后删除缓存

   实际使用中,发起采用这种方案。固然,这种方案着实一样也可能有失败的情况。
    当数据同步时,如果更新数据库成功,而删除 Redis 缓存失败,那么此时数据库中是最新值,Redis 缓存中是旧值。之后的应用体系的读哀求读到的都是 Redis 缓存中旧数据。只有当 Redis 缓存数据失效后,才气从数据库中重新获得精确的值。读的时间,先读缓存,缓存没有的话,就读数据库,然后取出数据后放入缓存,同时返回相应。更新的时间,先更新数据库,然后再删除缓存。


  该方案还存在并发引发的一致性问题,假设同时有两个线程举行数据更新操纵,如下图所示。当数据库的数据被更新后,如果此时缓存还没有被删除,那么缓存中的数据仍然是旧值。如果此时有新的读哀求(查询数据)发生,由于缓存中的数据是旧值,这个读哀求将会获取到旧值。当缓存刚好失效,这时有哀求来读缓存(线程一),未命中缓存,然后到数据库中读取,在要写入缓存时,线程二来修改了数据库,而线程一写入缓存的是旧的数据,导致了数据的不一致。


四、办理办法

  当我们在应用中同时使用MySQL和Redis时,如何包管两者的数据一致性呢?下面就来分享几种实用的办理方案。
4.1 双写一致性

  最直接的办法就是在业务代码中同时对MySQL和Redis举行更新。通常我们会先更新MySQL,然后再更新Redis。
  1. // 更新MySQL
  2. userMapper.update(user);
  3. // 更新Redis
  4. redisTemplate.opsForValue().set("user_" + user.getId(), user);
复制代码
  这种方式最大的问题就是在于网络故障或者程序异常的情况下,可能会导致MySQL和Redis中的数据不一致。因此,我们必要额外的手段来检测和修复数据不一致的情况。
4.2 异步更新(异步通知)

  在更新数据库数据时,同时发送一个异步通知给Redis,让Redis知道数据库数据已经更新,必要更新缓存中的数据。这个过程是异步的,不会壅闭数据库的更新操纵。当Redis收到异步通知后,会立即删除缓存中对应的数据,确保缓存中没有旧数据。这样,即使在这个过程中有新的读哀求发生,也不会读取到旧数据。等到数据库更新完成后,Redis再次从数据库中读取最新的数据并缓存起来。
  1. // 更新MySQL
  2. userMapper.update(user);
  3. // 发送消息
  4. rabbitTemplate.convertAndSend("updateUser", user.getId());
  5. /**
  6. * 然后在消息消费者中更新Redis。
  7. */
  8. @RabbitListener(queues = "updateUser")
  9. public void updateUser(String userId) {
  10.     User user = userMapper.selectById(userId);
  11.     redisTemplate.opsForValue().set(redisTemplate.opsForValue().set("user_" + user.getId(), user);
  12. }
复制代码
  这种异步通知的方式,可以确保Redis中的数据与数据库中的数据保持一致,制止出现数据不一致的情况。这种方案可以低沉数据不一致的风险,但仍然无法完全制止。因为消息队列本身也可能因为各种缘故原由丢失消息。
4.3 使用Redis的事务支持

  Redis提供了事务(Transaction)支持,可以将一系列的操纵作为一个原子操纵执行。我们可以使用Redis的事务来实现MySQL和Redis的原子更新。
  1. redisTemplate.execute(new Sessioncallback<Object>(){
  2.     @0verridepublic Object execute(RedisOperations operations) throws DataAccessException {
  3.         // 开启事务
  4.         operations.multi();
  5.         // 更新MySQL
  6.         userMapper.update(user);
  7.         // 更新Redis
  8.         operations.opsForValue().set("user_" + user.getId(),user);
  9.         // 执行事务
  10.         operations.exec();
  11.         return null;
  12.     }
  13. });
复制代码
  使用Redis事务可以确保MySQL和Redis的更新在同一事务中执行,制止了中间出现不一致的情况。但必要注意的是,Redis的事务并非严格的ACID事务,可能存在部分成功的情况。
4.4 用 Redisson 实现读锁和写锁

  Redisson 是一个基于 Redis 的分布式 Java 对象存储和缓存框架,它提供了丰富的功能和 API 来操纵 Redis 数据库,其中包括了读写锁的支持。读写锁是一种常用的并发控制机制,它答应多个线程同时读取共享资源,但在写操纵时互斥,只答应一个线程举行写操纵。使用 Redisson 的读写锁方法:
  下面是一个使用 Redisson 读写锁的示例,通过 Redisson 的 RReadWriteLock 对象获取读锁和写锁,并在必要的代码段中举行相应的操纵。执行完操纵后,使用 unlock() 方法释放锁,最后关闭 Redisson 客户端。
  1. import org.redisson.Redisson;
  2. import org.redisson.api.RLock;
  3. import org.redisson.api.RedissonClient;
  4. import org.redisson.config.Config;
  5. public class RedissonReadWriteLockExample {
  6.     public static void main(String[] args) {
  7.         // 创建 Redisson 客户端
  8.         Config config = new Config();
  9.         config.useSingleServer().setAddress("redis://127.0.0.1:6379");
  10.         RedissonClient redisson = Redisson.create(config);
  11.         
  12.         // 获取读写锁
  13.         RReadWriteLock rwLock = redisson.getReadWriteLock("myLock");
  14.         RLock readLock = rwLock.readLock();
  15.         RLock writeLock = rwLock.writeLock();
  16.         
  17.         try {
  18.             // 获取读锁并进行读操作
  19.             readLock.lock();
  20.             // 读取共享资源
  21.             
  22.             // 获取写锁并进行写操作
  23.             writeLock.lock();
  24.             // 写入共享资源
  25.         } finally {
  26.             // 释放锁
  27.             writeLock.unlock();
  28.             readLock.unlock();
  29.         }
  30.         
  31.         // 关闭 Redisson 客户端
  32.         redisson.shutdown();
  33.     }
  34. }
复制代码
五、结语

  综上所述,我们提供了更全面的MySQL与Redis数据一致性办理方案。根据详细的业务需求和体系环境,选择合适的方案可以提高数据一致性的可靠性。然而,每种方案都有其优缺点和适用场景,必要综合考虑权衡。
  对于并发几率很小的数据(如个人维度的订单数据、用户数据等),这种几乎不用考虑这个问题,很少会发生缓存不一致,可以给缓存数据加上逾期时间,每隔一段时间触发读的主动更新即可。就算并发很高,如果业务上能容忍短时间的缓存数据不一致(如商品名称,商品分类菜单等),缓存加上逾期时间依然可以办理大部分业务对于缓存的要求。
   把今天最好的体现看成明天最新的出发点…...~

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。




欢迎光临 ToB企服应用市场:ToB评测及商务社交产业平台 (https://dis.qidao123.com/) Powered by Discuz! X3.4