ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

MySQL与Redis数据一致性及分布式锁实战解析

MySQL与Redis数据一致性及分布式锁实战解析

1. MySQL与Redis数据一致性深度解析

在分布式系统中,数据库与缓存的数据一致性问题是每个开发者都必须面对的挑战。我经历过多次因缓存与数据库不一致导致的线上事故,今天就来系统梳理这个经典问题的解决方案。

1.1 缓存与数据库不一致的本质原因

先看一个典型场景:用户下单后,订单数据写入MySQL,同时需要更新Redis中的库存缓存。如果这两个操作不是原子性的,就可能出现:

  1. 先更新数据库成功,但更新缓存失败
  2. 先删除缓存成功,但数据库更新失败
  3. 并发请求导致缓存与数据库写入顺序错乱

这种不一致在以下情况会被放大:

  • 高并发场景(如秒杀活动)
  • 网络波动导致操作失败
  • 服务重启或故障转移

1.2 主流解决方案对比

方案1:Cache Aside Pattern(旁路缓存)

这是最常用的模式,核心逻辑:

def get_data(key): # 1. 先查缓存 data = redis.get(key) if not data: # 2. 缓存不存在则查数据库 data = db.query("SELECT * FROM table WHERE id=?", [key]) # 3. 写入缓存 redis.setex(key, 3600, data) return data def update_data(key, value): # 1. 先更新数据库 db.execute("UPDATE table SET value=? WHERE id=?", [value, key]) # 2. 再删除缓存 redis.delete(key)

关键经验:删除缓存而非更新,避免并发写导致的脏数据。但可能引发"先更新DB后删缓存失败"的问题。

方案2:Write Through(直写)

通过中间件(如阿里云的Tair)自动同步写操作到数据库和缓存。优点是强一致,但性能损耗大,适合写少读多的场景。

方案3:延迟双删

针对高并发场景的优化方案:

def update_data(key, value): # 第一次删除 redis.delete(key) # 更新数据库 db.update(...) # 延迟再次删除 threading.Timer(0.5, lambda: redis.delete(key)).start()

通过二次删除解决并发请求导致的脏数据问题。

1.3 实战中的注意事项

  1. 重试机制:缓存操作失败时建议采用指数退避重试
def safe_delete(key, retries=3): for i in range(retries): try: redis.delete(key) break except Exception as e: wait = (2 ** i) * 0.1 time.sleep(wait)
  1. 缓存过期时间:建议设置随机TTL避免缓存雪崩
import random ttl = 3600 + random.randint(0, 300) # 3600~3900秒随机过期
  1. 监控指标:必须监控的关键指标
  • 缓存命中率(正常应>90%)
  • 数据库QPS突增(可能缓存大面积失效)
  • 缓存操作失败率

2. Redis与ZooKeeper分布式锁实现对比

分布式锁是解决并发问题的另一利器,但Redis和ZooKeeper的实现机制截然不同。

2.1 Redis分布式锁实现

基础版(SETNX)
SET lock_key unique_value NX PX 30000
  • NX表示key不存在时才设置
  • PX设置过期时间(毫秒)
  • unique_value用于安全释放锁(防止误删)
红锁算法(Redlock)

应对Redis单点故障的方案,需要至少3个独立节点:

  1. 获取当前时间(毫秒)
  2. 依次向所有节点申请锁
  3. 当获得多数节点(N/2+1)的锁时才算成功
  4. 锁的有效时间 = 原始有效时间 - 获取锁耗时

坑点警示:系统时钟漂移可能导致锁提前失效

2.2 ZooKeeper分布式锁实现

临时顺序节点方案
// 创建临时顺序节点 String lockPath = zk.create("/locks/resource_", null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL); // 获取所有子节点 List<String> children = zk.getChildren("/locks", false); // 判断自己是否是最小节点 if (isLowestNode(children, lockPath)) { // 获取锁成功 } else { // 监听前一个节点 waitForPreviousNode(children, lockPath); }
对比Redis实现的核心差异:
  1. 自动释放:会话结束临时节点自动删除
  2. 排队机制:顺序节点天然支持公平锁
  3. 无过期时间:避免时钟问题

2.3 关键场景对比测试

我们在压测环境中对比了两种方案(100并发,锁持有时间100ms):

指标Redis(单节点)Redis(Redlock)ZooKeeper
获取锁平均耗时(ms)2.315.78.2
死锁发生率0.12%0%0%
网络分区容忍度低中高
吞吐量(ops/s)450012001800

选型建议:

  • 追求性能:Redis单节点(需做好容灾)
  • 强一致性:ZooKeeper
  • 折中方案:Redlock

3. 生产环境中的血泪教训

3.1 Redis锁的典型踩坑记录

案例1:未设置唯一标识导致锁误删

# 错误示范 def release_lock(key): redis.delete(key) # 可能删除其他线程的锁 # 正确做法 def release_lock(key, identifier): script = """ if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end """ redis.eval(script, 1, key, identifier)

案例2:未考虑业务执行时间超过锁过期时间

# 错误:锁过期时间小于业务执行时间 redis.set("lock", "1", nx=True, px=10000) # 10秒过期 long_running_task() # 可能执行15秒 # 解决方案:自动续期 def renew_lock(): while holding_lock: redis.pexpire("lock", 10000) time.sleep(5000) # 每5秒续期一次

3.2 ZooKeeper的注意事项

  1. watch数量限制:单个znode的watch数量过多会影响性能
  2. 连接状态处理:SESSION_EXPIRED事件必须正确处理
  3. 脑裂问题:虽然罕见但仍需考虑,建议配合监控

4. 终极方案选型指南

经过多年实践,我的推荐方案是:

  1. 数据一致性:

    • 最终一致性场景:Cache Aside + 异步补偿
    • 强一致性需求:Write Through + 事务消息
  2. 分布式锁:

    • 短期锁(<1s):Redis单节点
    • 长期锁:ZooKeeper
    • 跨地域部署:Redlock + 时钟同步
  3. 监控体系:

    • Redis:slowlog监控、内存碎片率
    • ZooKeeper:watch数量、znode大小
    • 通用:锁等待时间、死锁检测

最后分享一个真实案例:某电商平台大促时,因Redis锁竞争导致下单接口超时。最终通过以下优化解决:

  • 锁粒度细化(从商品级到SKU级)
  • 引入本地缓存减少锁竞争
  • 添加锁等待超时机制(避免无限等待)

这些经验都是用真金白银的线上事故换来的,希望你能避开这些坑。记住:没有银弹,只有最适合业务场景的方案。

返回列表