基于springboot开发的商品秒杀系统所遇到的高并发问题
使用jmeter进行测试所遇到的各种高并发带来的问题:
原始业务代码:
/**
* 测试用下单
* @param itemId 商品id
* @param userId 用户id
* @return true false
*/
@Override
public 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>>() {
@Override
public <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;
}