秒杀商品表:t_seckill_goodsjava
CREATE TABLE `t_seckill_goods` (
`id` bigint NOT NULL,
`goods_name` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL,
`seckill_num` int DEFAULT NULL,
`price` decimal(10,2) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
复制代码
秒杀订单表:t_seckill_ordergit
CREATE TABLE `t_seckill_order` (
`id` bigint NOT NULL,
`seckill_goods_id` bigint DEFAULT NULL,
`user_id` bigint DEFAULT NULL,
`seckill_goods_name` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL,
`seckill_goods_price` decimal(10,2) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
复制代码
初始数据:redis
一个商品,库存10spring
掩饰代码结构使用的是 springboot+redis,其中使用了自定义异常,通用异常拦截,统一返回对象等sql
下载jmeter,修改配置文件 jmeter.properties数据库
language=zh_CN # 把语言改成中文
sampleresult.default.encoding=UTF-8 # 默认编码改成utf-8
复制代码
@Service
public class TSeckillGoodsServiceImpl extends ServiceImpl<TSeckillGoodsMapper, TSeckillGoods> implements TSeckillGoodsService {
@Autowired
private TSeckillGoodsMapper seckillGoodsMapper;
@Autowired
private TSeckillOrderMapper seckillOrderMapper;
@Override
@Transactional
public void seckill(SeckillDto seckillDto) {
//1 判断该商品是否存在
TSeckillGoods goods = seckillGoodsMapper.selectOne(new LambdaQueryWrapper<TSeckillGoods>()
.eq(TSeckillGoods::getId, seckillDto.getGoodsId()));
if (goods == null) {
throw new BusinessException(202, "商品不存在");
}
//2 判断该用户是否已经购买过
Integer boughtNum = seckillOrderMapper.selectCount(new LambdaQueryWrapper<TSeckillOrder>()
.eq(TSeckillOrder::getSeckillGoodsId, seckillDto.getGoodsId())
.eq(TSeckillOrder::getUserId, seckillDto.getUserId()));
if (boughtNum > 0) {
throw new BusinessException(201, "您已购买过该商品");
}
//3 减库存操做
int i = 0;
if (goods.getSeckillNum() > 0) {
goods.setSeckillNum(goods.getSeckillNum() - 1);
i =seckillGoodsMapper.updateById(goods);
}
if (i > 0) {
//4 插入订单
TSeckillOrder order = TSeckillOrder.builder()
.id(SnowIdUtils.nextId())
.seckillGoodsId(seckillDto.getGoodsId())
.userId(seckillDto.getUserId())
.seckillGoodsName(goods.getGoodsName())
.seckillGoodsPrice(goods.getPrice()).build();
int insertNum = seckillOrderMapper.insert(order);
if (insertNum > 0) {
System.out.println("发送mq,告诉用户尽快付款");
}
} else {
throw new BusinessException(203, "存库不足,请抢购其余商品");
}
}
}
复制代码
输出:设计模式
结论:安全
商品表没有超卖可是订单多了不少并且有重复购买现象,因此问题出现的位置在:springboot
//2 判断该用户是否已经购买过
Integer boughtNum = seckillOrderMapper.selectCount(new LambdaQueryWrapper<TSeckillOrder>()
.eq(TSeckillOrder::getSeckillGoodsId, seckillDto.getGoodsId())
.eq(TSeckillOrder::getUserId, seckillDto.getUserId()));
if (boughtNum > 0) {
throw new BusinessException(201, "您已购买过该商品");
}
//3 减库存操做
int i = 0;
if (goods.getSeckillNum() > 0) {
goods.setSeckillNum(goods.getSeckillNum() - 1);
i =seckillGoodsMapper.updateById(goods);
}
if (i > 0) {
//4 插入订单
TSeckillOrder order = TSeckillOrder.builder()
.id(SnowIdUtils.nextId())
.seckillGoodsId(seckillDto.getGoodsId())
.userId(seckillDto.getUserId())
.seckillGoodsName(goods.getGoodsName())
.seckillGoodsPrice(goods.getPrice()).build();
int insertNum = seckillOrderMapper.insert(order);
if (insertNum > 0) {
System.out.println("发送mq,告诉用户尽快付款");
}
} else {
throw new BusinessException(203, "存库不足,请抢购其余商品");
}
复制代码
多个线程同时判断当前用户没有购买过商品,而后数量>0,进而插入了订单markdown
@Override
@Transactional
public void seckill(SeckillDto seckillDto) {
//1 判断该商品是否存在
TSeckillGoods goods = seckillGoodsMapper.selectOne(new LambdaQueryWrapper<TSeckillGoods>()
.eq(TSeckillGoods::getId, seckillDto.getGoodsId()));
if (goods == null) {
throw new BusinessException(202, "商品不存在");
}
//2 判断该用户是否已经购买过
Integer boughtNum = seckillOrderMapper.selectCount(new LambdaQueryWrapper<TSeckillOrder>()
.eq(TSeckillOrder::getSeckillGoodsId, seckillDto.getGoodsId())
.eq(TSeckillOrder::getUserId, seckillDto.getUserId()));
if (boughtNum > 0) {
throw new BusinessException(201, "您已购买过该商品或商品不存在");
}
//3 减库存操做
//update t_seckill_goods set seckill_num = seckill_num -1 where seckill_num > 0 and id = #{id}
int i = seckillGoodsMapper.updateInventory(seckillDto.getGoodsId());
if (i > 0) {
//4 插入订单
TSeckillOrder order = TSeckillOrder.builder()
.id(SnowIdUtils.nextId())
.seckillGoodsId(seckillDto.getGoodsId())
.userId(seckillDto.getUserId())
.seckillGoodsName(goods.getGoodsName())
.seckillGoodsPrice(goods.getPrice()).build();
int insertNum = seckillOrderMapper.insert(order);
if (insertNum > 0) {
System.out.println("发送mq,告诉用户尽快付款");
}
} else {
throw new BusinessException(203, "存库不足,请抢购其余商品");
}
}
复制代码
输出:
结论:
修改了第三个步骤,把多步操做改成一条sql原子操做后,没有了超卖现象;可是任有重复购买问题,主要就是第二步"判断该用户是否已经购买过"有线程安全问题
@Override
@Transactional
public void seckill(SeckillDto seckillDto) {
//1 判断该商品是否存在
TSeckillGoods goods = seckillGoodsMapper.selectOne(new LambdaQueryWrapper<TSeckillGoods>()
.eq(TSeckillGoods::getId, seckillDto.getGoodsId()));
if (goods == null) {
throw new BusinessException(202, "商品不存在");
}
//2 判断该用户是否已经购买过
Integer boughtNum = seckillOrderMapper.selectCount(new LambdaQueryWrapper<TSeckillOrder>()
.eq(TSeckillOrder::getSeckillGoodsId, seckillDto.getGoodsId())
.eq(TSeckillOrder::getUserId, seckillDto.getUserId()));
if (boughtNum > 0) {
throw new BusinessException(201, "您已购买过该商品或商品不存在");
}
//3 减库存操做
//update t_seckill_goods set seckill_num = seckill_num -1 where seckill_num > 0 and id = #{id}
int i = seckillGoodsMapper.updateInventory(seckillDto.getGoodsId());
//4 插入订单
if (i > 0) {
//模拟volatile单例设计模式,双重校验;这里的冗余代码就懒得改了
boughtNum = seckillOrderMapper.selectCount(new LambdaQueryWrapper<TSeckillOrder>()
.eq(TSeckillOrder::getSeckillGoodsId, seckillDto.getGoodsId())
.eq(TSeckillOrder::getUserId, seckillDto.getUserId()));
if (boughtNum > 0) {
throw new BusinessException(201, "您已购买过该商品或商品不存在");
}
TSeckillOrder order = TSeckillOrder.builder()
.id(SnowIdUtils.nextId())
.seckillGoodsId(seckillDto.getGoodsId())
.userId(seckillDto.getUserId())
.seckillGoodsName(goods.getGoodsName())
.seckillGoodsPrice(goods.getPrice()).build();
int insertNum = seckillOrderMapper.insert(order);
if (insertNum > 0) {
System.out.println("发送mq,告诉用户尽快付款");
}
} else {
throw new BusinessException(203, "存库不足,请抢购其余商品");
}
}
复制代码
输出:
为了增长出错率,我调大了库存容量为20
结论:
这一版改动是在第四步,模拟volatile单例设计模式的双重校验;能够看得出来重复购买概率降低了不少,可是任然有,因此并不能彻底的解决问题
@Override
@Transactional
public void seckill(SeckillDto seckillDto) {
//1 判断该商品是否存在
TSeckillGoods goods = seckillGoodsMapper.selectOne(new LambdaQueryWrapper<TSeckillGoods>()
.eq(TSeckillGoods::getId, seckillDto.getGoodsId()));
if (goods == null) {
throw new BusinessException(202, "商品不存在");
}
final String key = "lock:" + seckillDto.getUserId() + "-" + seckillDto.getGoodsId();
RLock lock = redissonClient.getLock(key);
try {
//默认30s的redis过时时间
lock.lock();
//2 判断该用户是否已经购买过
Integer boughtNum = seckillOrderMapper.selectCount(new LambdaQueryWrapper<TSeckillOrder>()
.eq(TSeckillOrder::getSeckillGoodsId, seckillDto.getGoodsId())
.eq(TSeckillOrder::getUserId, seckillDto.getUserId()));
if (boughtNum > 0) {
throw new BusinessException(201, "您已购买过该商品或商品不存在");
}
//3 减库存操做
if (seckillGoodsMapper.updateInventory(seckillDto.getGoodsId()) > 0) {
//4 插入订单
TSeckillOrder order = TSeckillOrder.builder()
.id(SnowIdUtils.nextId())
.seckillGoodsId(seckillDto.getGoodsId())
.userId(seckillDto.getUserId())
.seckillGoodsName(goods.getGoodsName())
.seckillGoodsPrice(goods.getPrice()).build();
int insertNum = seckillOrderMapper.insert(order);
if (insertNum > 0) {
System.out.println("发送mq,告诉用户尽快付款");
}
} else {
throw new BusinessException(203, "存库不足,请抢购其余商品");
}
} finally {
lock.unlock();
}
}
复制代码
输出:
结论:
这一版,使用了redisson框架来作分布式锁,测试了好些次没有问题;
为何不适用redis来作分布式锁呢?
缘由主要仍是redis没有智能处理过时时间的功能,依旧会引起线程安全甚至死锁问题;那redisson就没有
难道redisson就没有问题了吗?
在redis主从架构下,若是master宕机时没有同步数据到salve中,依旧仍是会出现问题,可是概率很是小,因此redisson只能保证ap(partition tolerance分区容错性),没法保证consistency(一致性);使用zookeeper能够解决数据一致性问题,可是avaliability(可用性)会差一些
补充:
为何不把锁的范围只控制在第二步呢?问题的发生不就是插入了重复数据吗?