抢红包的一些实现思路

薄洪涛4年前PHP1152

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

本篇文章主要探讨了

  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 redis Hash操作

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

redis持久化的两种方式

redis持久化的两种方式

redis为了内存数据的安全考虑,会把内存中的数据以文件形式保存到硬盘中一份,在服务器重启之后会自动把硬盘的数据恢复到内存(redis)的里边。数据保存到硬盘的过程就称为“持久化”效果。redis支持...

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

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

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

Yii2.0认证及限流

Yii2.0认证及限流

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

csrf攻击原理及防范

csrf攻击原理及防范

CSRF(Cross-site request forgery),中文名称:跨站请求伪造,也被称为:one click attack/session riding,缩写为:CSRF/XSRF。在不少的...

大文件之分片上传

大文件之分片上传

    我们知道,无论是nginx还是php,都会对上传文件的大小做限制,今天刚刚做了一个客户端包的管理,要求上传apk或者ipa文件,而且都不小(超出了php...

发表评论    

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