对于每一个订单,其id是必不可少的。
生成特定时间戳:
public static void main(String[] args) {
LocalDateTime time = LocalDateTime.of(2022, 1, 1, 0, 0, 0);
long timeStamp = time.toEpochSecond(ZoneOffset.UTC);
System.out.println(timeStamp);
}
生成唯一Id值
public long nextId(String keyPrefix){
// 生成时间戳
LocalDateTime now = LocalDateTime.now();
long nowSecond = now.toEpochSecond(ZoneOffset.UTC);
long timeStamp = nowSecond - BEGIN_TIMESTAMP;
// 生成序列号
String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + date);
// 拼接并返回
return timeStamp << COUNT_BITS | count;
}
插入数据的时候发现中文乱码了,解决:
application.properties
中mysql地址不全,加了&useUnicode=yes&characterEncoding=utf8
@Transactional
public Result seckillVoucher(Long voucherId) {
// 1. 查询优惠券
SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
// 2. 判断秒杀是否开始
if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
return Result.fail("秒杀尚未开始!");
}
// 3. 判断秒杀是否已经结束
if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
return Result.fail("秒杀已经结束!");
}
// 4. 判断库存是否充足
if (voucher.getStock() < 1) {
return Result.fail("库存不足!");
}
// 5. 减库存
/*
SeckillVoucher updateVoucher = voucher.setStock(voucher.getStock() - 1);
seckillVoucherService.updateById(updateVoucher);
*/
boolean success = seckillVoucherService.update()
.setSql("stock = stock - 1")
.eq("voucher_id", voucherId)
.update();
if (!success) {
return Result.fail("购买失败");
}
// 6. 创建订单
VoucherOrder voucherOrder = new VoucherOrder();
// 6.1 订单id
long orderId = redisIdWorker.nextId("order");
voucherOrder.setId(orderId);
// 6.2 用户id
voucherOrder.setUserId(UserHolder.getUser().getId());
// 6.3 代金券id
voucherOrder.setVoucherId(voucher.getVoucherId());
// 6.4 保存订单到数据库
save(voucherOrder);
return Result.ok(orderId);
}
TRUNCATE `tb_voucher_order`
清空这个表乐观锁的关键是判断之前查询得到的数据是否有被修改过
普通乐观锁:
boolean success = seckillVoucherService.update()
.setSql("stock = stock - 1")
.eq("voucher_id", voucherId).eq("stock", voucher.getStock())
.update();
结果:
由于多线程并发访问stock数值,而一个线程修改成功会导致其他线程同时失败,失败率高,造成并发安全问题
对于此情况的优化方法:由于库存数量的要求并不是特别严格,可以将成功条件改为库存 > 0
boolean success = seckillVoucherService.update()
.setSql("stock = stock - 1")
.eq("voucher_id", voucherId).gt("stock", 0)
.update();
结果(成功):
而对于必须要求相等的数值,可以采用分段锁,多张表分别抢的方法。
在减库存前进行判断代码
// 4-1 判断是否购买过
int count = query().eq("user_id", UserHolder.getUser().getId())
.eq("voucher_id", voucherId).count();
if (count > 0) {
return Result.fail("已经购买过!");
}
锁的范围:
// 1-1
// 这样是所有的用户来了都加锁。我们只需要对当前用户加锁(userId)
@Transactional
public synchronized Result createVoucherOrder(Long voucherId) {
提取出修改库存的部分(注意Spring事务失效)
synchronized (userId.toString().intern()) {
/*
事务生效是因为spring做了动态代理
this没有事务功能
不能直接 return this.createVoucherOrder(voucherId);
*/
// 获取代理对象(事务)
IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
return proxy.createVoucherOrder(voucherId);
}
springboot启动类加注解@EnableAspectJAutoProxy(exposeProxy = true)
结果(成功)
然而,当架构变为分布式时,每个锁都在不同的机器上,当同样的请求打到不同的服务器上时仍会造成线程安全问题。这时就要用到分布式锁
来源: 黑马程序员Redis入门到实战教程 https://www.bilibili.com/video/BV1cr4y1671t