使用Memcached实现抽奖活动

上个礼拜和同事讨论了一个活动抽奖的技术问题,觉得很有意思,特此记录一下。产品需求非常简单,每天有一个奖品,准时在12点抽取,从技术角度考虑,需要满足两点:

  • 公平原则,谁的抽奖请求先到达就会抽中。
  • 只能被一个人抽到,不能导致两个以上的人抽中,显然这是最核心的技术问题。

1:Mysql 解决方案

我首先想到的就是通过 Mysql 的特性来解决该问题,先看数据表(tb)结构:

奖品ID(id)日期(dt)奖品状态(st)
2201811090
3201811101

奖品状态为 1 表示该奖品已经被抽取了,0 表示奖品还没有被抽取。

接下去我们看伪代码:

$con = mysql_connect("","","");
$sqk = "select * from tb where st = 0 and dt=" . date("Ymd");
$rs = mysql_query($sql,$con);
$data = mysql_fetch_array($rs);
$id = $data["id"] ;
if (($id!="") && ($data["st"]===0)) {
    $sql = "update tb set st=1 where st = 0 and id={$id}";
    mysql_query($sql,$con);
    $num = mysql_affected_rows($con);
    if ($num == 1) {
        echo "恭喜您,获得一个奖品";
    } else {
        echo "很遗憾,没有抽中键盘";
    }
}

简单解释下,如果奖品还存在,则执行一条 sql 语句,由于 Mysql 本身 Update 操作是原子性的,所以永远只有一个人会将 status = 0 变更为 status =1。

也就是说通过 Mysql 的原子操作保证奖品抽取公平以及确保只有一个人抽中,这种解决方案很简单,但如果同时有很多人在线抽奖,并行执行大量的 Update 操作,对数据库有比较大的压力,接下去看另外一种解决方案。

2:Memcached add 操作

Memcached 以它的高性能、高并发著称,主要用于缓存,但现在很多开发者在很多应用场景使用它,比如抽奖活动,先看伪代码:

//代表奖品是否存在的key
$key = date("Ymd");
//代表奖品是否已经被抽取的key
$sign = "s-" . date("Ymd");
$m = new Memcached();
$m->addServer('localhost', 11211);
if ($m->get($key)!="") {
    $bool = $m->add($sign,0,24*3600);
    if ($bool) {
        $m->delete($key);
        echo "恭喜您,获得一个奖品";
    } else {
        echo "很遗憾,没有抽中键盘";
    }
}

主要技术解决点就在于 Memcached add 命令具有原子性,如果 $sign 对应的 key 已经被设置了,那么第二个命令序列执行 add 操作就会失败,这样保证只会有一个人抽中,是不是非常巧妙。

3:Memcached cas 特性

其实在 Memcached 中,原子操作或者称为版本控制有专门的一个命令,那就是 cas 子命令,先看看官方的解释:

Check And Set (or Compare And Swap). An operation that stores data,

but only if no one else has updated the data since you read it last.

Useful for resolving race conditions on updating cache data.

这是一个检查然后设置的操作,当从 Memcached 中读取 key 对应的数据后,在执行 cas 操作的时候,如果你的 local 数据(从memcached读取的副本)和 Memcached 服务器端数据相同则操作成功,如果不相同则 cas 操作失败。

从内部原理来说,就是每次读取的时候,会获取数据的一个版本号(通过 memcached gets 命令获得),在执行 cas 的时候,会使用版本号对应的数据和 Memcached 服务器端进行比较,从而决定执行结果,先看伪代码:

$m = new Memcached();
$m->addServer('localhost', 11211);
$key = "g" . date("Ymd");
$cas=null;
//第三个参数是引用传递,获取版本号 token
//底层相当于执行 gets memcached 命令
$m->add($key,1);
$data = $m->get($key, null, $cas);
echo $m->getResultCode();
echo $cas . "\n";
echo $data . "\n";
//$data == 1 表示至少在抽奖请求到来的时候奖品还在
if ($data == 1) {
    $m->cas($cas,$key,"-1",24*3600);
    $code = $m->getResultCode();
    if ($code == Memcached::RES_DATA_EXISTS) {
        echo "很遗憾,没有抽中键盘";
    } else if ($code == Memcached::RES_SUCCESS) {
        echo "恭喜您,获得一个奖品";
    }
}

简单解释下,首先获取到一个 《使用Memcached实现抽奖活动》cas 版本对应的值已经被修改了,就告知客户端 Memcached::RES_DATA_EXISTS,表示该 key 对应的值已经被修改了,即奖品被别人获取了。

关于 cas 命令,可参考 http://www.w3big.com/memcached/memcached-cas.html,里面有详细的原生命令操作演示,而 php 代码做了一层封装,我写的也是伪代码,可能会运行错误,此处简单提供一个思路。

个人觉得 cas 命令在应付抽奖活动技术需求的时候比 add 命令更合适,另外本文介绍的抽奖活动需求也比较简单,如果遇到复杂的需求以及大并发的抽奖,可能会出现一些新的情况。

【这篇文章于2018-11-10号发表于公众号,地址https://mp.weixin.qq.com/s/agUU5ZjcVep-vPIKVjB6Fg,也可以关注我的公众号(ID:yudadanwx,虞大胆的叽叽喳喳)】

    原文作者:虞大胆的叽叽喳喳
    原文地址: https://www.jianshu.com/p/5db4d44cf254
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞