它来了,它来了,它带着高并发走来了。
现在网络越来越发达,网民越来越多,直播带货越来越多,所以我觉得非常有必要学习下高并发技术。
解决高并发问题有很多种方法,我这里用到的主要技术是 RabbitMQ + Redis
首先介绍下 整体的高并发流程。
我们这个过程全程数据库都不参与,全程使用Redis来代替,这样就大大的提高了并发能力。因为Redis只基于内存的操作,非常快。
第一步、上架商品
代码有点多, 是因为我的项目逻辑比较复杂。
主要功能是:
1、把活动和对应的skuId保存到redis
2、把每个sku商品的库存保存到redis
3、把每个sku商品的信息保存到redis
/**
* 1、加锁
* 2、保证幂等性
*/
@Scheduled(cron =" */10 * * * * ?")
public void uploadSeckillSkuLatest3Days() {
log.info("上架秒杀商品。。。");
RLock lock = redissonClient.getLock(upload_lock);
try {
lock.lock(10, TimeUnit.SECONDS);
seckillService.uploadSeckillSkuLatest3Days();
}catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
private final String SESSION__CACHE_PREFIX = "seckill:sessions:";
private final String SECKILL_CHARE_PREFIX = "seckill:skus";
private final String SKU_STOCK_SEMAPHORE = "seckill:stock:"; //+商品随机码
@Override
public void uploadSeckillSkuLatest3Days() {
//1、扫描最近三天的商品需要参加秒杀的活动
R lates3DaySession = couponFeignService.getLates3DaySession();
if (lates3DaySession.getCode() == 0) {
//上架商品
List<SeckillSessionWithSkusVo> sessionData = (List<SeckillSessionWithSkusVo>) lates3DaySession.getData("data", new TypeReference<List<SeckillSessionWithSkusVo>>() {
});
//缓存到Redis
//1、缓存活动信息
saveSessionInfos(sessionData);
//2、缓存活动的关联商品信息
saveSessionSkuInfo(sessionData);
}
}
/**
* 缓存秒杀活动信息
* @param sessions
*/
private void saveSessionInfos(List<SeckillSessionWithSkusVo> sessions) {
if (CollectionUtils.isEmpty(sessions)) {
return;
}
sessions.stream().forEach(session -> {
//获取当前活动的开始和结束时间的时间戳
long startTime = session.getStartTime().getTime();
long endTime = session.getEndTime().getTime();
//存入到Redis中的key
String key = SESSION__CACHE_PREFIX + startTime + "_" + endTime;
//判断Redis中是否有该信息,如果没有才进行添加
Boolean hasKey = redisTemplate.hasKey(key);
//缓存活动信息
if (!hasKey) {
//获取到活动中所有商品的skuId
List<String> skuIds = session.getRelationSkus().stream()
.map(item -> item.getPromotionSessionId() + "-" + item.getSkuId().toString()).collect(Collectors.toList());
redisTemplate.opsForList().leftPushAll(key,skuIds);
}
});
}
/**
* 缓存秒杀活动所关联的商品信息
* @param sessions
*/
private void saveSessionSkuInfo(List<SeckillSessionWithSkusVo> sessions) {
if (CollectionUtils.isEmpty(sessions)) {
return;
}
sessions.stream().forEach(session -> {
//准备hash操作,绑定hash
BoundHashOperations<String, Object, Object> operations = redisTemplate.boundHashOps(SECKILL_CHARE_PREFIX);
session.getRelationSkus().stream().forEach(seckillSkuVo -> {
//生成随机码
String token = UUID.randomUUID().toString().replace("-", "");
String redisKey = seckillSkuVo.getPromotionSessionId().toString() + "-" + seckillSkuVo.getSkuId().toString();
if (!operations.hasKey(redisKey)) {
//缓存我们商品信息
SeckillSkuRedisTo redisTo = new SeckillSkuRedisTo();
Long skuId = seckillSkuVo.getSkuId();
//1、先查询sku的基本信息,调用远程服务
R info = productFeignService.getSkuInfo(skuId);
if (info.getCode() == 0) {
SkuInfoVo skuInfo = (SkuInfoVo) info.getData("skuInfo",new TypeReference<SkuInfoVo>(){});
redisTo.setSkuInfo(skuInfo);
}
//2、sku的秒杀信息
BeanUtils.copyProperties(seckillSkuVo,redisTo);
//3、设置当前商品的秒杀时间信息
redisTo.setStartTime(session.getStartTime().getTime());
redisTo.setEndTime(session.getEndTime().getTime());
//4、设置商品的随机码(防止恶意攻击)
redisTo.setRandomCode(token);
//序列化json格式存入Redis中
String seckillValue = JSON.toJSONString(redisTo);
operations.put(seckillSkuVo.getPromotionSessionId().toString() + "-" + seckillSkuVo.getSkuId().toString(),seckillValue);
//如果当前这个场次的商品库存信息已经上架就不需要上架
//5、使用库存作为分布式Redisson信号量(限流)
// 使用库存作为分布式信号量
RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + token);
// 商品可以秒杀的数量作为信号量
semaphore.trySetPermits(seckillSkuVo.getSeckillCount());
}
});
});
}
结果:
上架就完成。
第二步、抢购
直接上代码了,代码中有详细注释
@GetMapping("/kill")
@ResponseBody
public R kill(@RequestParam("killId") String killId,
@RequestParam("key") String key,
@RequestParam("num") Integer num,
Model model) throws InterruptedException {
String orderSn = seckillService.kill(killId,key,num);
return R.ok().setData(orderSn);
}
public String kill(String killId, String key, Integer num) throws InterruptedException {
long l1 = System.currentTimeMillis();
MemberResponse memberResponse = LoginUserInterceptor.loginUser.get(); // 线程直接获取用户信息
// 1、 redis中获取商品数据
BoundHashOperations<String, String, String> hasOps = redisTemplate.boundHashOps(SECKILL_CHARE_PREFIX);
String skuInfo = hasOps.get(killId);
if (StringUtils.isEmpty(skuInfo)) {
return null;
}
// 2、 简单校验、防止恶意请求
SeckillSkuRedisTo redisTo = JSON.parseObject(skuInfo, SeckillSkuRedisTo.class);
// 活动的开始结束时间
Long startTime = redisTo.getStartTime();
Long endTime = redisTo.getEndTime();
long currentTime = System.currentTimeMillis();
// 3、判断当前时间是否在秒杀时间段内
if (currentTime < startTime || currentTime > endTime) {
return null;
}
//4、效验随机码和商品id
String randomCode = redisTo.getRandomCode();
String skuId = redisTo.getPromotionSessionId() + "-" + redisTo.getSkuId();
if (!killId.equals(skuId) || !key.equals(randomCode)) {
// 校验失败,返回null
return null;
}
// 5、 暂时设定每个人只能抢购一次, 防止脚本疯狂刷接口
// redis key
String redisKey = memberResponse.getId() + "-" + skuId;
// redis 时间
long ttl = endTime - startTime;
// set nx ex
Boolean flag = redisTemplate.opsForValue().setIfAbsent(redisKey, num.toString(), ttl, TimeUnit.MILLISECONDS);
if (!flag) {
// 已经抢购过了
return null;
}
// 6、 使用信号量原子性,减库存
RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + randomCode);
boolean semaphoreCount = semaphore.tryAcquire(num, 100, TimeUnit.MILLISECONDS);
// 减库存是否成功
if (!semaphoreCount) {
// 没成功
return null;
}
//7、创建订单号和订单信息发送给MQ
String orderSn = IdWorker.getTimeId();
SeckillOrderTo seckillOrderTo = new SeckillOrderTo();
seckillOrderTo.setOrderSn(orderSn);
seckillOrderTo.setNum(num);
seckillOrderTo.setMemberId(memberResponse.getId());
seckillOrderTo.setPromotionSessionId(redisTo.getPromotionSessionId());
seckillOrderTo.setSkuId(redisTo.getSkuId());
seckillOrderTo.setSeckillPrice(redisTo.getSeckillPrice());
// 发消息。订单服务去处理
rabbitTemplate.convertAndSend("order-event-exchange", "order.seckill.order", seckillOrderTo);
long l2 = System.currentTimeMillis();
log.info("秒杀耗时...." + (l2 - l1));
return orderSn;
}
第三步、监听消息
先创建好我们的MQ中的队列、交换机和绑定关系。
/**
* 商品秒杀队列
* @return
*/
@Bean
public Queue orderSecKillOrrderQueue() {
Queue queue = new Queue("order.seckill.order.queue", true, false, false);
return queue;
}
@Bean
public Binding orderSecKillOrrderQueueBinding() {
Binding binding = new Binding(
"order.seckill.order.queue",
Binding.DestinationType.QUEUE,
"order-event-exchange",
"order.seckill.order",
null);
return binding;
}
创建监听
@RabbitHandler
public void listener(SeckillOrderTo orderTo, Channel channel, Message message) throws IOException {
log.info("开始创建秒杀订单信息》。。。");
try {
orderService.createSeckillOrderInfo(orderTo);
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
} catch (IOException e) {
channel.basicReject(message.getMessageProperties().getDeliveryTag(), true);
throw new RuntimeException(e);
}
}
@Override
@Transactional
public void createSeckillOrderInfo(SeckillOrderTo orderTo) {
// 保存订单信息
OrderEntity orderEntity = new OrderEntity();
orderEntity.setOrderSn(orderTo.getOrderSn());
orderEntity.setMemberId(orderTo.getMemberId());
orderEntity.setCreateTime(new Date());
BigDecimal totalPrice = orderTo.getSeckillPrice().multiply(BigDecimal.valueOf(orderTo.getNum()));
orderEntity.setPayAmount(totalPrice);
orderEntity.setStatus(OrderStatusEnum.CREATE_NEW.getCode());
//保存订单
this.save(orderEntity);
//保存订单项信息
OrderItemEntity orderItem = new OrderItemEntity();
orderItem.setOrderSn(orderTo.getOrderSn());
orderItem.setRealAmount(totalPrice);
orderItem.setSkuId(orderTo.getSkuId());
orderItem.setSkuQuantity(orderTo.getNum());
// todo 1
// 保存商品的spu信息
// 都是一样的逻辑,远程调用一下商品服务就可以了
// .......
// todo 2
// 地址可以先使用用户的默认地址, 当抢购成功后,用户可以再进行修改地址信息
// .....
//保存订单项数据
orderItemService.save(orderItem);
}
结果:
总结
核心思想就是使用Redis中的信号量来控制减库存,完成快速秒杀。
RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + token);
semaphore.trySetPermits(seckillSkuVo.getSeckillCount());
这样一个最简单的高并发下单减库存逻辑就完成了。看一下我本机测试的结果:
我本机秒杀一次,秒杀接口的处理速度是7ms, 我们把中间的网管转发,网络传输时间再加上,一次秒杀的请求大约可以是15ms,那么我们单个tomcat至少能够支持1000线程的并发,那么我自己这台电脑现在1秒钟能够支持的并发就达到了 1*1000ms/15ms*1000 = 6.666万。所以如果想要达到百万并发,我们可以提升服务器硬件质量或者集群部署就能轻松达到。