前提了解!
秒杀肯定离不开电商,那么需要了解用户下单的过程.
1、当用户下单时,会把下单的信息存入一张表中,表中有一个字段用于判断该订单当前的状态.
订单表
CREATE TABLE `tb_voucher_order` (
`id` bigint NOT NULL COMMENT '主键',
`user_id` bigint unsigned NOT NULL COMMENT '下单的用户id',
`voucher_id` bigint unsigned NOT NULL COMMENT '购买的代金券id',
`pay_type` tinyint unsigned NOT NULL DEFAULT '1' COMMENT '支付方式 1:余额支付;2:支付宝;3:微信',
`status` tinyint unsigned NOT NULL DEFAULT '1' COMMENT '订单状态,1:未支付;2:已支付;3:已核销;4:已取消;5:退款中;6:已退款',
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '下单时间',
`pay_time` timestamp NULL DEFAULT NULL COMMENT '支付时间',
`use_time` timestamp NULL DEFAULT NULL COMMENT '核销时间',
`refund_time` timestamp NULL DEFAULT NULL COMMENT '退款时间',
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=COMPACT;
2、主键存在的问题。
该表中的主键id不能够是自增。会导致一些问题:
解决方案:使用全局唯一id生成器,它应该具有的特性:
唯一性
高可用(分布式中也可以用)
高性能(能够快速生成)
递增性(保证数据库能够高效的查询)
安全性
3、全局唯一id生成策略:
符号位(0或1) + 时间戳(31 Bit)+ 序列号(32 Bit)
redis自增策略:
1、自定义起始时间戳
public void test02() {
//生成自定义时间秒
LocalDateTime now = LocalDateTime.of(2021,8,24,0,0,0);
long second = now.toEpochSecond(ZoneOffset.UTC);
System.out.println(second);
}
//结果得:1629763200
2、全局唯一id生成器编写
/**
* @Author: songshu
* @Date: 2022/06/19/13:11
* @version: v1.0
*/
public class RedisIdWorker {
//开始时间戳
private static final long BEGIN_TIMESTAMP = 1629763200L;
//序列号位数
private static final int COUNT_BITS = 32;
private StringRedisTemplate redisTemplate;
public RedisIdWorker(StringRedisTemplate stringRedisTemplate) {
this.redisTemplate = stringRedisTemplate;
}
public long nextId(String keyPrefix) {
//生成时间戳
//当前时间
LocalDateTime now = LocalDateTime.now();
long nowSecond = now.toEpochSecond(ZoneOffset.UTC);
long timestamp = nowSecond - BEGIN_TIMESTAMP;
//生成序列号
//定义规则,每天一个key,方便统计
String data = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
long count = redisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + data);
//拼接并返回
//时间戳左移32位,并与序列号或(前面的32位就留给了序列号)
//可能count也会超过32位,所以定义COUNT_BITS
return timestamp << COUNT_BITS | count;
}
}
3、测试
@Autowired
private StringRedisTemplate redisTemplate;
private ExecutorService es = Executors.newFixedThreadPool(500);
@Test
public void test03() throws InterruptedException {
//记时器
CountDownLatch countDownLatch = new CountDownLatch(300);
RedisIdWorker redisIdWorker = new RedisIdWorker(redisTemplate);
Runnable task = () -> {
for(int i = 0; i < 100; i++) {
long id = redisIdWorker.nextId("order");
System.out.println(id);
}
countDownLatch.countDown();
};
long begin = System.currentTimeMillis();
//任务提交300次
for (int i = 0; i < 300; i++) {
es.submit(task);
}
countDownLatch.await();
long end = System.currentTimeMillis();
System.out.println("time = " + (end - begin));
}
查看reids中得到:
key为: icr:order:2022:06:19
value为: 30000
优惠卷表
CREATE TABLE `tb_voucher` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
`shop_id` bigint unsigned DEFAULT NULL COMMENT '商铺id',
`title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '代金券标题',
`sub_title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '副标题',
`rules` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '使用规则',
`pay_value` bigint unsigned NOT NULL COMMENT '支付金额,单位是分。例如200代表2元',
`actual_value` bigint NOT NULL COMMENT '抵扣金额,单位是分。例如200代表2元',
`type` tinyint unsigned NOT NULL DEFAULT '0' COMMENT '0,普通券;1,秒杀券',
`status` tinyint unsigned NOT NULL DEFAULT '1' COMMENT '1,上架; 2,下架; 3,过期',
`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`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=COMPACT;
库存表
CREATE TABLE `tb_seckill_voucher` (
`voucher_id` bigint unsigned NOT NULL COMMENT '关联的优惠券的id',
`stock` int NOT NULL COMMENT '库存',
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`begin_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '生效时间',
`end_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '失效时间',
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`voucher_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=COMPACT COMMENT='秒杀优惠券表,与优惠券是一对一关系';
注意点:
- 判断秒杀是否开始或结束
- 库存是否充足
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {
@Autowired
private ISeckillVoucherService seckillVoucherService;
@Autowired
private StringRedisTemplate redisTemplate;
@Override
@Transactional
public Result seckillVoucher(Long voucherId) {
// 获取优惠开始时间和结束时间
SeckillVoucher seckillVoucher = seckillVoucherService.getById(voucherId);
//判断是否开始
if(seckillVoucher.getBeginTime().isAfter(LocalDateTime.now())) {
//尚未开始
return Result.fail("秒杀尚未开始!");
}
//判断是否结束
if(seckillVoucher.getEndTime().isBefore(LocalDateTime.now())) {
//已结束
return Result.fail("秒杀已经结束!");
}
//判断库存是否充足
if(seckillVoucher.getStock() < 1) {
//库存不足
return Result.fail("秒杀卷库存不足!");
}
//符合以上要求,可以下单
//库存减一
LambdaUpdateWrapper<SeckillVoucher> voucherUpdateWrapper = new LambdaUpdateWrapper<>();
voucherUpdateWrapper.eq(SeckillVoucher::getVoucherId, voucherId);
voucherUpdateWrapper.setSql("stock = stock-1");
boolean update = seckillVoucherService.update(voucherUpdateWrapper);
if(!update) {
return Result.fail("下单失败!");
}
VoucherOrder voucherOrder = new VoucherOrder();
//生成订单id
RedisIdWorker redisIdWorker = new RedisIdWorker(redisTemplate);
long orderId = redisIdWorker.nextId("order");
voucherOrder.setId(orderId);
//用户id
Long userId = UserHolder.getUser().getId();
voucherOrder.setUserId(userId);
//代金券id
voucherOrder.setVoucherId(voucherId);
boolean flag = save(voucherOrder);
if(!flag) {
return Result.fail("下单失败!");
}
return Result.ok(orderId);
}
}
该方法存在高并发问题!!!
需要优化!
解决方案:
1、通过锁解决
- 悲观锁
- Syschronized
- Lock
- 乐观锁
- 数据库加字段 version(版本号法)
乐观锁解决高并发
修改库存表,添加version字段:
ALTER TABLE `tb_seckill_voucher`
ADD COLUMN `version` int(4) NULL DEFAULT 0 COMMENT '乐观锁';
MyBatis-Plus中添加乐观锁插件
注意:
- 仅支持
updateById(id)
与update(entity, wrapper)
方法- 在
update(entity, wrapper)
方法下,wrapper
不能复用!!!
@Configuration
public class MybatisConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
//分页
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
// 乐观锁
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
return interceptor;
}
}
修改下单代码
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {
@Autowired
private ISeckillVoucherService seckillVoucherService;
@Autowired
private StringRedisTemplate redisTemplate;
@Override
@Transactional
public Result seckillVoucher(Long voucherId) {
// 获取优惠开始时间和结束时间
SeckillVoucher seckillVoucher = seckillVoucherService.getById(voucherId);
//判断是否开始
if(seckillVoucher.getBeginTime().isAfter(LocalDateTime.now())) {
//尚未开始
return Result.fail("秒杀尚未开始!");
}
//判断是否结束
if(seckillVoucher.getEndTime().isBefore(LocalDateTime.now())) {
//已结束
return Result.fail("秒杀已经结束!");
}
//判断库存是否充足
if(seckillVoucher.getStock() < 1) {
//库存不足
return Result.fail("秒杀卷库存不足!");
}
//符合以上要求,可以下单
//库存减一
SeckillVoucher byId = seckillVoucherService.getById(voucherId);
UpdateWrapper<SeckillVoucher> updateWrapper = new UpdateWrapper<>();
updateWrapper.setSql("stock = stock-1");
boolean update = seckillVoucherService.update(byId, updateWrapper);
if(!update) {
return Result.fail("下单失败!");
}
VoucherOrder voucherOrder = new VoucherOrder();
//生成订单id
RedisIdWorker redisIdWorker = new RedisIdWorker(redisTemplate);
long orderId = redisIdWorker.nextId("order");
voucherOrder.setId(orderId);
//用户id
Long userId = UserHolder.getUser().getId();
voucherOrder.setUserId(userId);
//代金券id
voucherOrder.setVoucherId(voucherId);
boolean flag = save(voucherOrder);
if(!flag) {
return Result.fail("下单失败!");
}
return Result.ok(orderId);
}
注意这里会出现问题!
会有大量线程同时拿到同一个version,这就会导致乐观锁的成功率低!!!
乐观锁的特点:
- 成功率低,效率高
由于是抢票,所以可以直接判断库存是否大于0即可,代码不演示。
解决方式:加悲观锁
会遇到的问题:
问题一:
//加悲观锁,
//userid.toString() 底层是重新new一个对象,所以需要使用intern()方法到常量池中获取.问题二:
//下面方法的事务不能够启用
//由于createVoucherOrder是VoucherOrderServiceImpl的对象,这里调用的是this,
//而不是VoucherOrderServiceImpl的代理对象
//由于spring需要代理对象才能够启用事务
引依赖
<dependency>
<groupId>org.aspectjgroupId>
<artifactId>aspectjweaverartifactId>
dependency>
在启动类上加注解
//暴露代理对象
@EnableAspectJAutoProxy(exposeProxy = true)
代码:
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {
@Autowired
private ISeckillVoucherService seckillVoucherService;
@Autowired
private StringRedisTemplate redisTemplate;
@Override
public Result seckillVoucher(Long voucherId) {
// 获取优惠开始时间和结束时间
SeckillVoucher seckillVoucher = seckillVoucherService.getById(voucherId);
//判断是否开始
if(seckillVoucher.getBeginTime().isAfter(LocalDateTime.now())) {
//尚未开始
return Result.fail("秒杀尚未开始!");
}
//判断是否结束
if(seckillVoucher.getEndTime().isBefore(LocalDateTime.now())) {
//已结束
return Result.fail("秒杀已经结束!");
}
//判断库存是否充足
if(seckillVoucher.getStock() < 1) {
//库存不足
return Result.fail("秒杀卷库存不足!");
}
//解决一人一票
Long userid = UserHolder.getUser().getId();
//加悲观锁,
//userid.toString() 底层是重新new一个对象,所以需要使用intern()方法到常量池中获取
synchronized (userid.toString().intern()) {
//下面方法的事务不能够启用
//由于createVoucherOrder是VoucherOrderServiceImpl的对象,这里调用的是this,
//而不是VoucherOrderServiceImpl的代理对象
//由于spring需要代理对象才能够启用事务
IVoucherOrderService currentProxy = (IVoucherOrderService) AopContext.currentProxy();
return currentProxy.createVoucherOrder(voucherId, userid);
}
}
@Transactional
public Result createVoucherOrder(Long voucherId, Long userid) {
// 判断是否存在该用户够的票
LambdaQueryWrapper<VoucherOrder> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(VoucherOrder::getUserId, userid)
.eq(VoucherOrder::getVoucherId, voucherId);
VoucherOrder one = getOne(queryWrapper);
if(BeanUtil.isNotEmpty(one)) {
return Result.fail("不可重复抢购!");
}
//符合以上要求,可以下单
//库存减一
UpdateWrapper<SeckillVoucher> updateWrapper = new UpdateWrapper<>();
updateWrapper.setSql("stock = stock-1").gt("stock",0);
boolean update = seckillVoucherService.update(updateWrapper);
if(!update) {
return Result.fail("下单失败!");
}
VoucherOrder voucherOrder = new VoucherOrder();
//生成订单id
RedisIdWorker redisIdWorker = new RedisIdWorker(redisTemplate);
long orderId = redisIdWorker.nextId("order");
voucherOrder.setId(orderId);
//用户id
Long userId = UserHolder.getUser().getId();
voucherOrder.setUserId(userId);
//代金券id
voucherOrder.setVoucherId(voucherId);
boolean flag = save(voucherOrder);
if(!flag) {
return Result.fail("下单失败!");
}
return Result.ok(orderId);
}
}
注意:
该方法是能在单应用中使用,集群中就会出问题!!