今天遇到一个线上问题,很有意思,也很难得。之前就有学习到一致性的解决问题,今天终于来了一个上线案例了,好在并发场景并不得多。~~
场景:
拼车子单再确认接单的过程中,会生成对应司机的形成单号,并最终落库。落库的时候,用到了Redis实现的延迟双删,保证缓存与数据库保持一致;另一个线程操作表中相同的记录行,记录该订单免抽佣。两个线程共用一把分布式锁。按道理看,一个线程执行完,保存数据,释放锁,另一个线程才会操作相同数据行,没什么问题。问题是还有一个线程3,简单画个图。
线程\时间轴(格子不能表示时间长短,只能表示时间顺序) | A | B | C | D | E | F | G | H | I | J | K | L | M |
线程1 | 查缓存(无) | 查库(旧) | 存缓存(旧) | 返回数据 | |||||||||
线程2 | 获取分布式锁 | 查库(旧) | 逻辑处理 | 提交事务(新1) | 删除缓存 | 释放分布式锁 | 延迟删除缓存(200ms) | ||||||
线程3 | 获取分布式锁 | 查缓存(旧) | 业务处理 | 提交事务(新2) | 删除缓存 | 释放分布式锁 | 延迟删除缓存(200ms) |
上述三个线程并发,主要问题是线程2更细数据库,并删除缓存后,线程1把旧的数据保存到缓存。导致线程3用旧的数据处理业务逻辑并保存到了数据库。
简单分析:
线程2和线程3共享分布式锁,所以相互隔离,没有问题。问题出现在线程1的F列持有旧的缓存,更新进了Redis。在线程2延迟删除缓存没操作时,被线程3用到了缓存数据。多个线程并发操作一行数据的修改和缓存的存储就会有这个问题。
解决:
1.线程3查缓存(旧)改成查数据库。虽然并发问题是不可避免的,但是数据库事务为我们做了这些,考虑到每条订单记录修改量不大可以直接查库。
2.考虑到项目处理订单数据都是状态机processor中处理,也就是修改数据过程中都是持有分布式锁的。那么可以考虑释放分布式锁之前,手动把最新的数据放入缓存。
总结
遇到问题还是要具体分析的,每一种解决方案都不是完美的,但每种解决方案都有最适合他的场景。上面我们选用的方案1。其中还可以考虑延迟删除缓存的时间,我们业务设置的200ms,这种想法只能缩小并发问题,不能根本上解决。
小伙伴可以多多评论,给我打开更多的思路,相互学习。