前言:此篇文章系本人学习过程中记录下来的笔记,里面难免会有不少欠缺的地方,诚心期待大家多多给予指教。
基础篇:
进阶篇:
接上期内容:上期完成了RedisBigKey和MoreKey方面的学习。下面开始学习Redis缓存双写的知识,话不多说,直接发车。
一、引言
在当今高并发、大数据量的应用场景中,缓存技术已经成为提升系统性能和响应速度的关键手段。然而,在使用缓存的过程中,如何保证数据库和缓存之间的数据一致性是一个极具挑战性的问题。缓存双写作为其中一种处理方式,对于理解数据一致性至关重要。本文将探讨缓存双写的相关概念、策略以及如何保证数据库和缓存的一致性。
二、什么是缓存双写?
缓存双写是指在对数据库进行写操作(如插入、更新、删除)时,同时也对缓存进行相应的写操作。其目的是确保数据库和缓存中的数据尽可能保持一致。例如,当应用程序更新了数据库中某条用户记录的信息后,为了让后续的读操作能够从缓存中获取到最新的数据,需要同时更新缓存中对应的用户记录。
缓存双写看似简单直接,但在实际应用中,由于数据库和缓存的读写性能差异、网络延迟、并发操作等因素的影响,要实现完全的一致性并不容易。如果处理不当,可能会导致数据库和缓存中的数据不一致,进而影响到应用程序的正确性。
三、缓存策略
缓存按照操作划分为只读缓存和读写缓存,读写缓存又分为同步直写和异步缓写策略。目的:使redis缓存和数据库数据一致。
(一)、同步直写策略
1、原理
同步直写(Synchronous Write - Through)策略,也叫读写穿透策略。在写操作时,应用程序向数据库发送写请求,数据库接收到请求后,会先将数据写入。当数据库写入成功后,会同步将相同的数据写入缓存中。只有当缓存和数据源都成功写入数据后,才会向应用程序返回写入成功的响应。在读操作时,应用程序先从缓存中读取数据,如果缓存命中,直接返回数据;若缓存未命中,则从数据源中读取数据,然后将数据写入缓存并返回给应用程序。
简单来说:写数据库后也同步写redis缓存,保证缓存和数据库中的数据⼀致;读数据先从缓存读,有返回;无,则从数据库读,在回写到缓存。
2、优劣势
优:
- 数据一致性高:由于每次写操作都同时更新缓存和数据源,能最大程度保证两者的数据一致性,很大程度上避免了数据不一致的情况发生。
- 实现相对简单:该策略的逻辑较为清晰,不需要复杂的异步处理和额外的一致性检查机制,开发和维护相对容易。
劣:
- 性能瓶颈:写操作需要等待缓存和数据源都完成写入后才能返回,这会增加写操作的响应时间。在高并发写操作场景下,可能会导致系统性能下降,因为每次写操作都需要等待,可能会出现大量的请求阻塞。
(二)、异步缓写策略
1、原理
异步缓写(Asynchronous Write-Behind)策略,也叫异步写入缓存策略。在写操作时,应用程序向数据库发送写请求,数据库接收到请求后,会立即将数据写入,并迅速向应用程序返回写入成功的响应,而不会等待缓存的写入完成。然后,程序会以异步(消息队列)的方式将数据写入缓存。在读操作时,与同步直写策略类似,先从缓存中读取数据,如果缓存命中,直接返回数据;若缓存未命中,则从数据源中读取数据,然后将数据写入缓存并返回给应用程序。
简单来说:先将数据写入,在异步将数据写入缓存;读数据先从缓存读,有返回;无,则从数据库读,在回写到缓存。
2、优劣势
优:
- 高写性能:写操作只需要将数据写入数据源,不需要等待缓存写入操作,响应时间短
劣:
- 数据一致性风险:由于数据写入缓存是异步的,可能会出现缓存和底层存储数据不一致的情况。
- 数据库承受风险大:如果在数据还未写入缓存时,缓存为空,过多的请求直接绕过缓存,请求数据库,数据库可能因此宕机。
四、什么是双检加锁?
(一)、概念
双检加锁(Double-Checked Locking)策略常用于多线程环境下,确保在高并发场景下对缓存的操作是线程安全的。
双检加锁策略常用于实现分布式锁,用来保证缓存数据的一致性和高效性,比如在缓存击穿场景中,大量并发请求同时查询一个刚好过期的缓存项,此时多个请求可能会同时去查询数据库并更新缓存,使用双检加锁策略可以避免这种情况。
(二)、原理
1、首先进行第一次检查,判断缓存对象是否已经存在。如果已经存在,直接返回缓存对象,避免不必要的加锁操作,提高性能。
2、如果第一次检查发现缓存对象不存在,则进入同步代码块(加锁)。
3、在同步代码块内,进行第二次检查,再次确认缓存对象是否存在。这是因为在多线程环境下,可能有多个线程同时通过了第一次检查,进入同步代码块之前,其他线程可能已经回写了缓存对象。
4、如果第二次检查发现缓存对象仍然不存在,则从数据库获取数据并回写缓存对象。
(三)、实现
基于双检加锁策略的缓存查询方法的实现。
public String get(String key) {
String value = redis.get(key);// 查询缓存
if (value != null) {
//缓存存在直接返回
return value;
} else {
//缓存不存在则对方法加锁
//假设请求量很大,缓存过期
synchronized (this) {
value = redis.get(key); // 在查一遍redis
if (value == null) {
// 从数据库获取数据
value = dao.get(key);
// 设置过期时间并回写到缓存
redis.setex(key, time, value);
}
return value;
}
}
}
五、数据库和缓存一致性更新策略
(一)、目的
目的:最终实现缓存和数据库的数据一致。
(二)、常用策略
1、先更新数据库,在更新缓存
1.1、问题①
场景模拟一:
- A update myslq 100 --success
- A update redis 100 -- error
最终结果,数据库里面和缓存redis里面数据不一致,下一次读到redis脏数据。
1.2、问题②
场景模拟二:
【正常逻辑】:先更新数据库,再更新缓存,A、B两个线程发起调用
- A update mysql 100
- A update redis 100
- B update mysql 80
- B update redis 80
最终结果,mysql和redis数据一致,皆大欢喜。
【异常逻辑】:多线程环境下,A、B两个线程有快有慢,有前有后并行。
- A update mysql 100
- B update mysql 80
- B update redis 80
- A update redis 100
最终结果,mysql和redis数据不一致,o(╥﹏╥)o。
2、先更新缓存,在更新数据库
出现的异常问题跟第一种策略类似。此外,业内一般把数据库作为底单数据库,保证最后解释,一切都以数据库为主。
3、先更新数据库,在删除缓存
3.1、异常问题
先更新数据库,再删除缓存场景模拟

最低0.47元/天 解锁文章
1673

被折叠的 条评论
为什么被折叠?



