赞
踩
我认为在抢红包业务里面,主要有以下几个关键问题:
1、多个人同时抢一个红包存在的数据竞争问题(并发问题)
2、判断一个人是否已抢过该红包 (可见性问题)
3、拼手气红包的分配算法
4、红包抢到后钱怎么到账?
当多个人同时抢同一个红包时,会存在数据竞争,这个好理解。那么什么发生了竞争?首先红包个数会有竞争,当两个人同时抢最后一个红包时,只有一个人能抢到红包,类似于秒杀系统中的库存,如果没有解决好竞争问题,就会出现诸如超卖或少卖的问题。
解决竞争问题的主要思路:1、单线程执行(如加互斥锁、redis+lua脚本保证原子性)2、CAS
当我们抢完红包时,下次点进去,就不会显示可抢的状态了,即使红包还没有抢完。那如果我们很快地点开同一个红包呢?会不会都显示可抢状态。如果你抢红包通过更新数据库来完成,判断有没有抢过红包是有时间差的,那怎么解决呢?
解决数据可见性问题:1、还是单线程执行(如加互斥锁、redis+lua脚本保证原子性) 2、在单机环境下采用volitile修饰变量(有可以关注下这个问题:volatile 修饰数组,那么数组元素可见吗?)
红包分配算法有很多种,满足随机,加上一些基本的限制:比如每个红包金额必须>=0.01。
常见的红包分配算法有:1、二倍均值法:适用于实时计算抢到的金额 (据说是之前微信用的 微信红包算法) 2、线段法:适用于发红包时预先算好每个红包的金额
可以参考一下:红包算法
当然你也可以根据业务条件设计一套自己的分配算法。
红包金额的到账与抢红包逻辑两者不是一个耦合的逻辑,可以进行解耦,可以采用抢到红包后将打款消息写入到消息队列
,由后续业务处理,这样既解决了业务解耦的问题,同时起到了削峰填谷的作用,提供抢红包核心业务的性能。(如果业务量不大,或没有消息队列的时候,采用线程池异步执行也是一个弥补的方案)
那么我是如何解决以上问题的呢?
表结构:
CREATE TABLE `red_envelope_info` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `uid` int(10) unsigned NOT NULL COMMENT '红包所属用户', `title` varchar(255) DEFAULT '' COMMENT '红包主题', `amount` mediumint unsigned NOT NULL COMMENT '红包金额总金额,单位:分', `detail_num` smallint unsigned DEFAULT 0 COMMENT '子红包数量', `receive_num` smallint unsigned NOT NULL COMMENT '多少人已领', `receive_rule` varchar(255) NOT NULL DEFAULT '' COMMENT '领取规则', `type` tinyint unsigned NOT NULL DEFAULT 0 COMMENT '分享类型 1:普通红包 2:拼手气红包', `status` tinyint unsigned NOT NULL DEFAULT 0 COMMENT '状态 0待支付 1正常(支付成功) 2过期 3支付失败', `pay_no` varchar(60) NOT NULL DEFAULT '' COMMENT '交易识别编号,业务id', `gmt_pay` int(10) unsigned NOT NULL COMMENT '支付时间', `gmt_begin` int(10) unsigned NOT NULL COMMENT '开始时间', `gmt_end` int(10) unsigned NOT NULL COMMENT '结束时间', `gmt_create` int(10) unsigned NOT NULL COMMENT '创建时间', `gmt_modified` int(10) unsigned NOT NULL COMMENT '更新时间', PRIMARY KEY (`id`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT ='红包发放表'; CREATE TABLE `red_envelope_detail` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `gift_id` int(10) unsigned NOT NULL DEFAULT 0 COMMENT 'red_envelope_info.id', `uid` int(10) unsigned NOT NULL COMMENT '领取的用户UID', `amount` mediumint unsigned NOT NULL COMMENT '红包金额,单位:分', `status` tinyint(1) unsigned NOT NULL DEFAULT 0 COMMENT '状态 1已领取', `gmt_receive` int(10) unsigned NOT NULL COMMENT '领取时间', `gmt_create` int(10) unsigned NOT NULL COMMENT '创建时间', `gmt_modified` int(10) unsigned NOT NULL COMMENT '更新时间', PRIMARY KEY (`id`), KEY `idx_uid` (`uid`), KEY `idx_gift_id` (`gift_id`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT ='红包领取记录表';
lua脚本:
// KEYS[1]为红包库存的key // 获取红包剩余个数 local stock = tonumber(redis.call('hget', KEYS[1], 'remain_num')) // 记录不存在 if stock == nil then return 3 end // 已抢完 if stock <= 0 then return 0 end // 是否已抢过(KEYS[2]为红包已抢用户id集合的key,用set存储,ARGV[1]为用户id) if redis.call('SISMEMBER', KEYS[2], ARGV[1]) ~= 0 then return 4 end // 成功抢到红包,库存减1 stock = stock - 1 redis.call('hset', KEYS[1], 'remain_num', tostring(stock)) // 设置已抢用户id redis.call('SADD', KEYS[2], ARGV[1]) if stock == 0 then // 标记最后一个红包 return 2 end return 1
这里通过redis + lua脚本保证了抢红包动作的原子性,但要注意几个问题:
1、在redis集群中,一个lua脚本中的多个key必须在同一个节点上
,否则会报错。可采用的方案为hashtag
。如
red_envelope_{%s}
red_envelope_uid_list_{%s}
%s传红包id,保证同一个红包数据在一个节点上,同时可以是数据分布更加均匀。
2、即使这里解决了抢红包时的竞争问题,但在后续红包数据更新时,仍然存在竞争。在这种情况下,可选方案:
解决这个问题,我们也可以看到,其实解决竞争还有一个办法,那就是消除竞争。毕竟这才是更好的方案
贴出部分业务代码:
public GrabResTO grab(GrabReqTO reqTO) { log.info("抢红包请求:{}", reqTO); // 解析红包链接 RedEnvelopeShareInfoTO shareInfoTO = defaultShareIdCodec.decodeShareId(reqTO.getShareId()); if (Validator.isNull(shareInfoTO)) { throw new BizException(String.format("解析shareId失败,shareId:%s", reqTO.getShareId())); } // 执行lua脚本 Long scriptRes = redisClient.executeScript(LUA_SCRIPT, Lists.newArrayList(CommonUtil.stringFormat(RedEnvelopesConstant.ENVELOPE_KEY_PREFFIX, shareInfoTO.getId()), CommonUtil.stringFormat(RedEnvelopesConstant.UID_LIST_OF_ENVELOPE_KEY_PREFFIX, shareInfoTO.getId())), Lists.newArrayList(reqTO.getUid().toString())); log.info("红包id:{},uid:{},lua执行结果:{}", shareInfoTO.getId(), reqTO.getUid(), scriptRes); if (Validator.isNull(scriptRes)) { return new GrabResTO(Boolean.FALSE, RedEnvelopesConstant.TIPS_FOR_EXCEPTION); } GrabResTO result = new GrabResTO(); // 根据执行结果进行后续处理 switch (scriptRes.intValue()) { case RedEnvelopesConstant.STATUS_REPEAT: result = new GrabResTO(Boolean.FALSE, RedEnvelopesConstant.TIPS_FOR_REPEAT_GRAB); break; // redis数据不存在,异常情况 || 红包已领完 case RedEnvelopesConstant.STATUS_NOT_EXSITS: case RedEnvelopesConstant.STATUS_END: result = new GrabResTO(Boolean.FALSE, RedEnvelopesConstant.TIPS_FOR_END); break; // 成功 case RedEnvelopesConstant.STATUS_LAST_SUCCESS: case RedEnvelopesConstant.STATUS_SUCCESS: // 如果采用文中的方案,后续还要加锁 result = this.getRedEnvelopeSuccessful(shareInfoTO, result, reqTO); break; default: log.error("lua脚本未知结果,shareInfoTO:{},scriptRes:{}", shareInfoTO, scriptRes); break; } return result; }
其他问题应该还好,如果错误请指出
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。