
1、背景
系统设计首先我们要考虑几个问题:
2、系统高并发瓶颈会出现在哪里?
4、如何应对羊毛党缛羊毛问题?
系统现有基座层面采用SpringCloud微服务技术框架,SLB负载,应用层面可以加多个应用节点,实现水平扩展,应用服务压力可以有效分解(应用服务基础架构方面基本固化,可改造空间有限);
2、解决思路
上游限流。
1、页面静态化,就是将整个页面静态化放到OSS或CDN节点中(前端是小程序,整体页面不好做静态化,只能在图片、JS、CSS等方面做点工作)。
2、防止前端操作频繁或重复提交,可以将短时间内同一个用户多次请求合并,也可以分批暂停机制或加数学验证码:用户在计算验证码结果时可以减少大量请求同时进入,减少redis, mysql,服务器的压力。
3、采用多级缓存机制,有一些不常变化字典可以缓存在前端;后端应用程序做多级缓存机制,
b、第二级缓存:应用服务器本地缓存Ehcache本地磁盘化缓存一些关键数据;
例:
boolean over = map.get(goodsId;
if(over { return Result.error(‘库存不足’; }
当我们map通过key读取到value值为true的时候,就返回错误提示给用户,这样不管以后有多个请求进入都只运行两行代码,后面的操作无法进入。
4、防刷规则限定
5、redis预减库存
6、加入MQ消息队列
7、采用负载均衡Nginx
3、高并发设计
1、前端做好频繁重复提交策略(如合并提交),这是限流的第一步;
3、接口层面做好一些业务限流,比如同一个用户多次提交去重合并,抽过的不给再抽,可以有效防止程序机器刷单情况;
5、利用本地服务器内存标记当用作二级缓存,当redis库存<0时把一些无效请求过滤,减少redis访问压力。
抢红包处理流程图
前端和后端接口交互逻辑
1、前端摇一摇发起接口请求,后端接口做一些规则校验之后,快速返回并附带异步任务查询ID即:jobId。
3、暂停3/5分钟后再摇如果上次已经抽中,这次再摇,根据业务要求,后端不入抽奖队列,即不给再抽中的机会,直接返回提示给前端。如果上次没有抽中,可以再次入抽奖队列排队再抽。
抢红包处理流程时序图
4、代码实现
秒杀或抢红包实现代码片断,标注说明,代码中有很多业务操作(写入、查询等),当时写的代码优雅性较差,不要看代码优雅性,读者可以不管它,只需要理解高并发处理思路即可。
/**
* 限时红包雨,红包抽奖
*
* @param con 前端提交的业务参数
* @param request 用于获取请求头信息
*/
public ResponseData onRedPackageRain(ActivityPrizeRainCondition con, HttpServletRequest request {
//各种校验,校验必填参数
this.validateParam(con;
//校验签名
this.checkAsign(con, request;
//活动轮数id
String liveActivityRoundsId = con.getLiveActivityRoundsId(;
String ip = NetworkUtils.getClientIp(request;
//同openID或ip限流
this.rateLimit(con.getOpenid(, ip;
//校验是否在活动时间执行
this.checkedActivityTime(liveActivityRoundsId;
//所有奖品抽完,内存标识,减少Redis访问
Assert.isTrue(!localFlag, "奖品已抽完了,请等下一轮";
//校验当天抽奖次数
this.checkedLotteryNum(liveActivityRoundsId, con.getOpenid(, 86400L;
//取缓存有奖品数量的奖品 随机抽,某一轮红包轮数id前缀
String prefix_key = GlobalConstant.WECHAT_LIVE_ACTIVITY_ROUNDS + liveActivityRoundsId + ":";
Set<String> keys = stringRedisTemplate.keys(prefix_key + "*";
Assert.isTrue(!CollectionUtils.isEmpty(keys, "没有奖品啦";
//可抽奖的奖品id
List<String> newKeys = new ArrayList<>(;
for (String key : keys {
String mapKey = key.substring(key.lastIndexOf(":" + 1;
String mapValue = stringRedisTemplate.opsForValue(.get(key;
if (StringUtils.isNotEmpty(mapValue && Integer.parseInt(mapValue > 0 {
newKeys.add(mapKey;
}
}
if (CollectionUtils.isEmpty(newKeys {
localFlag = true;
Assert.isTrue(false, "奖品已抽完了,请等下一轮";
}
/*随机抽一个奖品id*/
Integer rand = RandomUtils.nextInt(newKeys.size(;
String prizeLotteryId = newKeys.get(rand;
con.setPrizeNumber(prizeLotteryId;
con.setIp(ip;
//符合条件的用户请求放入MQ队列
rabbitTemplate.convertAndSend(MIAOSHA_QUEUE, con;
LOG.info("-----------红包雨抽到奖品,加入队列:{}", con.toString(;
return renderSuccess("具备秒杀资格";
}
/**
* 异步消费抽中红包队列
* @param con 活动请求业务参数
* @param message MQ消息对象
* @param channel MQ通道对象
*/
@RabbitListener(queues = MIAOSHA_QUEUE
public void consumeMessage(ActivityPrizeRainCondition con, Message message, Channel channel {
try {
LOG.info("rabbitmq message consume======={}", con.toString(;
//设置最大服务消息数量,避免消息处理不过来,全部堆积在本地缓存里
// 会告诉RabbitMQ不要同时给一个消费者推送多于N个消息,即一旦有N个消息还没有ack,则该consumer将block掉,直到有消息ack
// channel.basicQos(0,5,false;
//确认应答信息
channel.basicAck(message.getMessageProperties(.getDeliveryTag(, false;
//执行红包发放流程
this.doRedPackageRain(con;
}catch (Exception e{
LOG.error("Message consume ERROR!",e;
}
}
/**
* 红包发放关键执行程序
* @param con 业务请求参数
*/
private void doRedPackageRain(ActivityPrizeRainCondition con{
//初始中奖金额
double randomMoney = 0;
String liveActivityRoundsId= con.getLiveActivityRoundsId(;
String prizeId = con.getPrizeNumber(;
String prefix_key = GlobalConstant.WECHAT_LIVE_ACTIVITY_ROUNDS + liveActivityRoundsId+ ":"+prizeId;
String prizeNum = stringRedisTemplate.opsForValue(.get(prefix_key;
//入队后再次校验Redis是否有库存
if(StringUtils.isEmpty(prizeNum||Integer.parseInt(prizeNum<=0{
//再次 检查缓存所有奖品是否还有库存 有的随机抽
// 某一轮轮数id 前缀
String prefix_key2 = GlobalConstant.WECHAT_LIVE_ACTIVITY_ROUNDS + liveActivityRoundsId+ ":";
Set<String> keys = stringRedisTemplate.keys(prefix_key2+"*";
if(CollectionUtils.isEmpty(keys