具体实现
库存表
--- 删除数据库
drop database seckill;
--- 创建数据库
create database seckill;
--- 使用数据库
use seckill;
--- 创建库存表
DROP TABLE IF EXISTS `t_seckill_stock`;
CREATE TABLE `t_seckill_stock` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '库存ID',
`name` varchar(50) NOT NULL DEFAULT 'OnePlus 7 Pro' COMMENT '名称',
`count` int(11) NOT NULL COMMENT '库存',
`sale` int(11) NOT NULL COMMENT '已售',
`version` int(11) NOT NULL COMMENT '乐观锁,版本号',
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='库存表';
--- 插入一条商品,初始化10个库存
INSERT INTO `t_seckill_stock` (`count`, `sale`, `version`) VALUES ('10', '0', '0');
--- 创建库存订单表
DROP TABLE IF EXISTS `t_seckill_stock_order`;
CREATE TABLE `t_seckill_stock_order` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`stock_id` int(11) NOT NULL COMMENT '库存ID',
`name` varchar(30) NOT NULL DEFAULT 'OnePlus 7 Pro' COMMENT '商品名称',
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='库存订单表';
核心接口
总体上分为3步
校验库存
扣库存,先更新db在更新缓存.
创建订单
应用层
/**
* 传统方式(名称注入)
*/
@Autowired
@Qualifier("seckillTraditionService")
private ISeckillService seckillTraditionService;
@Override
@Transactional(rollbackFor = Exception.class)
public Integer createWrongOrder(Integer id) throws Exception {
// 检查库存
StockDto stockDto = seckillTraditionService.checkStock(id);
// 扣库存
Integer saleCount = seckillTraditionService.saleStock(stockDto);
if (saleCount <= 0) {
throw new CustomException("扣库存失败");
}
// 下订单
Integer orderCount = seckillTraditionService.createOrder(stockDto);
if (saleCount <= 0) {
throw new CustomException("下订单失败");
}
return orderCount;
}服务层接口
package com.example.seckill;
import com.example.dto.custom.StockDto;
/**
* 统一接口
*
* @author wliduo[[email protected]]
* @date 2019-11-20 18:03:33
*/
public interface ISeckillService {
/**
* 检查库存
*
* @param 商品id
* @return com.example.dto.custom.StockDto
* @throws
* @author wliduo[[email protected]]
* @date 2019/11/20 20:22
*/
StockDto checkStock(Integer id);
/**
* 扣库存
*
* @param stockDto
* @return java.lang.Integer 操作成功条数
* @throws
* @author wliduo[[email protected]]
* @date 2019/11/20 20:24
*/
Integer saleStock(StockDto stockDto);
/**
* 下订单
*
* @param stockDto
* @return java.lang.Integer 操作成功条数
* @throws
* @author wliduo[[email protected]]
* @date 2019/11/20 20:26
*/
Integer createOrder(StockDto stockDto);
}具体实现类
使用redis的mget,mset原子性命令进行批量操作
package com.example.seckill.impl;
import com.example.constant.Constant;
import com.example.dao.StockDao;
import com.example.dao.StockOrderDao;
import com.example.dto.custom.StockDto;
import com.example.dto.custom.StockOrderDto;
import com.example.exception.CustomException;
import com.example.seckill.ISeckillService;
import com.example.util.JedisUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Transaction;
import java.util.List;
/**
* 使用乐观锁加缓存的方式(线程安全)(使用Redis批量操作mget和mset具有原子性)
*
* @author wliduo[[email protected]]
* @date 2019-11-20 18:03:33
*/
@Service("seckillOptimisticLockRedisSafeService")
public class SeckillOptimisticLockRedisSafeServiceImpl implements ISeckillService {
/**
* logger
*/
private static final Logger logger = LoggerFactory.getLogger(SeckillOptimisticLockRedisSafeServiceImpl.class);
@Autowired
private StockDao stockDao;
@Autowired
private StockOrderDao stockOrderDao;
@Override
public StockDto checkStock(Integer id) throws Exception {
// 使用缓存读取库存,减轻DB压力,Redis批量操作(具有原子性)解决线程安全问题
List<String> dataList = JedisUtil.mget(Constant.PREFIX_COUNT + id,
Constant.PREFIX_SALE + id, Constant.PREFIX_VERSION + id);
Integer count = Integer.parseInt(dataList.get(0));
Integer sale = Integer.parseInt(dataList.get(1));
Integer version = Integer.parseInt(dataList.get(2));
if (count > 0) {
// 还有库存
StockDto stockDto = new StockDto();
stockDto.setId(id);
stockDto.setCount(count);
stockDto.setSale(sale);
stockDto.setVersion(version);
Thread.sleep(10);
return stockDto;
}
throw new CustomException("库存不足");
}
@Override
public Integer saleStock(StockDto stockDto) throws Exception {
Integer saleCount = stockDao.updateByOptimisticLock(stockDto);
// 操作数据大于0,说明扣库存成功
if (saleCount > 0) {
logger.info("版本号:{} {} {}", stockDto.getCount(), stockDto.getSale(), stockDto.getVersion());
Thread.sleep(10);
// 更新缓存,这里更新需要保证三个数据(库存,已售,乐观锁版本号)的一致性,使用mset原子操作
updateCache(stockDto);
}
return saleCount;
}
/**
* 这里遵循先更新数据库,再更新缓存,详细的数据库与缓存一致性解析可以查看
* https://note.dolyw.com/cache/00-DataBaseConsistency.html
*
* @param stockDto
* @return java.lang.Integer
* @throws
* @author wliduo[[email protected]]
* @date 2019/11/22 16:25
*/
public void updateCache(StockDto stockDto) {
Integer count = stockDto.getCount() - 1;
Integer sale = stockDto.getSale() + 1;
Integer version = stockDto.getVersion() + 1;
JedisUtil.mset(Constant.PREFIX_COUNT + stockDto.getId(), count.toString(),
Constant.PREFIX_SALE + stockDto.getId(), sale.toString(),
Constant.PREFIX_VERSION + stockDto.getId(), version.toString());
}
@Override
public Integer createOrder(StockDto stockDto) {
StockOrderDto stockOrderDto = new StockOrderDto();
stockOrderDto.setStockId(stockDto.getId());
return stockOrderDao.insertSelective(stockOrderDto);
}
}db的乐观锁实现
/**
* 乐观锁更新扣减库存
*
* @param stockDto
* @return int
* @throws
* @author wliduo[[email protected]]
* @date 2019/11/22 14:14
*/
@Update("UPDATE t_seckill_stock SET count = count - 1, sale = sale + 1, version = version + 1 " +
"WHERE id = #{id, jdbcType = INTEGER} AND version = #{version, jdbcType = INTEGER}")
int updateByOptimisticLock(StockDto stockDto);
Last updated
Was this helpful?