抢红包的一些实现思路

薄洪涛5年前PHP1336

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

本篇文章主要探讨了

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

相关文章

Yii2.0 jQuery(...).yiiActiveForm is not a function

Yii2.0 jQuery(...).yiiActiveForm is not a function

记一个常见的错误,如下图当我们在view中创建一个activeForm的时候,有时会报这种错误;通常情况下,是jquery重复引入导致的,因为你引入的jquey的版本跟yii2自带jquery版本冲突...

Elasticsearch第三篇之全文搜索及在Yii2.0中的使用

Elasticsearch第三篇之全文搜索及在Yii2.0中的使用

前几天做了一个模块,大数据的搜索,其实也不是特别大,组合起来差不多800万左右,用的是mysql数据库,需求有这么变态的两点;需要按照地址去搜索按照起止时间去搜索别的不说,就这两条,mysql也就只能...

redis持久化的两种方式

redis持久化的两种方式

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

Yii连接postgreSQL及与mysql优劣比较

一、 PostgreSQL 的稳定性极强, Innodb 等引擎在崩溃、断电之类的灾难场景下抗打击能力有了长足进步,然而很多 MySQL 用户都遇到过Server级的数据库丢失的场景——mysql系统...

Yii2.0框架ActiveForm总结

首先引入ActiveForm和Html这里两个是很重要的<?php     use yii\bootstrap\ActiveForm; &nbs...

Elasticsearch集群第一篇之安装

Elasticsearch集群第一篇之安装

    全文搜索属于最常见的需求,开源的 Elasticsearch (以下简称 Elastic)是目前全文搜索引擎的首选。它可以快速地储存...

发表评论    

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