做简单网站怎么做广州物流网站开发
news/
2025/10/3 11:55:31/
文章来源:
做简单网站怎么做,广州物流网站开发,杭州网络安全公司,四川住房和城乡建设局网站首页前言 如果项目业务处于起步阶段#xff0c;流量非常小#xff0c;那无论是读请求还是写请求#xff0c;直接操作数据库即可#xff0c;这时架构模型是这样的#xff1a; 但随着业务量的增长#xff0c;项目业务请求量越来越大#xff0c;这时如果每次都从数据库中读数据…前言 如果项目业务处于起步阶段流量非常小那无论是读请求还是写请求直接操作数据库即可这时架构模型是这样的 但随着业务量的增长项目业务请求量越来越大这时如果每次都从数据库中读数据那肯定会有性能问题。这个阶段通常的做法是引入缓存来提高读性能架构模型就变成了这样 在实际开发过程中缓存的使用频率是非常高的只要使用缓存和数据库存储就难免会出现双写时数据一致性的问题就是 Redis 缓存的数据和数据库中保存的数据出现不相同的现象。 如上图所示大多数人的很多业务操作都是根据这个图来做缓存的这样能有效减轻数据库压力。但是一旦设计到双写或者数据库和缓存更新等操作就很容易出现数据一致性的问题。无论是先写数据库在删除缓存还是先删除缓存在写入数据库都会出现数据一致性的问题。例如
先删除了redis缓存但是因为其他什么原因还没来得及写入数据库另外一个线程就来读取发现缓存为空则去数据库读取到之前的数据并写入缓存此时缓存中为脏数据。如果先写入了数据库但是在缓存被删除前写入数据库的线程因为其他原因被中断了没有删除掉缓存就也会出现数据不一致的情况。 总的来说写和读在多数情况下都是并发的不能绝对保证先后顺序就会很容易出现缓存和数据库数据不一致的情况那我们又该如何解决呢
一、谈谈一致性 首先我们先来看看有哪几种一致性的情况呢 强一致性如果你的项目对缓存的要求是强一致性的那么请不要使用缓存。这种一致性级别是最符合用户直觉的它要求系统写入什么读出来的也会是什么用户体验好但实现起来往往对系统的性能影响大。读请求和写请求会串行化串到一个内存队列里去这样会大大增加系统的处理效率吞吐量也会大大降低。弱一致性这种一致性级别约束了系统在写入成功后不承诺立即可以读到写入的值也不承诺多久之后数据能够达到一致但会尽可能地保证到某个时间级别比如秒级别后数据能够达到一致状态。最终一致性最终一致性是弱一致性的一个特例系统会保证在一定时间内能够达到一个数据一致的状态。这里之所以将最终一致性单独提出来是因为它是弱一致性中非常推崇的一种一致性模型也是业界在大型分布式系统的数据一致性上比较推崇的模型。一般情况下高可用只确保最终一致性不确保强一致性。
二、 情景分析
2.1 针对读场景 A请求查询数据如果命中缓存那么直接取缓存数据返回即可。如果请求中不存在数据库中存在那么直接取数据库数据返回然后将数据同步到Redis中。不会存在数据不一致的情况。 在高并发的情况下A请求和B请求一起访问某条数据如果缓存中数据存在直接返回即可如果不存在直接取数据库数据返回即可。无论A请求B请求谁先谁后本质上没有对数据进行修改数据本身没变只是从缓存中取还是从数据库中取的问题因此不会存在数据不一致的情况。 因此单独的读场景是不会造成Redis与数据库缓存不一致的情况因此我们不用关心这种情况。
2.2 针对写场景 如果该数据在缓存中不存在那么直接修改数据库中的数据即可不会存在数据不一致的情况。 如果该数据在缓存中和数据库中都存在那么就需要既修改缓存中的数据又修改数据库中的数据。如果写数据库的值与更新到缓存值是一样的可以马上更新缓存如果写数据库的值与更新缓存的值不一致在高并发的场景下还存在先后关系这就会导致数据不一致的问题。例如
当更新数据时如更新某商品的库存当前商品的库存是100现在要更新为99先更新数据库更改成99然后删除缓存发现删除缓存失败了这意味着数据库存的是99而缓存是100这导致数据库和缓存不一致。在高并发的情况下如果当删除完缓存的时候这时去更新数据库但还没有更新完另外一个请求来查询数据发现缓存里没有就去数据库里查还是以上面商品库存为例如果数据库中产品的库存是100那么查询到的库存是100然后插入缓存插入完缓存后原来那个更新数据库的线程把数据库更新为了99导致数据库与缓存不一致的情况。
三、同步策略 想要保证缓存与数据库的双写一致一共有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 缓存后更新数据库当发生读/写并发时还是存在数据不一致的情况。如何解决呢最简单的解决办法就是延时双删策略先淘汰缓存、再写数据库、休眠后再次淘汰缓存。这样做的目的就是确保读请求结束写请求可以删除读请求造成的缓存脏数据。
public void deleteRedisData(UserInfo userInfo){// 删除Redis中的缓存数据jedis.del(userInfo);// 更新MySQL数据库数据userInfoDao.update(userInfo);try {TimeUnit.SECONDS.sleep(2);} catch(Exception exp){exp.printStackTrace();}// 删除Redis中的缓存数据jedis.del(userInfo);
}延时双删就能彻底解决不一致吗当然不一定来。首先我们评估的延时时间并不能完全代表实际运行过程中的耗时运行过程如果因为系统压力过大我们评估的耗时就是不准确仍然会导致数据不一致的出现。其次延时双删虽然在保证事务提交完以后再进行删除缓存但是如果使用的是MySQL的读写分离的机构主从同步之间其实也会有时间差。
3.4 先更新数据库后删除缓存 实际使用中建议采用这种方案。当然这种方案其实一样也可能有失败的情况。 当数据同步时如果更新数据库成功而删除 Redis 缓存失败那么此时数据库中是最新值Redis 缓存中是旧值。之后的应用系统的读请求读到的都是 Redis 缓存中旧数据。只有当 Redis 缓存数据失效后才能从数据库中重新获得正确的值。读的时候先读缓存缓存没有的话就读数据库然后取出数据后放入缓存同时返回响应。更新的时候先更新数据库然后再删除缓存。 该方案还存在并发引发的一致性问题假设同时有两个线程进行数据更新操作如下图所示。当数据库的数据被更新后如果此时缓存还没有被删除那么缓存中的数据仍然是旧值。如果此时有新的读请求查询数据发生由于缓存中的数据是旧值这个读请求将会获取到旧值。当缓存刚好失效这时有请求来读缓存线程一未命中缓存然后到数据库中读取在要写入缓存时线程二来修改了数据库而线程一写入缓存的是旧的数据导致了数据的不一致。 四、解决办法 当我们在应用中同时使用MySQL和Redis时如何保证两者的数据一致性呢下面就来分享几种实用的解决方案。
4.1 双写一致性 最直接的办法就是在业务代码中同时对MySQL和Redis进行更新。通常我们会先更新MySQL然后再更新Redis。
// 更新MySQL
userMapper.update(user);
// 更新Redis
redisTemplate.opsForValue().set(user_ user.getId(), user);这种方式最大的问题就是在于网络故障或者程序异常的情况下可能会导致MySQL和Redis中的数据不一致。因此我们需要额外的手段来检测和修复数据不一致的情况。
4.2 异步更新(异步通知) 在更新数据库数据时同时发送一个异步通知给Redis让Redis知道数据库数据已经更新需要更新缓存中的数据。这个过程是异步的不会阻塞数据库的更新操作。当Redis收到异步通知后会立即删除缓存中对应的数据确保缓存中没有旧数据。这样即使在这个过程中有新的读请求发生也不会读取到旧数据。等到数据库更新完成后Redis再次从数据库中读取最新的数据并缓存起来。
// 更新MySQL
userMapper.update(user);
// 发送消息
rabbitTemplate.convertAndSend(updateUser, user.getId());/*** 然后在消息消费者中更新Redis。*/
RabbitListener(queues updateUser)
public void updateUser(String userId) {User user userMapper.selectById(userId);redisTemplate.opsForValue().set(redisTemplate.opsForValue().set(user_ user.getId(), user);
}这种异步通知的方式可以确保Redis中的数据与数据库中的数据保持一致避免出现数据不一致的情况。这种方案可以降低数据不一致的风险但仍然无法完全避免。因为消息队列本身也可能因为各种原因丢失消息。
4.3 使用Redis的事务支持 Redis提供了事务Transaction支持可以将一系列的操作作为一个原子操作执行。我们可以利用Redis的事务来实现MySQL和Redis的原子更新。
redisTemplate.execute(new SessioncallbackObject(){0verridepublic Object execute(RedisOperations operations) throws DataAccessException {// 开启事务operations.multi();// 更新MySQLuserMapper.update(user);// 更新Redisoperations.opsForValue().set(user_ user.getId(),user);// 执行事务operations.exec();return null;}
});使用Redis事务可以确保MySQL和Redis的更新在同一事务中执行避免了中间出现不一致的情况。但需要注意的是Redis的事务并非严格的ACID事务可能存在部分成功的情况。
4.4 用 Redisson 实现读锁和写锁 Redisson 是一个基于 Redis 的分布式 Java 对象存储和缓存框架它提供了丰富的功能和 API 来操作 Redis 数据库其中包括了读写锁的支持。读写锁是一种常用的并发控制机制它允许多个线程同时读取共享资源但在写操作时互斥只允许一个线程进行写操作。使用 Redisson 的读写锁方法
获取读锁通过 Redisson 的 RReadWriteLock 对象的 readLock() 方法获取读锁。在获取读锁后可以并发读取共享资源不会阻塞其他获取读锁的线程。获取写锁通过 Redisson 的 RReadWriteLock 对象的 writeLock() 方法获取写锁。在获取写锁后其他获取读锁和写锁的线程将被阻塞只有当前线程能够进行写操作。释放锁使用完读锁或写锁后应该及时调用 unlock() 方法释放锁以便其他线程可以获取锁并进行操作。在 Redisson 中读锁和写锁都继承自锁对象 RLock因此可以使用 RLock 的 unlock() 方法来释放锁。 下面是一个使用 Redisson 读写锁的示例通过 Redisson 的 RReadWriteLock 对象获取读锁和写锁并在需要的代码段中进行相应的操作。执行完操作后使用 unlock() 方法释放锁最后关闭 Redisson 客户端。
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;public class RedissonReadWriteLockExample {public static void main(String[] args) {// 创建 Redisson 客户端Config config new Config();config.useSingleServer().setAddress(redis://127.0.0.1:6379);RedissonClient redisson Redisson.create(config);// 获取读写锁RReadWriteLock rwLock redisson.getReadWriteLock(myLock);RLock readLock rwLock.readLock();RLock writeLock rwLock.writeLock();try {// 获取读锁并进行读操作readLock.lock();// 读取共享资源// 获取写锁并进行写操作writeLock.lock();// 写入共享资源} finally {// 释放锁writeLock.unlock();readLock.unlock();}// 关闭 Redisson 客户端redisson.shutdown();}
}五、结语 综上所述我们提供了更全面的MySQL与Redis数据一致性解决方案。根据具体的业务需求和系统环境选择合适的方案可以提高数据一致性的可靠性。然而每种方案都有其优缺点和适用场景需要综合考虑权衡。 对于并发几率很小的数据(如个人维度的订单数据、用户数据等)这种几乎不用考虑这个问题很少会发生缓存不一致可以给缓存数据加上过期时间每隔一段时间触发读的主动更新即可。就算并发很高如果业务上能容忍短时间的缓存数据不一致(如商品名称商品分类菜单等)缓存加上过期时间依然可以解决大部分业务对于缓存的要求。 把今天最好的表现当作明天最新的起点…..
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/925858.shtml
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!