V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
yangwenqian
V2EX  ›  PHP

求完美的 PHP 并发锁机制

  •  
  •   yangwenqian · 2016-11-23 21:36:10 +08:00 · 11342 次点击
    这是一个创建于 2915 天前的主题,其中的信息可能已经有所发展或是发生改变。
    要做一个抢红包的功能,环境是使用红包接口, 10 万用户在同 1 秒种进行抢红包的操作,红包只有 10 个,在高并发的情况下,就无法控制了。
    一般是先从数据库 count 红包记录,大于 0 个的话,就调用红包接口,然后在数据库写一条记录。
    10 万用户在同一毫秒请求,数据库读写间隔时间太长了,导致 count 出来都是 0 ,没法限制 10 个。
    请问该怎么锁?
    网上有很多锁,试过大部分用 cache 锁或文件锁实现,我试了,在压力测试下,cache读写间隔时间比数据库少很多,但还是有间隔时间,仍然没有办法限制 10 个红包。
    求解决办法。谢谢。
    44 条回复    2017-12-01 10:46:35 +08:00
    langmoe
        1
    langmoe  
       2016-11-23 21:40:59 +08:00
    数据库读锁
    lynnworld
        2
    lynnworld  
       2016-11-23 21:45:15 +08:00
    redis incr
    batnss
        3
    batnss  
       2016-11-23 21:46:12 +08:00
    redis 生成一个 10 个元素的队列
    virusdefender
        4
    virusdefender  
       2016-11-23 21:53:16 +08:00
    二楼说的对, redis incr 可以
    cxbig
        5
    cxbig  
       2016-11-23 22:01:09 +08:00
    同 2 楼,十万用户还是交给缓存来处理。
    php71
        6
    php71  
       2016-11-23 22:04:05 +08:00
    让请求只能进来 10 个,不直接操作数据库,我看别人做秒杀也是这个思路
    Yunhao
        7
    Yunhao  
       2016-11-23 22:08:36 +08:00 via iPhone
    无脑写入,把前一百的用户请求存起来,排个序,前十的给个红包 (逃...
    cuebyte
        8
    cuebyte  
       2016-11-23 22:47:22 +08:00
    @php71 怎么做才能让请求只进来十个?
    tulongtou
        9
    tulongtou  
       2016-11-23 23:27:18 +08:00 via iPhone
    JS 上做个处理,提交的时候截掉一部分,请求到后台了再截掉一部分,剩下的到业务逻辑的就不多了,再去进行秒杀
    php71
        10
    php71  
       2016-11-23 23:52:21 +08:00
    @cuebyte redis 的 ordered set 当 set 里有 10 个的时候,其他请求就可以不用处理了吧,没意义了。

    zadd 添加 /zcard 计算有多少个
    orvice
        11
    orvice  
       2016-11-24 00:14:31 +08:00
    @cuebyte 代码层上控制百分 90 的请求直接 return 失败。
    abelyao
        12
    abelyao  
       2016-11-24 00:32:11 +08:00
    先抽奖,中奖的话再去 count 数据库,再确认是否真的中奖,可以很大很大的降低数据库请求量。
    helloccav
        13
    helloccav  
       2016-11-24 00:43:35 +08:00
    @php71 正如楼主所说, "cache 读写间隔时间比数据库少很多,但还是有间隔时间,仍然没有办法限制 10 个红包", redis 也一样吧?
    lslqtz
        14
    lslqtz  
       2016-11-24 01:12:42 +08:00
    @orvice 代码层上控制百分 90 的请求直接 return 失败。
    用户数并不是固定的啊
    lianyue
        15
    lianyue  
       2016-11-24 01:21:11 +08:00
    。。。。。。 很简单吧

    INSERT 发布的红包 (id, count) values(1, 10)

    result = UPDATE 发布的红包 SET count = count - 1 WHERE id = 1 AND count > 0;
    if (result === true) {
    INSERT 已经抢了的红包 .....
    }
    lianyue
        16
    lianyue  
       2016-11-24 01:24:50 +08:00
    这样绝对不会多 可能会少一个进程异常中断等。。造成

    如果要绝对的强一致性 可以用 sql 事务
    mindcat
        17
    mindcat  
       2016-11-24 03:49:55 +08:00
    @Yunhao 做排序的剁死喵
    ryd994
        18
    ryd994  
       2016-11-24 04:25:17 +08:00 via Android
    很明显你那个锁就是错的
    对的锁只有慢的没有不一致的
    这种需求不要用 SQL ,反正数据量不大,加内存用 Redis
    Time2
        19
    Time2  
       2016-11-24 08:56:13 +08:00
    redis 队列正解
    qq496844026
        20
    qq496844026  
       2016-11-24 09:15:10 +08:00   ❤️ 1
    Nginx 控制请求数量,然后 redis 的 incr 保证处理的正确性.这样可以最大程度的处理高并发了
    amey9270
        21
    amey9270  
       2016-11-24 09:48:02 +08:00   ❤️ 3
    对于这种大并发抽奖, 注意 3 点
    1. redis incr 使用原子计数可以解决, 并发情况下不会超发的问题
    2. 用户锁, 解决同一用户同时多次秒杀, 除非你不 care 一个用户中了多次奖品
    3. 防人机, 简单的方案是图形码 或者是 CSRF 都可以
    amey9270
        22
    amey9270  
       2016-11-24 09:53:15 +08:00
    补充一下
    注意一下流控, 不要让大流量冲垮你的系统, 宁愿让用户看到系统忙, 也不要让用户看到 500
    lijinma
        23
    lijinma  
       2016-11-24 10:03:01 +08:00   ❤️ 1
    使用 Redis 两个思路,楼上都提到了。

    因为 Redis 支持原子操作,所以你可以使用 Redis 做。

    1. 比如 redis incr ,因为是原子操作,你不需要担心并发的问题,你只需要判断每次 incr 后的值是否小于等于 10.

    2. 使用有限资源的模式,比如使用 redis list ,先创建好 10 个资源,然后每次操作都是 pop ,因为只有 10 个资源,不会 pop 出来 11 个资源的,谁拿到资源谁就中奖。

    另外,一般的配置, Redis 并发几万的请求一点问题也没有。
    amey9270
        24
    amey9270  
       2016-11-24 10:38:52 +08:00
    @lijinma @lynnworld @ryd994 @qq496844026 哈哈,插个题外话, 阿里云的开发职位不知道你们是否有兴趣, 我是阿里云的开发, 不是猎头, 如果有兴趣可以发我 email: [email protected], 我们可以电话聊一聊.
    shuiguyu
        25
    shuiguyu  
       2016-11-24 10:50:51 +08:00
    @lianyue 这个是乐观锁的思路吧,加个版本号。
    控制超卖。
    但是高并发的情况下,可能限流更明显一些。可以用计数器也可以用令牌桶。
    限速的情况可能令牌桶更好一些,可以控制发令牌的间隔,不至于都是前面的人抢到。
    suren1986
        26
    suren1986  
       2016-11-24 11:09:37 +08:00   ❤️ 1
    1. 用 Redis 的 queue ,开奖前 enqueue 10 个元素进去,开奖后 dequeue ;
    2. 用 Redis 的 setNx ,预定义 10 个 key ,能 setNx 成功表示得奖了

    http://redis.io/commands
    shibingsw
        27
    shibingsw  
       2016-11-24 11:18:08 +08:00
    cache 读写间隔时间比数据库少很多,但还是有间隔时间,仍然没有办法限制 10 个红包。
    -----
    你把更新和读做成原子的不就行了, redis 的 incr ,数据库的 update 都行啊。可是如果你要是先 get 再减一,再判断,然后再 set 这样自然不行了。。
    quericy
        28
    quericy  
       2016-11-24 11:28:57 +08:00
    2 楼正解, 原子性的操作就可以了
    zts1993
        29
    zts1993  
       2016-11-24 11:36:07 +08:00
    感觉在 nginx 上用 lua+redis 控制更好.冲到 server 上,如果服务器少得话还是有可能跪..
    huigeer
        30
    huigeer  
       2016-11-24 11:37:04 +08:00
    redis incr
    vus520
        31
    vus520  
       2016-11-24 11:50:04 +08:00   ❤️ 1
    0 ,请求直接进 mysql 肯定是不对的,文件锁在单机上是可靠的,但有风险。
    1 ,说了很多 redis 的方案,都是对的,用 redis 通过计数器和队列模式都可以, incr, decr, lpop 都行,不会有超发的情况
    2 ,应该要有流控设计, 10 次红包、秒杀,可以按一定比例,控制流量,只放 1000 个进来,减少后端的计算压力
    ipconfiger
        32
    ipconfiger  
       2016-11-24 11:54:22 +08:00
    9 楼正解, 其实现在大多数秒杀都在用这类方法, 其实不光是 js 上, 页面刷出来的时候就预先决定了你能不能真正进入"抽"的环节, 就没有那么大的流量冲击了, 再加上用 redis 的方案, 上亿级别的秒杀都轻松搞定啊
    silenceeeee
        33
    silenceeeee  
       2016-11-24 11:57:36 +08:00
    @shuiguyu 他这个其实把所有压力放到 MySQL 上了 通过锁阻塞来实现. 个人感觉不是很合理. MySQL 的锁不应该这样应用
    lostvincent
        34
    lostvincent  
       2016-11-24 12:26:45 +08:00 via iPhone
    数据库层面的话,可以预先将红包分成 10 个,然后中奖者 id 留空,抢的时候 update set 中奖者 id=用户 id , where 红包=这个抢的红包,且中奖者 id=空(暂时我还没测试过可行性)
    缓存上面都说的差不多了,自行参考吧
    qq496844026
        35
    qq496844026  
       2016-11-24 12:35:49 +08:00
    @amey9270 谢谢了,有朋友在阿里,觉得还是不太喜欢里面的氛围.现在我的团队不错,还想继续和我的小伙伴一起奋斗,可以做个朋友一起探讨技术
    iyaozhen
        36
    iyaozhen  
       2016-11-24 13:07:30 +08:00 via Android
    我来说说我司的思路吧。把「抢」红包这个操作分成 2 个。先拿着红包 id 抢,这层业务只做拦截。然后再拿着红包 id 去拆红包。这时就是具体的资金业务了。当然也会拆失败。

    还有就是使用 redis 。
    shuiguyu
        37
    shuiguyu  
       2016-11-24 13:08:15 +08:00
    @silenceeeee 一般我们设计的原则是,凡是涉及到库存的,基本上都用乐观锁比较保险一点,但是那个已经是数据库层面的了。这种高并发的抢购,前面就要在请求数,缓存等拦截住请求了,然后再分批次的提交扣库存就保险了。

    Guava 的 RateLimiter 类,能很方便的实现令牌桶,对抢购甩红包这种场景很实用。
    jsq2627
        38
    jsq2627  
       2016-11-24 18:47:33 +08:00
    var random = 生成 0-999 之间的一个随机整数
    if (random == 0) {
    (此处按照中奖率 10%编写抽奖代码,无需考虑高并发)
    }
    return 没抽中
    jsq2627
        39
    jsq2627  
       2016-11-24 19:02:12 +08:00
    参照 redis benchmark :
    http://redis.io/topics/benchmarks
    10 万 qps 很有可能超出 redis 能力

    在不同的业务情形下,总有一种妥协方式
    比如不严格保证先到先得
    或者让用户看五秒动画再反馈结果
    在某个方面妥协一下,就能很容易实现啦
    lynnworld
        40
    lynnworld  
       2016-11-24 21:36:13 +08:00
    额, 10w 人在同一秒操作,保守估计 100w 人打开这个页面,就为这 10 个红包,想想也是心累:(
    2owe
        41
    2owe  
       2016-11-24 21:52:19 +08:00
    Redis 双队列,每次抢红包, rpoplpush 原子操作 A POP 、 B PUSH ,执行发红包处理:若有失败, B lRem 、 A lPush ;若成功, B lRem 。


    我们是小产品哈,并发其实没很高。
    2owe
        42
    2owe  
       2016-11-24 21:58:12 +08:00
    http://www.woshipm.com/pd/204789.html

    当时自己看了这篇文章,感觉帮助挺大的。
    fhefh
        43
    fhefh  
       2017-07-23 21:24:45 +08:00
    先 mark 谢谢
    nash
        44
    nash  
       2017-12-01 10:46:35 +08:00
    nginx 控制请求次数,超过的直接返静态页面,请求通过的统统给我到 REDIS 上面排队,进行二次过滤,这样万无一失了
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   997 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 28ms · UTC 22:42 · PVG 06:42 · LAX 14:42 · JFK 17:42
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.