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

关于抢京东券高并发的问题

  •  1
     
  •   ben548 · 2015-12-22 21:07:28 +08:00 · 5226 次点击
    这是一个创建于 3253 天前的主题,其中的信息可能已经有所发展或是发生改变。

    之前在一个微信公众号上做了一个抢京东券的功能, 50 张京东券,面额 50 、 100 不等,存在一张 card 表中,四个字段, id , number , money , is_taken 。

    因为之前没有这种高并发处理的经验,所以使用了一种最传统的方式来实现:

    方案一:来一个人我就从数据库中取一张京东券出来给他,并将该京东券标记为已使用(即更新 is_taken 字段),并将该用户插入到 winner 表中

    这个方案最终导致的悲剧是,有一张京东券被两个人领取到了。
    我所理解会出现这个问题的原因是获取未被占用的京东券数据( select )和更新该条京东券数据( update )是两个独立的操作,在这两个操作之间存在时间间隔,例如 A 用户刚得到了一张 100 元的京东券,还未来得及更新, B 用户涌入查询到这张京东券未被使用,所以 B 用户也获得了这张京东券。

    问题一:我这个理解是正确的吗?还是有更深入的原因?

    出了这个问题后,我在网上查找关于高并发相关的资料,几乎都提到了队列和锁。就我个人了解队列可以使用 redis 或者 memcacheq(这个没用过,不熟悉),所以自己想了第二种方案。

    方案二:事先将京东券 id 数据压入到 redis 的 list 中,每过来一个有效的用户,就 pop 一个 id 给他(当 pop 出来的数据为空时说明京东券已经被抢光),并将用户 id 与京东券 id 的对应关系存储到 redis 的 set 当中去,然后根据这个 id 来查找京东券数据,显示给用户京东券的面额,并将 set 中的数据存储到数据库当中去。

    个人觉得这种方案会比第一种方案要好的多。但是没有真正意义上去实践过,只是个人思考的一个结果。

    问题二:第二种方案是否可行?是否还有更优方案?或者说方案二是否有可以优化的地方?

    问题三:在高并发时很多文章中说到的锁是一个怎样的概念呢?我的理解是这个锁就像是数据库的一个大门,一次只放一个人进去,是这样吗?具体该如何设计和使用?

    问题四:在应对大流量高并发的情况时,在服务器层面要做哪些工作?

    问题五:我所举得这个例子与平常类似网上商城中的秒杀功能有哪些相同和相异之处呢?是否可以按照方案二的设计思路进行设计呢?

    23 条回复    2015-12-24 17:38:26 +08:00
    billlee
        1
    billlee  
       2015-12-22 22:08:07 +08:00   ❤️ 1
    原来方案产生问题的原因是查询操作和更新操作是分开的,不是原子操作。这样设计应该就没有问题
    CREATE TABLE coupon (id INTEGER, owner_id INTEGER DEFAULT NULL) PRIMARY KEY(id);
    CREATE INDEX coupon_owner_index ON coupon (owner_id INTEGER);
    分配券的时候,先
    UPDATE coupon SET owner_id = ? WHERE owner_id IS NULL LIMIT 1;
    然后再把券 id SELECT 出来就好了

    方案二里面,如果不是要用 redis 做缓存,用户 id 和券 id 就没必要写回 redis 了吧。要注意写数据库会比读写 redis 慢很多,如果写数据库时超时了,是不是要把券还给券池 ?
    li24361
        2
    li24361  
       2015-12-23 09:28:18 +08:00
    楼上方法不错,不过 is null 走不到索引吧
    ben548
        3
    ben548  
    OP
       2015-12-23 09:43:45 +08:00
    @billlee 方案不错,我是先 select 再 update ,而你的出发点则是先 update 再进行 select ,可以有效避免我所遇到的问题,关于方案二为什么要将用户 id 和券 id 对应关系存到 redis 里面就是担心出现写数据库超时的问题,如果数据出错了的话,还是可以从 redis 里面拿到数据的,就不必将券还给券池
    ben548
        4
    ben548  
    OP
       2015-12-23 09:46:45 +08:00
    @billlee 那像京东等大型电商网站那种量级的网站时,你的这种方案未必可行吧?估计是撑不住的
    realpg
        5
    realpg  
       2015-12-23 09:57:10 +08:00
    楼主设计能力不行……
    最基础的,如果你用数据库实现,好歹也 select for update 啊……
    MRJ
        6
    MRJ  
       2015-12-23 09:57:29 +08:00
    @billlee 方法不错,
    @ben548 楼主自己想的 redis 也可以, redis 的 incr 方法是原子性的,在操作的时候,把应发的券的 key 存储一下,券发完了再删掉就可以了
    jonemao
        7
    jonemao  
       2015-12-23 10:22:42 +08:00
    极端情况下 此场景 程序公平的级别可以调节到最低 保障整个系统的健壮性
    ben548
        8
    ben548  
    OP
       2015-12-23 10:35:30 +08:00
    @jonemao 你所说的公平级别是什么意思?没听懂哦
    ben548
        9
    ben548  
    OP
       2015-12-23 10:39:43 +08:00
    @realpg select for update 是什么意思?我是第一次处理类似这样的情景,没什么经验
    realpg
        10
    realpg  
       2015-12-23 10:42:35 +08:00
    @ben548
    innodb 事务啊。锁单行
    不管怎么做,这种场景怎么也得用事务处理吧,占用失败需要回滚的
    sunshinez1128
        11
    sunshinez1128  
       2015-12-23 13:35:27 +08:00
    这种需要根据业务来判定,根据楼主的描述个人认为单靠数据库行级锁就可以解决,也就是 select for update 语句。如果并发量实在太大,可以根据业务考虑其他方案,比如小米著名的耍猴模式,通过一个特殊算法先排除大部分用户(具体算法需要根据实际业务量来订),不满足条件的用户直接返回单机版抢购,满足条件的进入事务程序,或者采用 12306 的排队模式,所有用户进入后压入队列,按批次完成事务,等等。
    zonghua
        12
    zonghua  
       2015-12-23 14:08:44 +08:00
    @ben548 京东用的是 Nginx+Lua+Redis 进行妙杀活动,开涛的博客 http://jinnianshilongnian.iteye.com/blog/2187328
    ben548
        13
    ben548  
    OP
       2015-12-23 18:48:47 +08:00
    @realpg 对了,我当时使用的数据库是 mongodb ,不是 mysql , select for update 是 nosql 数据库没有的吧?
    ben548
        14
    ben548  
    OP
       2015-12-23 19:08:08 +08:00
    @sunshinez1128 能深入讲一下吗?很感兴趣
    sumuu
        15
    sumuu  
       2015-12-23 23:04:19 +08:00
    来个简单粗暴的方案, number 我的理解是唯一的券 id 。
    你现在做的是: select -> update -> insert.
    出现重复最简单的方案就是唯一,在数据库里面把 number 字段设置为唯一。
    在 update 成功才拿到券,失败就返回谢谢了。
    realpg
        16
    realpg  
       2015-12-24 00:29:56 +08:00
    @ben548
    没注意,你也没提你的环境。以为 MYSQL 了。
    这种涉及金额的操作,选型选用非事务的处理,这还不是问题?
    jonemao
        17
    jonemao  
       2015-12-24 09:44:22 +08:00
    @ben548 根据这个方案的背景可以设计一个相对公平的系统(如果如此设计能够提高其他系统的健壮性的话) 举例说就是一张券一个用户 0.99 秒发出的请求 一个用户 0.98 秒发出的请求 如果绝对公平设计思路的话 那就是 0.98 要拿到 而 0.99 的用户拿不到 但是其实在业务上并不需要如此精准的结构设计 这样的话解决方案会多很多 至于公平这事 其实抢券本身就是抽奖性质了 就算程序能保证绝对公平 还要取决于网速等其他因素 所以没必要在这上面去消耗太多的成本
    具体用什么方案还是要看手头的资源 我只是提出一个比较邪门的思路而已 正路嘛很多人都说过啦 也没啥搀和的必要了
    ben548
        18
    ben548  
    OP
       2015-12-24 10:24:52 +08:00
    @jonemao 按照你的思路,我想了想,其实完全可以在每个用户过来时生成一个随机数,随机数满足一定条件的时候才给他京东券,这从一定程度上回过滤掉大部分的用户请求,哈哈。不过觉得更好的方式是用 redis 的计数器去过滤
    sunjiayao
        19
    sunjiayao  
       2015-12-24 10:30:33 +08:00
    50 个。。。 update table set ... where .. 就可以了吧
    ben548
        20
    ben548  
    OP
       2015-12-24 11:29:00 +08:00
    @sunjiayao 是的。但是是可以发散开来看的,主要想讨论高并发的问题,不止只针对我的问题
    greenmoon55
        21
    greenmoon55  
       2015-12-24 13:31:44 +08:00
    1L 是个好想法~
    我用过 python-redis-lock 来加锁,不是秒杀并发小。。
    moro
        22
    moro  
       2015-12-24 16:06:57 +08:00
    方案二是可行的,代价最小,其他方案都是要另外加锁,或者事务。
    ben548
        23
    ben548  
    OP
       2015-12-24 17:38:26 +08:00
    @greenmoon55 redis 在里面扮演了什么角色呢?
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   3433 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 26ms · UTC 11:56 · PVG 19:56 · LAX 03:56 · JFK 06:56
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.