缓存一致性坑【done】
1 写db-delete redis
保可用性,这个方案不行,必须加入双删去扰动并发

保一致性(也不行,必须加select数据库锁与隔壁update互斥,redis分布式锁不行)

| 线程1 | 线程2 查询 | 线程3 | 线程2 redis包裹在数据库锁 | 线程3 redis包裹在数据库锁 | |
|
update db commit |
|||||
| delete redis |
|
||||
| query db 可能旧值 | update db |
情况1(查询先于线程3): 开启只读事务,加锁 使线程3阻塞 set redis(旧值包裹在锁里) 结束事务,线程3获得锁 |
update db | ||
| commit | 情况2:
开启只读事务,加锁,阻塞等待线程3提交 线程3commit,线程2获得锁并查询到新值 |
delete redis commit 使线程2的set redis永远比线程3的delete晚 |
|||
| 卡网或jvm停顿 | delete redis | ||||
| set redis | |||||
| 延时再删 |
坑!
根本困难:
1 update db与delete redis不是原子(没有数据一致性损失,被人插一脚进来没事)
2 query db与set redis不是原子(select无锁下有数据一致性损失),select与set之间,新值诞生
不能有下情况:
| 2 | 1 |
| query | |
| update | |
| delete | |
| set |
不加锁
| select | 第二条update |
| 发现null,查库,查到旧值 | update |
| 提交,delete redis | |
| set redis 旧值 | |
|
因为查和写非原子,被人插一脚进来 所以2必须原子 |
加锁
1)2原子1不原子
2先于1
| 2 select | 1 第二条update |
| 发现null,查库,加锁成功 | update阻塞 |
| set redis,提交事务 | |
| 获得锁,提交,delete redis | |
| 脏数据被delete,good |
2晚于1
| 2 select | 1 第二条update |
| 发现null,查库,加锁,阻塞 | update 获取锁 |
| 提交 | |
| 获得锁 set redis 提交事务 | delete redis(在锁外) |
| 无论delete与set的先后,都不会有脏数据 |
2)2原子1原子
2先于(同1)
| 2 select | 1 第二条update |
| 发现null,查库,加锁成功 | update阻塞 |
| set redis,提交事务 | |
| 获得锁,提交,delete redis | |
| 脏数据被delete,good |
2晚于1
| 2 select | 1 第二条update |
| 发现null,查库,加锁,阻塞 | update 获取锁 |
| delete redis,提交 | |
| 获得锁 set redis 提交事务 | |
| 与1)相比,性能增加了 |
核心解决方案:
使 1 与 2 在关系到某一条记录时,让他们2原子且有序
结论:
强一致性-倒不如写完db直接写redis,开事务使set redis有序,同时承担commit本身分布式事务不一致风险,commit异常时删除redis
强一致性(只要数据更新了db,隔壁一定要读取到新值)
分布式锁防止多个线程打到数据库,select加数据库锁开启只读事务且路由到主库,set redis包裹在事务中
原则上数据库锁事务里不应该有redis或mq的io
弱一致性
1)延迟再删扰动并发,但不是100%的,虽然可能性很小
双删的本质,增加扰动减小并发失误概率,尤其在读者分离场景,并以持续的双删推动缓存持续更新,不一定要一步到位更新到最新值,但最大程度避免旧值停滞,双删适用时间轮
| 2 | 1 |
| query | |
| update | |
| delete | |
| 它还就卡了2秒,比如等连接池 | 等2s再次delete |
| set |
那么读写分离的db场景呢?
| 2 | 1 | 另一个2 | db集群 |
| query 从 | |||
| update 主 | |||
| delete | |||
| set | 等2s再次delete | 就看2秒主到从同步有没有完成 | |
| delete | query 从 | ||
| set 旧值 |
弱一致性方案仍然存在一定可能出现缓存值停滞,应优先使用canal
2) canal,订阅主或从库binlog
db A操作早于B,然而刷缓存操作A晚于B;同时多线程下消息有序性几乎是不可能的
刷db时主线程可以强行刷redis,但是需要结合业务保护有序性,但是很难 除非分布式锁(比如db事务)
即使利用update本身加锁性质,从而在commit前发送mq,但仍然不能保证mq投递的有序性(比如网络抖动),除非在producer端业务层确保有序,比如commit前生成递增no再组织投递,这个操作可以被信任(因为没有网络)
然后投递到同一个partition,消费者检验no,如果下一个no比上一个还小,则丢弃(视具体业务);如果顺序则刷redis;
这个方案其实没有意义,如果我都能确保多线程mq有序性了,直接写redis不是更好吗,属于脱裤子放屁
浙公网安备 33010602011771号