黑马点评实战篇|第三篇:优惠卷秒杀
目录
redis实现全局唯一ID
这里只需要注意一下时间戳的生成方式,注意以UTC计算当前时间秒的方式
/**
* 开始时间戳
*/
private static final long BEGIN_TIMESTAMP = 1772556781L;
private static final long COUNT_BITS = 32;
private StringRedisTemplate stringRedisTemplate;
public RedisIdWorker(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
public long nextId(String keyPrefix) {
// 1.生成时间戳
long epochSecond = LocalDateTime.now().toEpochSecond(ZoneOffset.UTC);
long timestamp = epochSecond - BEGIN_TIMESTAMP;
// 2.生成序列号
// 2.1.获取当前日期,精确到天
String date = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
// 2.2.自增长
Long increment = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + date);
// 3.拼接并返回
return timestamp << COUNT_BITS | increment;
}
这里最后提到了位运算
举例:最开始时间戳只有高32位这些数字,左移32为后出现了低32位,然后序列号部分就做或运算之后填充进去


实现秒杀下单
为了实现下单功能,我们需要根据路径编写对应的程序

用mybatis-plus就可以快捷的操作每一个表的数据根据如下思路进行编写,不要忘了添加事务管理,因为有两个表的操作

@Resource
private ISeckillVoucherService seckillVoucherService;
@Resource
private RedisIdWorker redisIdWorker;
@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("库存不足");
}
//扣减库存
seckillVoucherService.update().setSql("stock = stock - 1")//相当于sql语句
.eq("voucher_id", voucherId)//相当于where
.update();
//创建订单
VoucherOrder voucherOrder = new VoucherOrder();
voucherOrder.setUserId(UserHolder.getUser().getId());
voucherOrder.setVoucherId(voucherId);
// 6.1.订单id
long orderId = redisIdWorker.nextId("order");
voucherOrder.setId(orderId);
save(voucherOrder);
return Result.ok(orderId);
}
我们观察一下为什么mybatis-plus能够知道操作哪一个数据库表的问题?
观察这个类,里面是没有自己书写getById这个数据库操作方法的

这里可以知道它继承自IService<SeckillVoucher>
IService 是 MyBatis-Plus 自带的接口,不是 MyBatis 的。
MyBatis vs MyBatis-Plus
重要区别:
MyBatis:原生框架,需要自己写 DAO 接口和 XML 映射
MyBatis-Plus:在 MyBatis 基础上的增强工具,提供了 IService 等通用服务

这里泛型SeckillVoucher中使用了@TableName注解,告诉了我们mybatis-plus操作的数据库

超卖测试:
用ApacheJmeter测试超卖问题,当库存只剩一个的时候,多个线程同时点击就会出现超卖的情况
在测试计划里添加http请求头管理器和cookie管理器

结构如图

根据cookie和authorization(也就是token)配置请求头的数据才能实现测试

库存有100但发送了200个请求,显示-9个数说明存在超卖问题

超卖问题解决方案:加锁

乐观锁使用方案:在设计数据时设计一个版本号,在更新数据后就版本号加1,在线程更新数据时就检测版本是否和一开始的版本一致,一致则说明没有其他线程操作数据,可以正常改,若不同就不改
CAS(比较)法:因为版本号的作用就是看是否数据进行过修改,那么我们去掉版本号只对比库存的数量就可以知道了

代码实现:


这里如果用eq的话会出现在多个线程同时查询到同一个stock时,就会有多个线程失败,所以失败率较高
所以只需要让stock数量大于0就行

实现一人一单
为了防止黄牛的情况出现,需要做到一人一单的情况

实现方式第一版
直接在扣减库存前查询是否存在这个用户

测试发现异常量比预期低,因为只能有一个成功

这是因为在同一时刻,同一个用户多个线程同时下单,在最快的线程把订单添加到数据库之前,有其他线程也下单了,所以会有线程安全问题,我们的解决方案是加悲观锁,因为这里不是读数据而是写数据
最终版:
把后3步提取出来,获取代理对象,才能调用使用了事务管理的方法

注意
1.只在方法内加锁的话,在释放锁的时候,方法的事务还没提交,也就是还没有写进数据库,若此时其他线程访问,此时会有线程安全问题
2.不在方法的声明上加锁的原因:只会让一个线程运行,其他用户就无法下单。不能实现一人一单
集群环境下的并发问题
在单体项目解决了一人一单的情况后,我们需要解决集群下并发问题
只需要在idea中设置多个端口同时运行,就可以模拟集群环境

有关锁失效原因分析
多个集群下有不同的jvm,一个jvm中有一个锁监视器,就只能监管一把锁,多个集群的情况下,就有多个锁监视器,所以每一个集群都有一个可以运行的线程
更多推荐




所有评论(0)