码农日记

薄洪涛的个人博客

抢红包的一些实现思路

最近探索了关于抢红包的一些实现思路,在此记录下

本篇文章主要探讨了

  1. 金额随机算法

  2. 关于分布式锁的一些探索

我们开始,首先说下抢红包的业务流程,分为两步,发红包和抢红包,在抢红包的时候,并发是非常大的,我们这里采用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中的数据如下,现在已经生成了随机的金额image.png


抢红包的实现逻辑

/*
 * 抢红包
 */
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分布式锁解决了线程安全问题

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

Powered By Z-BlogPHP 1.7.3

版权所有 | 转载请标明出处