网站首页 > 技术文章 正文
什么是双写一致性?
双写一致性的问题,是指在分布式系统中当数据要被写入到多个不同的存储系统中的时候可能会因为多种原因导致这些存储系统之间数据存储的不一致的问题。这种场景一般常见于缓存以及数据库写入的双写场景,也就是说,在更新数据库数据的同时,也需要更新数据缓存中的数据。
如果写入数据库的操作和写入缓存的操作不是原子性的也就是说无法同时成功或者是失败。那么就有可能会出现数据不一致的情况。如下所示,数据库更新成功,但缓存更新失败,导致缓存中数据是旧的,当应用程序从缓存读取数据的时候,就会获取到过期的数据。相反,缓存更新成功,但数据库更新失败,这种情况就会导致,缓存中的数据是新的,但数据库中的数据却是旧的这种情况下,就有可能导致读请求数据和写请求数据的不一致。
导致双写不一致的原因
- 操作的顺序问题:当更新缓存和数据库的顺序不一致的时候,就有可能会导致某一部分的数据是旧的,另一部分是新的。
- 网络延迟或故障:当缓存或数据库的更新请求因为网络问题而延迟或失败的时候,也会导致两部分数据的不一致性。
- 缓存过期:缓存的数据可能在更新之前过期,从而导致数据不一致。
- 并发写问题:多个并发操作同时修改数据库和缓存,可能会导致数据不一致。
在Spring Boot项目中如何解决双写一致性问题?
在SpringBoot项目中,多种方式来解决缓存和数据库数据双写一致性的问题,下面我们就来总结一下一般情况下常用的几种方式。
缓存延时失效策略
这种策略的核心思想是,只更新数据库,不更新缓存。当数据库中的数据发生变更时,将缓存中的数据设置为失效,下一次读取该数据时,强制从数据库中读取最新数据,并重新加载缓存。这种策略牺牲了写性能,但可以确保读性能和数据一致性。
如下所示,实现当数据更新的时候,仅更新数据库,然后删除缓存中的Key,进行数据获取的时候,如果缓存中查不到数据,那么就从数据库中读取到数据,并且将数据更新到缓存中。
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
@Autowired
private RedisTemplate<String, User> redisTemplate;
private static final String USER_CACHE_KEY = "user::";
public User updateUser(User user) {
// 更新数据库
userRepository.save(user);
// 删除缓存
redisTemplate.delete(USER_CACHE_KEY + user.getId());
return user;
}
public User getUserById(Long userId) {
String cacheKey = USER_CACHE_KEY + userId;
// 从缓存中获取
User cachedUser = redisTemplate.opsForValue().get(cacheKey);
if (cachedUser != null) {
return cachedUser;
}
// 缓存中没有,从数据库中查询
User dbUser = userRepository.findById(userId).orElse(null);
if (dbUser != null) {
// 将结果放入缓存
redisTemplate.opsForValue().set(cacheKey, dbUser);
}
return dbUser;
}
}
这种方式易于实现,可以有效保证数据的最终一致性,但是存在的问题就是,如果缓存删除失败了,那么就还是会导致旧的数据留在缓存中,这个时候,当缓存过了失效时间,那么就有可能会导致缓存雪崩的问题。
先更新数据库,再异步更新缓存
这种策略的核心思想是,先更新数据库,再通过异步机制去更新缓存。异步更新的好处是,即使缓存更新失败,也不会影响数据库的写操作,可以通过补偿机制重新更新缓存。
如下所示,当数据更新的时候,我们先去更新数据库中的数据,然后当数据更新完成之后,通过异步的操作将缓存中的数据也进行更新。
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
@Autowired
private RedisTemplate<String, User> redisTemplate;
private static final String USER_CACHE_KEY = "user::";
@Async
public void updateCache(User user) {
String cacheKey = USER_CACHE_KEY + user.getId();
redisTemplate.opsForValue().set(cacheKey, user);
}
public User updateUser(User user) {
// 更新数据库
userRepository.save(user);
// 异步更新缓存
updateCache(user);
return user;
}
}
这种方式的优点就是降低了写操作的延迟,提升了写操作的性能,然后通过异步更新缓存的操作来保证缓存中的数据是最新的数据。但是这种情况就可能会在异步更新失败之后,可能需要额外的机制来进行重试,这种机制在一个很短的时间内会导致缓存和数据库的数据不一致的情况。所以需要额外的补偿机制来处理这种情况。
使用消息队列实现缓存更新
使用消息队列可以保证更新缓存和数据库的解耦,通过消息异步通知更新缓存。数据库操作成功后,向消息队列发送缓存更新消息,消费者(监听者)收到消息后更新缓存。
如下所示,当数据库的更新操作完成之后,那么就会将更新操作的信息发送到消息队列中,当消息队列中的消费者监听到了这个消息之后,就会对对应的缓存进行更新,这与异步更新的操作是类似的。
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
@Autowired
private RabbitTemplate rabbitTemplate;
public User updateUser(User user) {
// 更新数据库
userRepository.save(user);
// 发送缓存更新消息
rabbitTemplate.convertAndSend("cacheUpdateQueue", user);
return user;
}
}
@Component
public class CacheUpdateListener {
@Autowired
private RedisTemplate<String, User> redisTemplate;
private static final String USER_CACHE_KEY = "user::";
@RabbitListener(queues = "cacheUpdateQueue")
public void updateCache(User user) {
String cacheKey = USER_CACHE_KEY + user.getId();
redisTemplate.opsForValue().set(cacheKey, user);
}
}
这种情况下,数据库和缓存的更新都是通过消息队列进行异步的处理,解耦了数据库和缓存的操作,这样有效的避免了双写不一致的问题。但是也是引入了消息队列系统,会出现短暂的数据延迟了数据不一致的情况。
使用分布式事务
为了解决严格一致性问题,可以使用分布式事务中的两阶段提交协议(2PC)三阶段提交、以及其他的一些分布式事务处理机制。但是引入分布式事务处理机制,可能会带来较大的性能开销,且增加了系统的复杂性。这种情况下,虽然保证了数据的一致性,但是性能较差,适合使用在一些对一致性要求较高的场景中使用,不适合在高并发场景中使用。
总结
在Spring Boot项目中,缓存和数据库的双写一致性问题有多种解决方案。常见的解决方式包括缓存延时失效、异步更新缓存、消息队列解耦、甚至使用分布式事务。选择何种方案取决于具体的业务场景和对数据一致性、性能的要求。
- 上一篇: 什么是分布式系统的一致性?
- 下一篇: 互联网面试-在高并发场景下如何保证缓存和数据库的最终一致性?
猜你喜欢
- 2024-11-30 分布式系统最全详解(图文全面总结)
- 2024-11-30 架构设计最全详解(万字图文总结)
- 2024-11-30 最佳实践:确保MySQL和Redis数据一致性的方法
- 2024-11-30 面试京东实习,如何保障三个数据库的数据一致性?
- 2024-11-30 无锁编程——从CPU缓存一致性讲到内存模型
- 2024-11-30 分布式系统一致性保障:CAP理论与BASE原则
- 2024-11-30 如何保证redis缓存与数据库数据一致性
- 2024-11-30 分库分表最全详解(图文全面总结)
- 2024-11-30 微服务架构数据一致性的解决思路
- 2024-11-30 如何确保高并发秒杀场景下Redis与数据库库存的一致性
你 发表评论:
欢迎- 最近发表
- 标签列表
-
- oraclesql优化 (66)
- 类的加载机制 (75)
- feignclient (62)
- 一致性hash算法 (71)
- dockfile (66)
- 锁机制 (57)
- javaresponse (60)
- 查看hive版本 (59)
- phpworkerman (57)
- spark算子 (58)
- vue双向绑定的原理 (68)
- springbootget请求 (58)
- docker网络三种模式 (67)
- spring控制反转 (71)
- data:image/jpeg (69)
- base64 (69)
- java分页 (64)
- kibanadocker (60)
- qabstracttablemodel (62)
- java生成pdf文件 (69)
- deletelater (62)
- com.aspose.words (58)
- android.mk (62)
- qopengl (73)
- epoch_millis (61)
本文暂时没有评论,来添加一个吧(●'◡'●)