> 文章列表 > java单机秒杀扛1万并发方案和代码

java单机秒杀扛1万并发方案和代码

java单机秒杀扛1万并发方案和代码

我们先来看普通的加锁加事务秒杀性能,

说明:

1.这里的秒杀业务执行一次耗时100毫秒

2.电脑配置16g内存 4核8线程 cpu i7 7代,数据库连接池max=20 

@RequestMapping("/purchase2")public ResultJson purchase2( Long productId){int userId = new Random().nextInt(10000);UserInfo.setUserId(userId);RLock lock = redissonClient.getLock("LOCK");lock.lock();OrderVO purchase = null;try {purchase = tOrderService.purchase2(productId);if(purchase.isSuccess()){return new ResultJson<>(200, "成功下单", purchase);}else{return new ResultJson<>(500, purchase.getMsg(), purchase);}} catch (Exception e) {e.printStackTrace();}lock.unlock();return new ResultJson<>(500, "下单失败", null);}

100个商品 8000并发秒杀

1. 普通加锁加事务 100个商品8000并发,入库100条需要13秒,吞吐量一秒9次,这里jmeter只跑了2000个线程就停止了因为实在是太慢了不想等了

 

1000个商品,8000并发秒杀

1. 普通加锁加事务 1000个商品8000并发,入库1000条需要199秒这里jmeter只跑了2000个线程就停止了因为实在是太慢了不想等了

以下是我优化的方案,自定义了Seckill注解和ProductIdMark注解采用aop处理

    @RequestMapping("/purchase")@Seckill(tableName = "products", inventoryColumn = "prod_num" ,productIdColumn="id" )public ResultJson purchase(@ProductIdMark Long productId){OrderVO purchase = null;try {purchase = tOrderService.purchase(productId);if(purchase.isSuccess()){return new ResultJson<>(200, "成功下单", purchase);}else{return new ResultJson<>(500, purchase.getMsg(), purchase);}} catch (Exception e) {e.printStackTrace();}return new ResultJson<>(500, "下单失败", null);
}

100个商品 8000并发秒杀

1. 普通加锁加事务 100个商品8000并发,入库100条需要4秒,吞吐量一秒1034次

 

1000个商品 8000并发秒杀

1. 普通加锁加事务 1000个商品8000并发,入库1000条需要7秒,吞吐量一秒615次

 

分析

100商品优化前: 入库13秒,吞吐9/秒

100商品优化后: 入库4秒,吞吐1034/秒

1000商品优化前:入库199秒,吞吐9/秒

1000商品优化后:入库7秒,吞吐615/秒

可以看得出差别还是很大的

实现思路:所有请求被aop拦截,aop将用户存储到redis中,1000商品购买率是70%所以redis只要存储1500个用户的id即可,多余的直接返回商品售空,接下来轮到1500个用户竞争1000个商品,预热时候把1000个商品分成20份也就是每份50个商品,将20份商品存入库中作为lockName锁使用,同步轮训获取数据库中的lockName,获取到对应的lockName即可对该记录的库存进行扣减操作大致流程就是这样

下面是aop的主要实现:

@Aspect
@Configuration
public class SeckillAspect {@Autowiredprivate RedissonClient redissonClient;@Autowiredprivate ProductsSubsectionService productsSubsectionService;@Pointcut("@annotation(seckill)")public void pointCut(Seckill seckill) {}private final String infoKey= "info:productId:";private final String indexKey= "index:productId:";private final Long time=60*10L;@Around("pointCut(seckill)")public Object around(ProceedingJoinPoint joinPoint,Seckill seckill) throws Throwable {String className = joinPoint.getTarget().getClass().getName();String methodName = joinPoint.getSignature().getName();String appName = className +":"+ methodName;Long productId = getProductId(joinPoint);//模拟用户int userId = new Random().nextInt(100000);UserInfo.setUserId(userId);//初始化商品数据init(appName,productId);if(stopRun(appName,productId)){return new ResultJson<>(500, "商品售空", null);}//防止同一个用户使用外挂疯狂点击RMap<Integer, Integer> map = redissonClient.getMap(appName+":userClicks");RLock lockClicks = redissonClient.getLock("LOCK:"+appName+":5");try {lockClicks.lock();map.put(userId,map.get(userId)+1);}finally {lockClicks.unlock();}if(map.get(userId) > 1){return new ResultJson<>(500, "请勿重复提交", null);}RLock lock = redissonClient.getLock("LOCK:"+appName+":2");lock.lock();if(isUpdatePrimaryTable(appName,productId)){updatePrimaryTable(seckill,productId);}String lockName = getLockName(appName, productId);RLock lock4 = redissonClient.getLock(lockName);try {lock4.lock();lock.unlock();ProductsSubsection productsSubsection = productsSubsectionService.queryByLockMark(lockName);if(productsSubsection.getNumber()==0){return new ResultJson<>(500, "商品售空", null);}Object proceed = joinPoint.proceed();productsSubsection.setNumber(productsSubsection.getNumber()-1);productsSubsectionService.update(productsSubsection);return proceed;}finally {lock4.unlock();}}private Long getProductId(ProceedingJoinPoint joinPoint){Object[] args = joinPoint.getArgs();MethodSignature signature = (MethodSignature) joinPoint.getSignature();Method method = signature.getMethod();//一维数组是注解位置,二维数组是注解个数Annotation[][] parameterAnnotations = method.getParameterAnnotations();Long productId=null;boolean noProductId = true;for (int i = 0; i < parameterAnnotations.length; i++) {Annotation[] annotation = parameterAnnotations[i];for (Annotation var : annotation) {if (var.annotationType().equals(ProductIdMark.class)) {productId= (Long) args[i];noProductId = false;break;}}}if(noProductId){throw new RuntimeException("形参未指定注解:ProductId");}return productId;}private boolean init(String appName, Long productId){if(!redissonClient.getMap(appName).isEmpty()){//已初始化return false;}RLock lock = redissonClient.getLock("LOCK:"+appName+":2");try {lock.lock();if(redissonClient.getMap(appName).isEmpty()){List<ProductsSubsection> list = productsSubsectionService.queryByProdId(productId);int inventory =0 ;LinkedHashSet<String> locks = new LinkedHashSet<>();for (ProductsSubsection subsection : list) {inventory+=subsection.getNumber();locks.add(subsection.getLockMark());}CommodityInfo info = new CommodityInfo();info.setLockNames(locks);info.setInventory(inventory);RMap<String, Object> map = redissonClient.getMap(appName);map.expire(time, TimeUnit.SECONDS);String key= infoKey+ productId;String key2= indexKey+ productId;map.put(key,info);map.put(key2,0);}}finally {lock.unlock();}return true;}//统计用户点击数private Integer userClicks(String appName,int size){RMap<Integer, Integer> user = redissonClient.getMap(appName+":userClicks");user.expire(time, TimeUnit.SECONDS);RLock lock = redissonClient.getLock("LOCK:"+appName+":3");int userLength = user.size();if(userLength >= size){return user.size();}try {lock.lock();if(user.size() < size){//初始化用户点击次数int userId = UserInfo.getUserId();Integer clicks = user.get(userId);if( clicks == null){user.put(userId,0);}else{user.put(userId,clicks+1);}}} finally {lock.unlock();}return user.size();}private boolean stopRun(String appName,Long productId){RMap<String, Object> map = redissonClient.getMap(appName);map.expire(time, TimeUnit.SECONDS);CommodityInfo info = (CommodityInfo) map.get( infoKey+ productId);double size = info.getProbability() *  info.getInventory();RMap<Integer, Integer> user = redissonClient.getMap(appName+":userClicks");user.expire(time, TimeUnit.SECONDS);if(userClicks(appName, (int) size) >= size && !user.containsKey(UserInfo.getUserId())){return true;}return false;}private String getLockName(String appName,Long productId){String key= infoKey+ productId;String key2= indexKey+ productId;RMap<Object, Object> map = redissonClient.getMap(appName);map.expire(time, TimeUnit.SECONDS);CommodityInfo  info = (CommodityInfo) map.get(key);Integer index = (Integer) map.get(key2);List<String> lockNamesList = new ArrayList<>(info.getLockNames());map.put(key2,index+1);return lockNamesList.get(index % lockNamesList.size());}private boolean isUpdatePrimaryTable(String appName,Long productId){String key= infoKey+ productId;String key2= indexKey+ productId;RMap<Object, Object> map = redissonClient.getMap(appName);map.expire(time, TimeUnit.SECONDS);CommodityInfo  info = (CommodityInfo) map.get(key);Integer index = (Integer) map.get(key2);return index==info.getInventory();}//更新主表private void updatePrimaryTable(Seckill seckill,Long productId){// 获取注解的值String tableName = seckill.tableName();String inventoryColumn = seckill.inventoryColumn();String productIdColumn = seckill.productIdColumn();System.out.println("开始更新"+tableName+"主表数据");productsSubsectionService.updatePrimaryTable(tableName,inventoryColumn,productIdColumn,productId);}
}

项目地址:

通用所有秒杀业务,只要商品表中有库存,商品id即可

使用规则:

1.创建分段表

CREATE TABLE `products_subsection` (`id` bigint(20) NOT NULL AUTO_INCREMENT,`prod_id` bigint(20) DEFAULT NULL,`number` int(11) DEFAULT NULL,`lock_mark` varchar(20) DEFAULT NULL,`create_date` datetime DEFAULT NULL,PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=21 DEFAULT CHARSET=utf8mb4

2.在Controller的方法中使用Seckill注解和@ProductIdMark

//商品表名,库存字段名,商品id名
@Seckill(tableName = "products", inventoryColumn = "prod_num" ,productIdColumn="id" )
//用于表示商品id
@ProductIdMark

访问后的效果

架构图