开发红包功能

16月17日更新:

使用发布订阅的方式来回收红包,存在2个问题:

第一个原因和Redis系统的稳定性有关。对于旧版Redis来说,如果一个客户端订阅了某个或某些频道,但它读取消息的速度却不够快的话,那么不断积压的消息就会使得Redis输出缓冲区的体积变得越来越大,这可能会导致Redis的速度变慢,甚至直接崩溃。也可能会导致Redis被操作系统强制杀死,甚至导致操作系统本身不可用。新版的Redis不会出现这种问题,因为它会自动断开不符合client-output-buffer-limit pubsub 配置选项要求的订阅客户端(本书第8章将对这个选项做更详细的介绍)。
第二个原因和数据传输的可靠性有关。任何网络系统在执行操作时都可能会遇上断线情况,而断线产生的连接错误通常会使得网络连接两端中的其中一端进行重新连接。本书使用的Python语言的Redis客户端会在连接失效时自动进行重新连接,也会自动处理连接池(connection pool,具体信息将在第4章介绍),诸如此类。但是,如果客户端在执行订阅操作的过程中断线,那么客户端将丢失在断线期间发送的所有消息,因此依靠频道来接收消息的用户可能会对Redis提供的PUBLISH 命令和SUBSCRIBE 命令的语义感到失望。

第一个问题目前来说应该不算问题了,但第二个仍然是个问题,如果监听程序因为某些原因奔溃的话就会丢失在奔溃期间所接收到的消息,我更希望是像聊天那样,即使你不在线,在上线之后也会接收到离线期间他人发送给你的消息,所以进行了改造,保证其高可用性,说一下思路。

使用有序集合(ZSET)来做处理,member记何种类型的红包以及红包ID,score记过期的时间戳,因为是有序集合,可以对时间戳进行排序,因而可以保证先过期的红包先被回收,写一下伪代码:

//24小时监听
while(1){
    $redis=Yii::$app->redis;//建立一个redis连接
    $arr=$redis->zrange('recycle_packet',0,1,'withscore');//取出第一条记录
    //$arr是一个索引数组,0为member,1为score
    if($arr[1)<time()){//如果还没有过期
        sleep(1);//等待一秒
        continute;
    }
    pacekt_func($arr[0]);把member传给回收红包的函数;过长这里不再贴出
    $redis->zrem('recycle_packet',$arr[0]);//从集中中已出这个任务;
}

大概就是这样,其中为了保证不出错,每次调用回收函数应当记录日志


最近业务需要,要开发红包,AA收款等功能,这里把主要的设计思想以及具体的实现方案进行介绍,如有设计或实现的纰漏,或是存在漏洞,还请大家指教指教。

红包的功能大家应该都很熟悉了,这里简单的对红包功能进行梳理。

红包的业务主要有4个部分,红包发送,红包接收(这里是2步,打开-抢),红包记录查询,以及红包过期回收机制。
以下这几部分依次分析。

红包发送:

因为功能上没有做随机红包的功能,这里发的全是等额红包。随机红包的话功能类似,这里我随便分析以下,不做具体介绍,听说微信的随机红包一开始是先计算好然后存数据库,后期才改成即时计算,个人认为业务量没上去的情况下预先计算比较好,然后存数据库,使用redis队列功能控制并发,差不多就是这样。

业务做的是等额红包,所以要简单些,前端只需传单个红包的金额,数量,用户ID等过来,国际惯例,做些常规验证,如余额是否足够,数量是否超出限制等。然后事务处理,减去余额、红包记录、钱包记录等存数据库,在redis里存入2个string:“红包标识+ID=>红包数量”,过期时间为24H,“红包标识+id=>随意”,过期时间同为24h。
这里的红包标识请随意定义,2个key不一样,第一个主要是用来判断红包是否存在,还有数量以及控制并发等问题,重复抢的问题可以在redis维护一个已分配集合(set)来限流,这里因为业务上不会有过多这种问题,我采用了直接查询数据库的方式。

至此,红包发送完毕。

红包接收:

微信的红包接收红包分两步,第一步是点击红包,如果红包还可领取,则在弹框里点击“抢”,否则返回列表。第二步是“抢”,此时才是真正抢红包的操作。

第一步:
领取人发起请求(携带红包ID,用户ID过来),接着后端基于redis和数据库中的数据做验证,主要判断红包是否已被领完,是否过期,用户是否重复领取等,严格点还可以判断用户是否在群组内,建议做个model方法。如果通过则进去第二步,否则返回列表(请求另外接口或直接)

第二步:
正常来说,到这一步,说明用户是可以领取红包的,但是为了安全起见,还需再次做常规验证(比如QQ,微信的XPosed插件好像是可以不点击直接领红包的)。因为redis是单线程的,我们使用incr和decr的原子性来控制并发,然后写数据库,一下update和insert操作,需要开启事务,主要使用锁来保证数据一致性。

到这里,红包接收完毕

红包过期回收:

一开始我考虑的是用传统的做法,定期(5min或10min或多少时间)查询数据库中过期的数据,然后做处理。这种做法问题很明显,一开始还好,做大后会有不必要的数据库压力和内存开销,还有可能存在进程多个在运行的问题。
于是去网上一番查找,发现redis在2.8版本后更新的key通知特性可以使用,具体介绍看这篇文章:点击这里

我们先修改一下redis的配置文件:

notify-keyspace-events "Ex"

重启配置,使其配置生效,然后我们在redis试着监听一下

127.0.0.1:6379> psubscribe __keyevent@0__:expired
Reading messages... (press Ctrl-C to quit)
1) "psubscribe"
2) "__keyevent@0__:expired"
3) (integer) 1

添加一个5秒过期的键:

127.0.0.1:6379> setex mrone 5 yinan
OK

5秒,另一边执行了堵塞订阅操作的终端接收到了消息:

1) "pmessage"
2) "__keyevent@0__:expired"
3) "__keyevent@0__:expired"
4) "mrone"

顺利获取到key,说明对过期key的订阅是成功的。

在composer中文镜像网搜索,发现已经有人写好了轮子:
点击这里

我们打开源代码看一看,这里展示部分:

private $redis;
   public $hostname = 'localhost';
   public $point = 6379;
   public $timeout = 0;
   public function init()
   {
       $this->redis = new \Redis();
       $this->redis->connect($this->hostname, $this->point, $this->timeout);
   }

但是,这个轮子要在服务器上用,还需要再修改点东西,我们打开源代码,定义一个公共变量

public $database=0;

在init()函数connect后增加一句:

$this->redis->select($this->database);

这么做,主要是为了将来在服务器上使用,可能会使用不同的redis库,为避免修改源码,这里认为地增加变量让其可以在配置中配置。在配置中的配置代码如下:

'redis_queue'=>[
            'class'=>'johnnylei\redis_queue\RedisQueue',
            'hostname' => 'localhost',
            'point' => 6379,
            'database' => 0,
        ],

继续,按照轮子上的介绍,

// console里面监听,并且处理,设置监听不超时
ini_set('default_socket_timeout', -1);
Yii::$app->redis_queue->subscribe('test', function($instance, $channelName, $message) {
    var_dump($message);
});

我们应该可以看到打印出来的key。但是在匿名函数那么写可读性很差,我们把它分离出来,在控制器同级目录下建立一个名为psCallBack.php的文件,定义一个function

function psCallBack($instance, $channelName, $message)
{
    //todo
}

然后修改controller里的方法,在监听方法里require文件且修改subscribe调用,修改完如下:

public function actionListenRedis()
{
    require_once (__DIR__.'/psCallBack.php');
    // console里面监听,并且处理,设置监听不超时
    ini_set('default_socket_timeout', -1);
    \Yii::$app->redis_queue->subscribe('__keyevent@0__:expired','psCallBack');
}

然后在todo里处理逻辑,主要是得到过期红包的id之后,进行一下常规判断,如红包是否已被领完,时间是否已过期等,根据结果进行退款,写钱包记录等操作。
这样做的好处很明显,没有多余的操作和额外开销,但是需要进程一直运行监听,和定查数据库比,个人觉得还是很不错的。
还需要让进程在服务器上一直运行,因为还未上线,而我又没有服务器….,所以这里的todo,后期更新
至此,红包过期回收完成。

红包记录查询:

这个就是普通的常规接口,根据UI的页面给数据就行,主要分2部分,一个是发送人的一些信息,还有就是下面的领取人列表信息。

到这里,基本流程就晚了,群定向红包,个人红包,群AA收付款等大同小异。
至此,红包功能结束。