【架构】缓存与数据库:双写一致性、缓存问题

这里我讲的是缓存和数据库,以redis和mysql举例,实际上缓存包括不限于浏览器缓存、redis、memcache、本地缓存guava等等,数据库也有很多种,这里我们仅仅以较常见的redis和mysql举例。

一,缓存与数据库的查询、写入

1,双写一致性

在以数据库作为查询终点的项目里,一般在查询的时候,使用被动更新,即查询不到数据,从数据库获取数据,将其更新到缓存中。而在进行写操作的时候,如果只是依靠缓存的过期,会存在比较大的一致性问题,因此需要使用主动更新,即需要主动对缓存进行更新。而主动更新,存在双写一致性问题,下面是4种主动更新的方案。

更新缓存、更新数据库
基本上更新缓存的方案都存在一致性的问题,虽然不一致的原因不一样,但是结果都是数据一致性问题比较严重。比如这个先更新缓存,再更新数据库。如果更新数据库失败,直到数据库回滚后,将缓存数据更新为旧值之前,缓存中被更新的值都存在一致性问题,数据库写操作和回滚比较耗时,这期间缓存数据一直是不一致的,因此这个方案一般不采用。

更新数据库、更新缓存
这个方案的一致性问题是因为多线程导致的。假设a线程更新数据库后,还没有更新缓存,此时b线程更新了数据库与缓存,最后a线程更新了缓存。b线程后更新的数据库,但是最后缓存里的数据是先更新的a线程的数据,如果后面再有新的线程来查询,并且读多写少,那么数据会在相当一段时间内存在一致性问题。

删除缓存、更新数据库
这个方案相比“更新缓存、更新数据库”,即使a线程更新数据库失败了,也不会存在一致性问题,因为b线程查询时缓存没有数据会从数据库中去读数据,读到的快照都是更新成功的数据,a线程更新失败的数据并不会被读到。

这个方案的问题是,更新数据库相比查询数据库(mysql更新基于LBCC,需要加锁锁定;查询在可重复读和读已提交是基于MVCC,是非锁定的快照读,读的undolog,往往都很快)往往是比较

### 缓存更新策略一致性解决方案 #### Cache Aside Pattern 一种常见的缓存更新策略是 **Cache Aside Pattern**,即缓存在更新时由调用方负责,在完成数据库更新之后再同步更新缓存[^1]。这种方式被称为方案,因为每次数据变更都需要同时修改数据库缓存。 #### 数据一致性的本质问题 当涉及机制时,不可避免会出现数据不一致的情况。这是因为缓存数据库之间的操作并非原子化执行,可能存在时间窗口使得两者状态不同步[^3]。 #### 设置过期时间和最终一致性 为了缓解这一问题,可以为缓存中的数据设定合理的过期时间。即使缓存更新失败,一旦到达预设的过期期限,后续读取请求将自动从数据库获取最新的值并重新填充到缓存中,从而逐步达成最终的一致性[^5]。 #### 主流解决方案及其适用场景 针对一致性问题,目前有四种主要的解决方案: 1. **强一致性锁机制** 使用分布式锁来确保同一时刻只有一个线程能够对某个键进行操作,无论是更新数据库还是刷新缓存都严格串行处理,以此保障实时一致性[^4]。 2. **异步消息队列驱动** 借助消息中间件(如Kafka、RabbitMQ),在数据库记录发生变化后发送事件通知给消费者服务,后者负责及时更新对应的缓存条目。 3. **基于版本号控制** 在每一条存储于数据库内的实体对象上增加一个`version`字段,每当发生改动便递增其数值;而关联至该资源的缓存则需携带相同的版本标识符,只有匹配成功才允许返回命中结果。 4. **TTL (Time-To-Live)** 方法 如前所述,利用短暂的有效期让陈旧副本自然淘汰掉,并依赖下一次查询触发加载新内容的过程重建临时高速缓冲层。 #### 生产环境下的最佳实践建议 综合考虑以上方法以及具体应用场景的要求,推荐采取如下措施以优化整体架构设计: - 对热数据采用较短生命周期管理; - 关键交易环节引入显式的事务边界界定; - 结合业务逻辑特性灵活选用适合的技术手段组合应对挑战。 ```python import redis from sqlalchemy import create_engine, update def db_update_and_cache_refresh(key, value): engine = create_engine('mysql+pymysql://user:password@host/dbname') with engine.connect() as conn: stmt = update(Table).where(Table.id == key).values(data=value) result = conn.execute(stmt) r = redis.Redis(host='localhost', port=6379, decode_responses=True) try: r.setex(name=key, time=300, value=value) # Set cache with TTL of 5 minutes except Exception as e: print(f"Failed to refresh cache due to {e}") ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值