事情的起因
mysql的四种事务的隔离级别,开发时候mysql默认可重复读(repeatable-read)
所以使用事务行锁,锁定一行记录,可以实现互斥,并发操作这一行记录的请求最终按顺序排队,避免常见的恶意并发多领奖励。
但是合作方的机房DBA,为了可能存现的小概率事件,假如行锁释放出问题,会导致数据库卡死。所以把事务隔离级别调整到,读未提交(read-uncommitted)。
这样确实可以避免互斥,大幅度减少数据库卡死的可能性,但是问题也跟着来了。恶意并发多领奖励。
场景:支付一个订单,发起10个并发领取奖励,10个php进程同时去数据库验证订单是否可以领取,结果全部都是可以领取(虽然启用了事务,但查询操作事务不互斥)。
结果就是很悲剧给恶意玩家发了10份奖励。
机房DBA主动预警了这个问题,但是拒绝调整事务隔离级别。
所以只能开发端改逻辑。
我曾经有一篇文章
对恶意的并发访问进行强制排队按顺序执行
https://blog.youkuaiyun.com/hangbobo/article/details/108646587
和合作方 DBA 沟通,对方对逻辑锁,队列,非常敏感,任务可能会导致更多的问题
反复沟通大致知道了这个DBA的担忧原因。
以前实现逻辑锁 都是用linux的文件锁,磁盘io是系统短板,所以很多老程序员都对逻辑锁畏惧,现在逻辑锁都是基于redis,根本没有io瓶颈问题。效率非常高。
可惜根本改变不了这个DBA的态度,只能另想办法。
最终使用唯一令牌方法,实现方法简单清晰就几行代码,开发修改代码少,不改变原有的业务逻辑。最关键的是合作的DBA能理解,也认可了方案。
具体代码
第一步:订单生成处签发订单唯一令牌
一个令牌就是redis中的一个key
假设订单id 789456
$redis->set('d:789456',1,['nx']);
技术点说明: 一个订单只能签发一个令牌,参数 'nx' 确保只能签发成功一个
第二步:领取订单奖励要获得令牌才能继续
if($redis->getset('d:79458',0)==1){
说明获取令牌成功了继续逻辑
}else{
说明获取令牌失败了 结束请求
}
技术点说明 getset先获取原来的值,再设置一个新数值。整个过程是原子操作不会被其他进程打断。
10个领取奖励的并发进程最终只有一个进程能成功拿到唯一令牌。其余的进程都会被丢弃。
第三步:重置令牌
$this->redis->set('d:79458',1);
如果检验订单支付未成功要重置令牌
第四步:删除令牌
$this->redis->del('d:79458')
领取成功也及时删除令牌
订单恶意领奖励的就防住了。
还有关卡战斗结束结算领奖励,也可能恶意并发多领奖励。
只要战斗开始,用uid和关卡id组合,签发一个唯一令牌.
战斗结束后结算奖励前先获取这个令牌,就可以保证一次战斗只发一份奖励。
问题最终完满解决,在此做个记号。