抢红包的一些实现思路

薄洪涛5年前PHP1363

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

本篇文章主要探讨了

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

相关文章

php用curl模拟post请求接口的坑

php用curl模拟post请求接口的坑

我们的接口是用java实现的,然后我需要用php去调用下接口,请求方式为post,需要传一个数组过去(不是json_encode的那种),之前的时候,是这么写的$post_params =&...

Elasticsearch按照日期聚合

Elasticsearch按照日期聚合

我们现在做的是医疗的业务,有个需求是这样的,查询出某位医生前七天的坐诊记录,并且,医生的坐诊记录是不连续的,这样就需要写一个dsl语句来实现es库的搜索首先我使用了es库中的聚合功能,按照日期去聚合,...

Yii2.0认证及限流

Yii2.0认证及限流

上次搭建了Yii2.0的接口框架后,现在开始搭建认证和限流模块,先说下这两个模块的作用认证:前后端分离,每次请求都是无状态的,及每一次请求服务器不知道你是谁,你有没有登陆;我们就需要做一个认证模块去识...

【转】TCP长连接和短连接区别

【转】TCP长连接和短连接区别

    当网络通信时采用TCP协议时,在真正的读写操作之前,server与client之间必须建立一个连接,当读写操作完成后,双方不再需要这个连接时它们可以释放...

php redis Hash操作

//为user表中的字段赋值。成功返回1,失败返回0。若user表不存在会先创建表再赋值,若字段已存在会覆盖旧值。 $redis->hSet('user', ...

Yii2.0整合ueditor并上传图片到七牛云

Yii2.0整合ueditor并上传图片到七牛云

某个项目要做一个文章模块,用到Ueditor,并且ue中的图片要上传到七牛,所以总结下步骤;1、Yii2.0下载ueditor for Yii2.0和七牛composer require&n...

发表评论    

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