springboot商品秒杀系统遇到的高并发问题
基于springboot开发的商品秒杀系统所遇到的高并发问题
使用jmeter进行测试所遇到的各种高并发带来的问题:
原始业务代码:
/*** 测试用下单* @param itemId 商品id* @param userId 用户id* @return true false*/@Overridepublic boolean testKill(Long itemId, Long userId) {SecItem item = selectItemByItemId(itemId);if(!boughtOrNot(userId,itemId)) {if (userId!=null && item != null) {if (item.getIsok() == '1') {return generateOrder(item, userId);}}log.warn("testKill----userId或item为Null");}return false;}/*** 判断是否买过同一个商品* @param userId 用户id* @param itemId 商品id* @return true false*/private boolean boughtOrNot(Long userId, Long itemId) {List<SecOrder> orders = orderMapper.selectOrderByUserId(userId);if (orders!=null&&orders.size()!=0) {for (SecOrder order : orders) {if (order.getItemId().longValue() == itemId) {log.info("boughtOrNot----找到订单");return true;}}log.warn("boughtOrNot----没有找到对应itemId");return false;}return false;}/*** 产生订单* @param item 商品信息* @param userId 用户id* @return 结果*/public boolean generateOrder(SecItem item,Long userId){SecOrder order=new SecOrder();order.setOrderId(SecKillUtils.getOrdeIdBySnow());order.setState('0');order.setCreateTime(new Date());order.setItemId(item.getItemId());order.setUserId(userId);order.setPrice(item.getPrice());if (updateStock(item, 1)){orderMapper.insertOrder(order);//发送邮件消息//sendService.sendMsg(order.getOrderId());//发送订单消息到死信队列sendService.sendDeadMsg(order.getOrderId());log.info("generateOrder----成功");return true;}log.warn("generateOrder----updateStock返回false");return false;}/*** 更新库存* @param item 商品信息* @param i 改变库存的数量* @return 是否修改成功*/private boolean updateStock(SecItem item, int i) {if (item!=null) {if(item.getItemStock()>0) {item.setItemStock(item.getItemStock() - i);int result = itemMapper.updateItem(item);if (result > 0) {log.info("generateOrder----成功");return true;}log.warn("updateStock----item更新失败,受影响行数为:"+result);return false;}log.warn("updateStock----库存小于0");return false;}log.warn("updateStock----item为Null");return false;}
问题1.1000个用户同时下单,只有99个下单成功
控制台输出情况如下,推测应该是高并发的情况下,mysql数据库更新库存数据失败。

想到的解决思路
使用redis来对商品库存进行一个缓存,然后在redis中使用decr这一个操作来减少库存,当redis库存变成0的时候,更新数据库的库存。同时,用户下单成功后,在redis服务器中存储一个唯一标识,这样,进行秒杀时就不需要从数据库中查询订单了,直接在redis中查看是否有对应的key就行。
主要改变的是更新库存的方法,如下:
private boolean updateStock(Long itemId, int i) {String stockKey=MyproCostant.REDIS_STOCK_KEY+itemId;if (itemId!=null) {//判断是否有库存数据if (redisTemplate.hasKey(stockKey)) {Long stock = Long.valueOf(String.valueOf(redisTemplate.opsForValue().get(stockKey)));if (stock>0) {//库存减一Long decr_stock = redisTemplate.opsForValue().decrement(stockKey);//再次判断if (decr_stock>=0){//库存为空时更新数据库if (decr_stock==0){SecItem item = itemMapper.selectItemByItemId(itemId);item.setItemStock(0L);int result = itemMapper.updateItem(item);if (result!=1){log.warn("库存更新失败");}else {log.info("库存已经为0");//删除缓存的商品信息,使redis重新获取数据库的新数据redisTemplate.delete(MyproCostant.REDIS_ITEM_KEY);}}return true;}}}}return false;}
在redis中存储的数据如下:

这种方法,测试到5000并发的时候,发现有一个userID下了两个单,在redis中order标识只有999个,在mysql中的确有一个userId有俩订单。


导致这样的原因应该是,同时有俩线程携带同一个userid同时进行一个插入订单的操作,解决方法想到使用redis的setnx来设置标识,修改后的更新库存代码如下:
private boolean updateStock(Long itemId,Long userId, int i) {String stockKey=MyproCostant.REDIS_STOCK_KEY+itemId;if (itemId!=null) {//判断是否有库存数据if (redisTemplate.hasKey(stockKey)) {Long stock = Long.valueOf(String.valueOf(redisTemplate.opsForValue().get(stockKey)));if (stock>0) {//库存减一Long decr_stock = redisTemplate.opsForValue().decrement(stockKey,i);//再次判断if (decr_stock>=0){//插入购买标识,如果已存在if (redisTemplate.opsForValue().setIfAbsent("order:"+userId+":"+itemId,1)) {//库存为空时更新数据库if (decr_stock==0){SecItem item = itemMapper.selectItemByItemId(itemId);item.setItemStock(0L);int result = itemMapper.updateItem(item);if (result!=1){log.warn("库存更新失败");}else {log.info("库存已经为0");//删除缓存的商品信息,使redis重新获取数据库的新数据redisTemplate.delete(MyproCostant.REDIS_ITEM_KEY);}}return true;}else {//如果购买标识已存在,则库存回退redisTemplate.opsForValue().increment(stockKey,i);}}}}}return false;}
这种方法解决问题后,经过测试,发现redis的库存出现超卖的情况。
问题2.解决超卖
在redis的库存减少时同时有多个线程执行减少操作,导致超卖问题,解决思路为使用redis的乐观锁(watch) 监视库存的key,然后使用事务进行减少库存操作。
修改过的更新库存方法的代码如下:
private boolean updateStock(Long itemId, Long userId, int i) {String stockKey = MyproCostant.REDIS_STOCK_KEY + itemId;if (itemId != null) {//判断是否有库存数据if (redisTemplate.hasKey(stockKey)) {Long stock = redisUtils.getLong(stockKey);if (stock != null && stock > 0) {//redis事务List<Object> results = redisTemplate.execute(new SessionCallback<List<Object>>() {@Overridepublic <K, V> List<Object> execute(RedisOperations<K, V> operations) throws DataAccessException {redisTemplate.watch(stockKey);operations.multi();redisTemplate.opsForValue().decrement(stockKey, i);return operations.exec();}});//事务提交成功则结果不为空if (results != null && !results.isEmpty()) {Long decr_stock = Long.parseLong(String.valueOf(redisTemplate.opsForValue().get(stockKey)));if (decr_stock >= 0) {//插入购买标识,如果已存在if (redisTemplate.opsForValue().setIfAbsent("order:" + userId + ":" + itemId, 1, Duration.ofMinutes(30))) {//库存为空时更新数据库if (decr_stock == 0) {SecItem item = itemMapper.selectItemByItemId(itemId);item.setItemStock(0L);int result = itemMapper.updateItem(item);if (result != 1) {log.warn("库存更新失败");} else {log.info("库存已经为0");//删除缓存的库存数据redisTemplate.delete(stockKey);//删除缓存的商品信息redisTemplate.delete(MyproCostant.REDIS_ITEM_KEY);}}return true;} else {//如果购买标识已存在,则库存回退redisTemplate.opsForValue().increment(stockKey, i);}}}}}}return false;}
本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!
