订单的唯一Id问题
对于每一个订单,其id是必不可少的。
唯一Id(分布式 全局ID生成器)需要满足的特点
- 唯一性
- 订单Id
- Redis.increment()
- 高可用
- 随时都可以生成,否则会导致其他业务出问题
- Redis集群、主从
- 高性能
- 不要把别的业务拖慢
- 递增性
- 替代数据库自增Id,有利于数据库创建索引
- 安全性
- 规律性不能太明显,不能让他人猜到Id特征
一种实现方法——Redis自增
策略:
- 每天一个key,方便统计订单量
- ID构造是 时间戳 + 计数器
其他的方案
- UUID
- 返回16进制串,字母数字都有
- 没有规律,不符合前面的特性
- Snowflake算法
- 数据库自增
- 单独一张表控制自增
- Redis自增的数据库版
- 性能不比Redis。企业方案:批量获取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
- 数据库编码有问题,改成了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);
}
利用jmeter进行压测步骤
- 设置200个线程
- 设置http请求
- 设置身份token
- 设置json断言(对于返回的json串,什么情况下视为返回错误)
- 运行结果
由此可见,出现了超卖问题,这次测试超卖了9单 - 测试可能用到的代码
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