求完美的 PHP 并发锁机制

2016-11-23 21:36:10 +08:00
 yangwenqian
要做一个抢红包的功能,环境是使用红包接口, 10 万用户在同 1 秒种进行抢红包的操作,红包只有 10 个,在高并发的情况下,就无法控制了。
一般是先从数据库 count 红包记录,大于 0 个的话,就调用红包接口,然后在数据库写一条记录。
10 万用户在同一毫秒请求,数据库读写间隔时间太长了,导致 count 出来都是 0 ,没法限制 10 个。
请问该怎么锁?
网上有很多锁,试过大部分用 cache 锁或文件锁实现,我试了,在压力测试下,cache读写间隔时间比数据库少很多,但还是有间隔时间,仍然没有办法限制 10 个红包。
求解决办法。谢谢。
11435 次点击
所在节点    PHP
44 条回复
amey9270
2016-11-24 09:48:02 +08:00
对于这种大并发抽奖, 注意 3 点
1. redis incr 使用原子计数可以解决, 并发情况下不会超发的问题
2. 用户锁, 解决同一用户同时多次秒杀, 除非你不 care 一个用户中了多次奖品
3. 防人机, 简单的方案是图形码 或者是 CSRF 都可以
amey9270
2016-11-24 09:53:15 +08:00
补充一下
注意一下流控, 不要让大流量冲垮你的系统, 宁愿让用户看到系统忙, 也不要让用户看到 500
lijinma
2016-11-24 10:03:01 +08:00
使用 Redis 两个思路,楼上都提到了。

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

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

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

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

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

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

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

在不同的业务情形下,总有一种妥协方式
比如不严格保证先到先得
或者让用户看五秒动画再反馈结果
在某个方面妥协一下,就能很容易实现啦
lynnworld
2016-11-24 21:36:13 +08:00
额, 10w 人在同一秒操作,保守估计 100w 人打开这个页面,就为这 10 个红包,想想也是心累:(

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://tanronggui.xyz/t/322782

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX