开发者社区> 问答> 正文

激活码发放,高并发下如何解决可能出现的异常。 400 请求报错 

现在有个发放激活码的系统,php+mysql。 code表: id(主键自增长),code(激活码内容,如123abc),status(1代表未被发放,0代表已被发放)。

现在有很多用户(注册用户,能拿到用户信息)去抽这些激活码,每人每天只能抽1次,这一次是肯定能抽到的。

每当用户抽一次激活码,就找到一行status为1的激活码记录,把这行记录的status置为0,同时在record表里添加一行记录(用户id和激活码id),然后返回激活码内容给用户。

逻辑挺简单,现在我要解决的问题是,就是高并发情况下可能会有问题:

比如很多用户同时抽激活码,我select一行status为1的激活码记录时,可能多个用户会select到同一个激活码。有没有什么办法当某个请求select一行记录时,就把这行记录锁住,包证其他请求不会拿到这个激活码。

希望各位大神帮我分析下我这种逻辑在高并发下可能会出现什么问题,有什么解决方案?因为我现在做的项目实际上会遇到高并发的情况,所以必须考虑进去。。。

展开
收起
kun坤 2020-05-31 13:02:27 785 0
1 条回答
写回答
取消 提交回答
  • 既然id是自增长,而且激活码看起来也是先生成好的,说明激活码应该是会按照顺序一条一条被是用掉。那么, 有个想法可以尝试一下:
    在内存中保留一个变量MAX_ID来记录当前可用的激活码的id(select min(id) + 1 from XXX where status=0,注意同步/锁)。 如果有用户过来请求激活码,具体流程如下:

    1. 判断用户是否满足收取激活码的要求,不满足则转到提醒页面。
    2. set CUR_ID = MAX_ID++ ,这2步操作必须通过同步或者锁的方式保证其原子性。
    3. 用 select * from XXX where id={CUR_ID} and status=1 来取得一条激活码。(这个时候取得激活码就不会和其他用户发生冲突了,因为无论哪个用户进到这一步获取的CUR_ID都是不同的)
    4. 新增record记录,并且将激活码记录的status设置为0。

    但是要额外考虑几个问题:

    1. 假如第三步取到的记录已经被激活过了,那么就必须在重新从第二步开始走。可以设置尝试多次的阀值。(注意记录好log)
    2. 如果CUR_ID超过了激活码的最大id怎么办? 
    3. 系统宕机后重启后需要重新获取MAX_ID。
    4. 假如第三步发生其他问题,导致这个激活码没有被用掉。那么激活码中就有可能会产生一些被漏掉的记录。这种情况下,可以在服务重启的时候把这些遗漏的id放到一个list中,然后第二步找CUR_ID时优先从这个list中获取。
    这个方法就可以把对数据记录的竞争转移到内存中一个MAX_ID的竞争,减小了需要考虑锁/同步的范围。 ###### 上面兄弟说的是一种方法,如果是多服务部署的话,应该得有额外的服务器存储{CUR_ID},保证一致性。也可以把{CUR_ID}放在数据库中的一张表中,利用数据库中的锁保证其同步,好处是可以整个业务可以写在一个存储过程中,不好的地方就是把压力交给了数据库。 利用数据库的话还有一种方式(保证在一个事务中): label_b:loop:     select id,激活码 from激活码表 where status=1 order by rand() limit 1 into @变量;     IF FOUND_ROWS()=1 THEN         update 对应激活码行状态;         if ROW_COUNT()=1 THEN             记录log;             LEAVE label_b;         end if;   ELSE                提示没有剩余的激活码了;                 LEAVE label_b;             END IF;         end loop label_b;       ###### 年底了。发红包的项目也挺多的,目前我在用的方案是 1.表为InnoDB表(支持行锁) 2.比如今天要发10个红包,那么我在红包表新增十条记录(这样保证不会超出预算,不然超出了多的钱可要自己出喔!) 3.来一个中奖用户,就在红包表选择一条未被使用的红包发出去,同时吧状态置为1 就可以了。反正发了这么多红包,没出问题 ######回复 @xia-yongsheng : 这个问题确实有的,除非锁表。。 目前做到的是不多发红包--。######有可能两个人拿到同一个红包啊,他们同时来的话。。。。###### 这种高并发下直接搞数据库只能说是你找死 方法:首先你要有一个生成激活码的方法,可以根据当前时间(精确到毫秒),请求IP等一切手段来保证生成的激活码唯一,然后在用户抢激活码的时候,判断他是否可以获得激活码,如果可以就生成激活码并返回给用户,同时把这个生成的激活码扔到一个消息队列中,后台在有一个程序平稳的将消息队列中生成的激活码批次进行持久化(保存数据库)等 ######这个方法好######补充一下,如果是先到先得那种,可以现在外部缓存中如redis中存在值,如10000,然后开抢时先请求到的则进行redis的原子减操作,如果减完后的值小于0,则说明没有了,大于0则进行正常的生成激活码和后续操作######这个方法靠谱###### 加锁。innodb才支持行锁。然后红包记录总量控制

    ######最简单的方法是加锁。如果对性能要求很高,就借助redis即可######php不知道,java我是缓存在线程安全的queue中,直接拿一个,然后update回数据库######这个有两个限制:1、激活码量不多 2、单机环境######缓存有了解吗?######嘿,简单点在事务里 select 出一条status为1的,然后update xxx set status=0 where id=id and status=1;如果更新成功就ok,失败【说明被别人先拿了】就回滚然后重新开始,select update 试个几次就行。这个我经常用,在多个人需要对同一行操作的情况下稍微比下面的优应为是到update才锁住行。注意控制重试次数。

    另外一种也是事务里,select 出一条status为1的,然后select * from xxx where id=123 for update,然后判断返回的status是否为1,不是就回滚事务重新试。在这种场景下应该可以采用,和上面一样要控制好重试的次数,会锁住一行记录,这在产品库存扣减之类的场景就不合适。

    最后一种,咳咳咳,是你这个场景的,recover表的激活码id设置成唯一索引,然后在事务里插入失败的话就回滚。会产生间隙锁和nextkey锁导致阻塞。
    另外的解决方案,是。。。建一个自增表,表就字段id,自增的,code varchar(大小您看着办),uid。 每次抽就往这里插入一行记录code为空就好。 然后拿到自增id,然后 $code_num = $id^COVER_NUM;# COVER_NUM是一个大数常量,最好是类常量。 #如果有gmp拓展,并且php版本大于5.3,且gpm_strval支持到62位的话 $code = gmp_strval(gmp_init($code_num),62); #如果没有gmp或gmp只能到36位的话 $code = base_covert($code_num,10,36); 然后把code update 回去,没有锁竞争,但不好的地方是code不能事先生成,如果你要事先生成也可已,拿到id的时候不去生成code而是去一个表里取出事先生成的id值相等的code就可以。

    2020-06-01 09:47:50
    赞同 展开评论 打赏
问答排行榜
最热
最新

相关电子书

更多
徐雷-Java为王,互联网高并发架构设计与选型之路6.0 立即下载
Redis 的高并发实战:抢购系统 立即下载
MySQL高并发场景实战 立即下载