Redis 之缓存和DB一致性解决

本文探讨了缓存数据一致性问题,包括双写模式和失效模式下的脏数据产生原因及解决方案,如加锁、设置过期时间等,适用于经常读偶尔写的场景。

缓存一致性问题

在日常开发中,为了提高数据响应速度,可能会将一些热点数据保存在缓存中,这样就不用每次都去数据库中查询了,可以有效提高服务端的响应速度,那么目前我们最常使用的缓存就是 Redis 了。

考虑一个问题,如果数据库修改了一个数据,而这个数据缓存中也有,这时再从缓存中拿数据,就是一条旧数据。

缓存数据一致性模式

双写模式

先更新数据库,再更新缓存。
1、A 线程更新数据库值为 1。
2、B 线程更新数据库值为 2。
3、由于卡顿,B 线程先更新了缓存值为 2。
4、最后 A 线程更新了缓存值为 1。
此时,缓存中保存的数据就是一个旧的脏数据了。
在这里插入图片描述
解决方案一:
加锁,产生并发写的时候,因为要更新数据库,同时要更新缓存,那么就可以对整个操作加一个锁,保证更新数据库和更新缓存是一个原子性。
这样就可以避免脏数据了。

解决方案二:
对缓存设置一个过期时间,过期时间一到,就会得到最新的数据,只是会出现暂时的数据不一致,最终会一致性的。
就要看业务是否允许这种情况,允许的情况下,容忍是多大。

失效模式

先更新数据库,再淘汰缓存。
A、B 线程更新数据库,再删除缓存。
C 线程读取数据库,在更新缓存。

1、A 线程更新数据库值为 1,删除缓存,A 执行完。
2、B 线程更新数据库值为 2,但是由于机器慢,花的的时间比较长。
3、这时 C 线程读取数据,从缓存读取不到,再从 DB 读取数据,这时读取到值为 1。
4、这时 B 线程执行完了,将数据值更新为 2,并且删除缓存。
5、最后 C 线程更新缓存值,因为 C 读取到的是 A 线程更新的数据,将旧数据更新到了缓存了。
此时,缓存中保存的数据就是一个旧的脏数据了。
在这里插入图片描述
解决方案:
上面的问题,其实就是写和读的并发问题,可以通过加读写锁进行解决问题。
对更新数据加写锁,对读数据加读锁。写读、读写、写写互斥,读读不互斥。
但是不管怎么样,只要用了锁,系统性能肯定会变低,但保证了数据的一致性,需要进行取舍。

对于经常修改的数据,与其进行加锁用缓存,还不如直接读数据库。
对于经常读,偶尔写的情况下,其实用缓存也就基本不会出现脏数据,因为并发写的情况基本不会出现,如果怕以防万一,可以用读写锁。因为读写锁适用于经常读的情况。

缓存数据一致性-解决方案

无论是双写模式还是失效模式,都会导致缓存的不一致问题。即多个实例同时更新会出事。怎么办?

  • 1、如果是用户纬度数据(订单数据、用户数据),这种并发几率非常小,不用考虑这个问题,缓存数据加 上过期时间,每隔一段时间触发读的主动更新即可。
  • 2、如果是菜单,商品介绍等基础数据,也可以去使用canal订阅binlog的方式。
  • 3、缓存数据+过期时间也足够解决大部分业务对于缓存的要求。
  • 4、通过加锁保证并发读写,写写的时候按顺序排好队。读读无所谓。所以适合使用读写锁。(业务不关心 脏数据,允许临时脏数据可忽略);

总结:

  • 我们能放入缓存的数据本就不应该是实时性、一致性要求超高的。所以缓存数据的时候加上过期时间,保 证每天拿到当前最新数据即可。
  • 我们不应该过度设计,增加系统的复杂性。
  • 遇到实时性、一致性要求高的数据,就应该查数据库,即使慢点。

在这里插入图片描述

总结系统的一致性解决方案

1、缓存的所有数据都有过期时间,数据过期下一次查询触发主动更新。
2、读写数据的时候,加上分布式的读写锁。 经常写(会有影响)。很少写,经常读(不会有影响)。

### 三级标题:Redis 缓存与数据库双写一致性解决方案 在高并发系统中,为了提高性能,通常会使用 Redis 作为缓存来减少对数据库的直接访问。然而,引入缓存后,如何保证缓存数据库之间的数据一致性成为了一个关键问题。以下是几种常见的解决方案,用于解决 Redis 缓存与数据库双写一致性问题。 #### 三级标题:更新数据库 + 更新缓存 在更新数据库的同时更新缓存,这种方法看似简单直接,但在并发情况下无法保证缓存数据库的一致性。此外,如果缓存更新失败,会导致缓存中的数据与数据库中的数据不一致。因此,这种方法并不推荐在高并发场景下使用。 #### 三级标题:更新数据库 + 删除缓存 更新数据库后删除缓存,这种方法可以确保在下次读取时从数据库中获取最新的数据并重新填充缓存。然而,在并发情况下,如果在删除缓存更新数据库之间有其他请求读取缓存,可能会导致读取到旧数据。为了解决这个问题,可以采用延迟双删策略,即在更新数据库后立即删除缓存,并在一段时间后再删除一次,以确保所有可能的旧数据都被清除。 #### 三级标题:先更新数据库,再删除缓存 为了保证两步都成功执行,可以配合消息队列或订阅变更日志的方案来做。通过消息队列将更新数据库删除缓存的操作异步化,并通过重试机制保证数据一致性。这种方法的本质是通过重试的方式保证数据一致性。 #### 三级标题:使用读写锁 对于并发几率很小的数据,可以通过加读写锁来保证并发读写或写写的时候按顺序排好队。读读的时候相当于无锁。这种方法可以有效避免数据不一致问题,但会降低系统性能。 #### 三级标题:监听数据库变更日志 通过监听数据库的 binlog 日志及时去修改缓存,例如使用阿里开源的 Cana,这种方法可以实时更新缓存,但引入了新的中间件,增加了系统的复杂度。 ### 三级标题:代码示例 以下是一个简单的 Python 示例,展示如何使用 Redis 数据库进行双写操作: ```python import redis import mysql.connector # 连接数据库 db = mysql.connector.connect( host="localhost", user="yourusername", password="yourpassword", database="yourdatabase" ) # 连接 Redis r = redis.Redis(host='localhost', port=6379, db=0) def update_data(data_id, new_value): cursor = db.cursor() try: # 更新数据库 cursor.execute("UPDATE yourtable SET value = %s WHERE id = %s", (new_value, data_id)) db.commit() # 删除缓存 r.delete(f"data:{data_id}") except Exception as e: db.rollback() print(f"Error: {e}") finally: cursor.close() def get_data(data_id): # 从缓存中获取数据 cached_data = r.get(f"data:{data_id}") if cached_data: return cached_data else: # 从数据库中获取数据 cursor = db.cursor() cursor.execute("SELECT value FROM yourtable WHERE id = %s", (data_id,)) result = cursor.fetchone() cursor.close() if result: # 将数据写入缓存 r.setex(f"data:{data_id}", 3600, result[0]) return result[0] else: return None ``` ###
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值