最近探索了关于抢红包的一些实现思路,在此记录下
本篇文章主要探讨了
金额随机算法
关于分布式锁的一些探索
我们开始,首先说下抢红包的业务流程,分为两步,发红包和抢红包,在抢红包的时候,并发是非常大的,我们这里采用redis作为中间件来实现高并发的抢红包流程
关于红包的随机生成算法,我们采用“二倍均值法”来生成随机的红包金额并先存下来
此算法的核心思想是根据每次剩余的总金额M和剩余人数N,执行M/N再乘以2的操作得到一个边界值E,然后指定一个从0到E的随机区间,在这个随机区间内将产生一个随机金额R,此时总金额M将更新为M-R,剩余人数N更新为N-1。再继续重复上述执行流程,以此类推,直至最终剩余人数N-1为0,即代表随机数已经产生完毕,剩余金额即为最后一个随机金额。
此算法实现如下
/* * @param $totalAmount 总金额分 * @param $userNum 总人数 */ public static function devidePackage($totalAmount, $userNum) { $resList = []; $restAmount = $totalAmount; $restUserNum = $userNum; for ($i = 0; $i < $userNum - 1; $i++) { $mount = mt_rand(0, $restAmount / $restUserNum * 2 - 1) + 1; $restUserNum--; $restAmount -= $mount; $resList[] = $mount; } $resList[] = $restAmount; return $resList; }
在发红包的时候,我们需要生成一个红包的唯一id
/* * 发红包 */ public function actionHandout() { Yii::$app->response->format = Response::FORMAT_JSON; $totalAmount = Yii::$app->request->post('totalAmount',''); $userNum = Yii::$app->request->post('userNum',''); // 生成随机金额 $packageDevideRs = RedPackageUtils::devidePackage($totalAmount, $userNum); // 生成红包唯一id $redId = RedPackageUtils::getUuid(); // 记录到缓存 $this->redis->lpush($redId, ...$packageDevideRs); // 剩下的业务代码(用户金额变动,发红包操作记录等) ... ... return ['code' => 200, 'msg' => '红包已经发送']; }
此时redis中的数据如下,现在已经生成了随机的金额
抢红包的实现逻辑
/* * 抢红包 */ public function actionRob() { Yii::$app->response->format = Response::FORMAT_JSON; $redPackageId = Yii::$app->request->post('packageid', ''); if (!$this->isRemain($redPackageId)) { return ['code' => 0, 'msg' => '红包已被抢光']; } // 抢到的红包 $robMoney = $this->redis->lpop($redPackageId); Yii::info("当前用户抢到了红包,抢到了" . $robMoney,'ROB'); return ['code' => 200, 'msg' => "当前用户抢到了红包,抢到了" . $robMoney]; } /* * 检测红包是否大于0,即红包是否被抢光 */ public function isRemain($redPackageId) { $len = $this->redis->llen($redPackageId); if ($len > 0) { return true; } return false; }
注意,这里的lpop是多线程安全的,因为redis是单线程的,即使请求到了也会排队
调用之后,运行结果如下
boht@Mac runtime % tail -f log-2020-11-08.log| grep ROB 2020-11-08 12:20:54 [127.0.0.1][-][-][info][ROB] 当前用户抢到了红包,抢到了1 2020-11-08 12:20:54 [127.0.0.1][-][-][info][ROB] 当前用户抢到了红包,抢到了8 2020-11-08 12:20:54 [127.0.0.1][-][-][info][ROB] 当前用户抢到了红包,抢到了9 2020-11-08 12:20:54 [127.0.0.1][-][-][info][ROB] 当前用户抢到了红包,抢到了16 2020-11-08 12:20:54 [127.0.0.1][-][-][info][ROB] 当前用户抢到了红包,抢到了14 2020-11-08 12:20:54 [127.0.0.1][-][-][info][ROB] 当前用户抢到了红包,抢到了14 2020-11-08 12:20:54 [127.0.0.1][-][-][info][ROB] 当前用户抢到了红包,抢到了24 2020-11-08 12:20:54 [127.0.0.1][-][-][info][ROB] 当前用户抢到了红包,抢到了1 2020-11-08 12:20:54 [127.0.0.1][-][-][info][ROB] 当前用户抢到了红包,抢到了6 2020-11-08 12:20:54 [127.0.0.1][-][-][info][ROB] 当前用户抢到了红包,抢到了7
但是,这里抢红包是不涉及用户的,即一个用户可以抢多次,我们需要记录抢红包的用户id和红包id的关系,保证每个用户每个红包只能抢一次,这里我们用到setnx这个方法
Redis Setnx(SET if Not eXists) 命令在指定的 key 不存在时,为 key 设置指定的值。
语法
redis Setnx 命令基本语法如下:
redis 127.0.0.1:6379> SETNX KEY_NAME VALUE可用版本
>= 1.0.0
返回值
设置成功,返回 1 。 设置失败,返回 0 。
我们优化之后的代码
/* * 抢红包 */ public function actionRob() { Yii::$app->response->format = Response::FORMAT_JSON; $redPackageId = Yii::$app->request->post('packageid', ''); $robUserId = Yii::$app->request->post('robuserid', ''); $lockKey = $robUserId . '-' . $redPackageId . '-lock'; if (!$this->redis->setnx($lockKey, $redPackageId)) { return ['code' => 0, 'msg' => '你已经抢过此红包']; } // 此关系维持24h,注意红包有效期应该也为24h $this->redis->expire($lockKey, 86400); if (!$this->isRemain($redPackageId)) { return ['code' => 0, 'msg' => '红包已被抢光']; } // 抢到的红包 $robMoney = $this->redis->lpop($redPackageId); Yii::info("当前用户抢到了红包,抢到了" . $robMoney, 'ROB'); return ['code' => 200, 'msg' => "当前用户抢到了红包,抢到了" . $robMoney]; }
好了,今天的教程到此结束,我们使用了redis分布式锁解决了线程安全问题