大众点评项目 基于Session的短信登录
- 需求:乐观锁解决超卖问题 悲观锁解决一人一单问题
- 业务代码
- 总结
SpringCloud章节复习已经过去,新的章节Redis开始了,这个章节中将会回顾Redis实战项目 大众点评
主要依照以下几个原则
- 基础+实战的Demo和Coding上传到我的代码仓库
- 在原有基础上加入一些设计模式,stream+lamdba等新的糖
- 通过DeBug调试,进入组件源码去分析底层运行的规则和设计模式
代码会同步在我的gitee中去,觉得不错的同学记得一键三连求关注,感谢:
Redis优化-链接: RedisVoucherCASProject
需求:乐观锁解决超卖问题 悲观锁解决一人一单问题
超卖问题
当秒杀进行时,如果有大量用户同时点击,就容易出现一种情况
大量库存被减到负数,这肯定是不友好的
下面是通过JMX进行的压测,模拟并发实现
一人一单
同样,我们不希望一个人拿到所有的秒杀商品
所以我们需要对这两种情况进行处理,锁机制就来了
版本号:就是通过定义版本Id,去进行判断,通过sql中的where条件就可以实现乐观锁
CAS:就是自旋,查看数据是否被改变,没有变就是ok的
业务代码
/**
* <p>
* 服务实现类
* </p>
*
* @author 张大树
* @since 2022-12-12
*/
@Service
@Slf4j
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {
@Resource
private ISeckillVoucherService seckillVoucherService;
@Resource
private RedisIDProductor redisIDProductor;
和上一节相同,可以参考上一节
@Override
public Result seckillVoucher(Long voucherId) {
SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
return Result.fail("秒杀尚未开始");
}
if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
return Result.fail("秒杀已经结束");
}
if (voucher.getStock() < 1) {
return Result.fail("库存不足");
}
实现一人一单功能,
为了保证一人一单,使用了悲观锁synchronized,需要注意
- synchronized想要保证效率,不要直接加载类上,判断出,一人一单为了是保证每个人下一单,那个我们直接锁ID,就可以了,这样每个id都有各自的线程可以走,不是所有都锁住!
- userId.toString()本身返回的是一个new类型
return new String(buf, true);
,所以我们必须保证每次拿到的ID的对象是同一个,这里就是用了intern方法,它回去常量池中查找对应的id,保证了唯一性 - 这里的 @Transactional也需要注意,如果直接 调用的this.createVoucherOrder方法,没有实际对象,是事务失效的几种情况之一,所以需要找代理对象来实现!
//实现一人一单
Long userId = UserHolder.getUser().getId();
//intern()是去常量池中去找userId,处理锁失效,事务失效
synchronized(userId.toString().intern()) {
IVoucherOrderService currentProxy = (IVoucherOrderService) AopContext.currentProxy();
return currentProxy.createVoucherOrder(voucherId);
/*调用的this.create方法,没有实际对象,是事务失效的几种情况之一,所以需要找代理对象来实现
return createVoucherOrder(voucherId);*/
}
}
@Transactional
public Result createVoucherOrder(Long voucherId) {
Long userId = UserHolder.getUser().getId();
int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
if (count > 0) {
return Result.fail("已经购买");
}
为了防止超卖,其本身就是不断更新数量,所以使用了乐观锁,CAS方法,需要注意
- CAS: 通过设定判断库存数量来进行,适合更新数据使用
- 注意MP的使用
seckillVoucherService.update() .setSql("stock = stock - 1") .eq("voucher_id", voucherId) .gt("stock", 0)//CAS: 通过设定判断库存数量来进行,适合更新数据使用 .update();
//验证结束,扣减库存
boolean flag = seckillVoucherService.update()
.setSql("stock = stock - 1")
.eq("voucher_id", voucherId)
.gt("stock", 0)//CAS: 通过设定判断库存数量来进行,适合更新数据使用
.update();
if (!flag) {
return Result.fail("库存不足");
}
封装订单
//封装订单
VoucherOrder voucherOrder = new VoucherOrder();
long orderId = redisIDProductor.nextId("order");
//订单Id、用户Id、优惠券Id
voucherOrder.setId(orderId).setUserId(userId).setVoucherId(voucherId);
save(voucherOrder);
return Result.ok(orderId);
}
}
总结
如果引入代理对象 IVoucherOrderService currentProxy = (IVoucherOrderService) AopContext.currentProxy(); return currentProxy.createVoucherOrder(voucherId);
一定要注意加依赖,开启扫描注解
@EnableAspectJAutoProxy(exposeProxy = true)
@MapperScan("com.hmdp.mapper")
@SpringBootApplication
public class HmDianPingApplication {
public static void main(String[] args) {
SpringApplication.run(HmDianPingApplication.class, args);
}
}
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
</dependency>