一、全局ID生成器
1.1 概念
订单表的订单号使用数据库自增id会存在一些问题:
(1)id规律性太明显,会暴露一些信息如下单数量。
(2)受表单数据量影响(时间长了几百万、几千万的订单)。
全局ID生成器,是一种在分布式系统下用来生成全局唯一ID的工具。具有以下特点:
(1)唯一性;(2)高可用(稳定);(3)高性能(生成快);(4)递增性(有利于数据库创建索引);(5)安全性(不会暴露信息)。
使用Redis来满足上面的要求:
(1)使用redis的string的自增(incr)来保证唯一性。
(2)使用redis的集群、哨兵策略来保证高可用。
(3)redis本身性能高。
(4)使用redis的string的自增(incr)。
(5)使用redis保证安全性的方案如下(使用String的Long型):

当然,redis不是全局ID的唯一方案,还有其他很多方案。
1.2 Redis自增实现

@Component
public class RedisIdWorker {
private static final long BEGIN_TIMESTAM= 1640995200L;
private StringRedisTemplate stringRedisTemplate;
/*
*序列号位数
*/
private final int COUNT_BITS = 32;
public RedisIdWorker(StringRedisTemplate stringRedisTemplate){
this.stringRedisTemplate=stringRedisTemplate;
}
public long nextId(String keyPrefix){
//1.生成时间戳
LocalDateTime now = LocalDateTime.now();
long nowSecond = now.toEpochSecond(ZoneOffset.UTC);
long timestamp = nowSecond-BEGIN_TIMESTAM;
//2.生成序列号
String date = now.format(DateTimeFormatter.ofPattern("yyyyMMdd"));
long count = stringRedisTemplate.opsForValue().increment("icr:"+keyPrefix+":"+date);
//3.拼接并返回
return timestamp << COUNT_BITS | count;
}
}
其他的全局唯一ID生成策略:
UUID
snowflake算法
数据库自增
二、线程并发安全问题
2.1 秒杀下单功能(一般实现)
2.1.1 功能点描述


2.1.2 代码实现






@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {
@Resource
private ISeckillVoucherService seckillVoucherService;
@Resource
private RedisIdWorker redisIdWorker;
@Override
@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.扣减库存
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
long userId = UserHolder.getUser().getId();
voucherOrder.setUserId(userId);
// 7.3.代金券id
voucherOrder.setVoucherId(voucherId);
save(voucherOrder);
// 7.返回订单id
return Result.ok(orderId);
}
}
启动服务测试:





补充:使用jmeter模拟真实秒杀场景进行测试
上面的测试方法是在前端页面手动点击的,但现实场景中会有多个并发请求,可以使用jmeter进行模拟。


注意:要在请求头里添加authorization
可以查到刚才请求的:








问题分析:
正常情况下:

异常情况:

引发了并发安全问题,解决方法是锁机制。
2.1.3 悲观锁与乐观锁

悲观锁方法简单,下面说明实现乐观锁的方法,常见的两种方法是版本号法(表有一个字段为版本号,将版本号的值作为更新的条件)和CAS法(用库存数量代替版本号字段,原理与版本号相同)。

boolean success = seckillVoucherService.update().setSql("stock=stock-1").
eq("voucher_id",voucherId).eq("stock",voucher.getStock()).update();
重启服务,删除数据库数据进行重试:



结果:

原因:因版本号字段前后不一致更新失败的线程下单失败,失败率非常高。
解决失败率高的问题:
把版本号前后一致的条件改为库存大于0.

重启服务,删除数据库数据进行测试:




补充:乐观锁的缺点
乐观锁要访问数据库,对数据库的压力还是很大的,对于高并发场景还是有缺点的。
2.2 一人一单功能
目标:一个用户只能抢购一张优惠券。
2.2.1 实现思路

2.2.2 代码实现

// 一人一单
Long userId = UserHolder.getUser().getId();
int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
// 5.2.判断是否存在
if (count > 0) {
// 用户已经购买过了
return Result.fail("用户已经购买过一次!");
}
重启服务,删除数据库数据进行测试:




还是发生了线程安全问题。但这里不能使用乐观锁方案,只能使用悲观锁方案,将“是否下过单,更新库存,下单”的过程上锁。
2.2.3 解决线程安全问题(悲观锁)

@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {
@Resource
private ISeckillVoucherService seckillVoucherService;
@Resource
private RedisIdWorker redisIdWorker;
@Override
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("库存不足!");
}
return createVoucherOrder(voucherId);
}
@Transactional
public Result createVoucherOrder(Long voucherId) {
// 一人一单
Long userId = UserHolder.getUser().getId();
synchronized (userId.toString().intern()){
//intern()可以保证对象唯一(toString方法底层是new一个String,即时值相同也不是同一个对象),去字符串常量池找值一样的字符串的地址返回
int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
// 5.2.判断是否存在
if (count > 0) {
// 用户已经购买过了
return Result.fail("用户已经购买过一次!");
}
//5.扣减库存
boolean success = seckillVoucherService.update().setSql("stock=stock-1").
eq("voucher_id", voucherId).gt("stock",0).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
//long userId = UserHolder.getUser().getId();
voucherOrder.setUserId(userId);
// 7.3.代金券id
voucherOrder.setVoucherId(voucherId);
save(voucherOrder);
// 7.返回订单id
return Result.ok(orderId);
}
}
}
但上述代码还是有问题,因为createVoucherOrder()里的锁是在方法执行完,事务提交前释放的,所以可能出现一个线程释放了锁没提交事务(未写入数据库),另一个线程查询不到更新结果导致线程安全问题。
补充:悲观锁写法优化

但上面的写法会发生事务失效的问题,这里把老师原话写在下面(是的,我还不懂):
createVoucherOrder()方法加了事务,但外边的seckillVoucher()没有加事务,createVoucherOrder()方法调用拿到的是当前的VoucherOrderServiceImpl对象,而不是它的代理对象。事务要想生效是因为spring对类做了动态代理,获取了类(这里是VoucherOrderServiceImpl)的代理对象,用代理对象做的事务处理,当前的VoucherOrderServiceImpl对象不是代理对象,无事务功能。
举个例子可能会理解一些:
不会失效的例子1:
@Service
public class OrderService {
@Autowired
private UserService userService; // 这里注入的是代理对象
public void placeOrder() {
userService.saveUser(); // 通过代理调用
}
}
@Service
public class UserService {
@Transactional
public void saveUser() {
// 业务逻辑
}
}
会失效的例子1:
public interface IOrderService {
public void placeOrder();
}

最低0.47元/天 解锁文章
517

被折叠的 条评论
为什么被折叠?



