黑马视频地址:https://www.bilibili.com/video/BV1cr4y1671t?p=49&spm_id_from=pageDriver&vd_source=79bbd5b76bfd74c2ef1501653cee29d6
参考博客代码:https://cyborg2077.github.io/2022/10/22/RedisPractice/#%E4%BC%98%E6%83%A0%E5%88%B8%E7%A7%92%E6%9D%80
csdn地址:https://blog.csdn.net/weixin_50523986/article/details/131815165
- stringRedisTemplate.opsForValue().increment函数:
package com.hmdp.utils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
@Component
public class RedisIdWorker {
// 定义一个初始时间戳
public static final long BEGIN_TIME = 1640995200L;
// 序列号位数
public static final long COUNT_BITS = 32;
// 用到redis的自增长
@Autowired
private StringRedisTemplate stringRedisTemplate;
public long nextId(String keyPrefix){
// 1 生成时间戳
LocalDateTime now = LocalDateTime.now();
long nowSecond = now.toEpochSecond(ZoneOffset.UTC);
long timeStamp = nowSecond - BEGIN_TIME;
// 2 生成序列号
// 确定当天序列号的key 获取当天日期 精确到天
String data = now.format(DateTimeFormatter.ofPattern("yyyy:MM:DD"));
// 实现自增长
long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + data);
// 3 拼接并返回 借助位运算
// 移位之后后面32位全都是0 或运算可以保证原来的样子
return timeStamp << COUNT_BITS | count;
}
public static void main(String[] args) {
// LocalDateTime time = LocalDateTime.of(2022, 1, 1, 0, 0, 0);
// long timeToSecond = time.toEpochSecond(ZoneOffset.UTC);
// System.out.println(timeToSecond);
}
}
- com/hmdp/service/impl/VoucherOrderServiceImpl.java
package com.hmdp.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.hmdp.dto.Result;
import com.hmdp.entity.SeckillVoucher;
import com.hmdp.entity.Voucher;
import com.hmdp.entity.VoucherOrder;
import com.hmdp.mapper.VoucherOrderMapper;
import com.hmdp.service.ISeckillVoucherService;
import com.hmdp.service.IVoucherOrderService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.utils.RedisIdWorker;
import com.hmdp.utils.UserHolder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
/**
* <p>
* 服务实现类
* </p>
*
* @author 虎哥
* @since 2021-12-22
*/
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {
@Autowired
private ISeckillVoucherService seckillVoucherService;
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Autowired
private RedisIdWorker redisIdWorker;
/**
* 优惠券秒杀功能
* @param voucherId
* @return
*/
@Override
@Transactional
public Result seckillVoucher(Long voucherId) {
// 1 根据id查询优惠券信息
LambdaQueryWrapper<SeckillVoucher> lqw = new LambdaQueryWrapper<>();
lqw.eq(SeckillVoucher::getVoucherId,voucherId);
SeckillVoucher seckillVoucher = seckillVoucherService.getOne(lqw);
// 2 判断秒杀是否开始
if (seckillVoucher.getBeginTime().isAfter(LocalDateTime.now())){
return Result.fail("秒杀尚未开始");
}
// 3 判断秒杀是否结束
if (seckillVoucher.getEndTime().isBefore(LocalDateTime.now())){
return Result.fail("秒杀已经结束");
}
// 4 判断库存是否充足
if (seckillVoucher.getStock()<1){
return Result.fail("秒杀券已经不足");
}
// 5 扣减库存
seckillVoucher.setStock(seckillVoucher.getStock()-1);
boolean success = seckillVoucherService.update(seckillVoucher, null);
if (!success){
return Result.fail("秒杀券已经不足");
}
// 6 创建订单 写入数据库
VoucherOrder voucherOrder = new VoucherOrder();
// 生成全局唯一Id
long orderId = redisIdWorker.nextId("order");
voucherOrder.setId(orderId);
voucherOrder.setUserId(UserHolder.getUser().getId());
voucherOrder.setVoucherId(voucherId);
baseMapper.insert(voucherOrder);
// 7 返回订单id
return Result.ok(orderId);
}
}
@Override
public Result seckillVoucher(Long voucherId) {
LambdaQueryWrapper<SeckillVoucher> queryWrapper = new LambdaQueryWrapper<>();
//1. 查询优惠券
queryWrapper.eq(SeckillVoucher::getVoucherId, voucherId);
SeckillVoucher seckillVoucher = seckillVoucherService.getOne(queryWrapper);
//2. 判断秒杀时间是否开始
if (LocalDateTime.now().isBefore(seckillVoucher.getBeginTime())) {
return Result.fail("秒杀还未开始,请耐心等待");
}
//3. 判断秒杀时间是否结束
if (LocalDateTime.now().isAfter(seckillVoucher.getEndTime())) {
return Result.fail("秒杀已经结束!");
}
//4. 判断库存是否充足
if (seckillVoucher.getStock() < 1) {
return Result.fail("优惠券已被抢光了哦,下次记得手速快点");
}
+ // 一人一单逻辑
+ Long userId = UserHolder.getUser().getId();
+ int count = query().eq("voucher_id", voucherId).eq("user_id", userId).count();
+ 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");
//6.2 设置用户id
Long id = UserHolder.getUser().getId();
//6.3 设置代金券id
voucherOrder.setVoucherId(voucherId);
voucherOrder.setId(orderId);
voucherOrder.setUserId(id);
//7. 将订单数据保存到表中
save(voucherOrder);
//8. 返回订单id
return Result.ok(orderId);
}
一人一单
- 需求:修改秒杀业务,要求同一个优惠券,一个用户只能抢一张
- 具体操作逻辑如下:我们在判断库存是否充足之后,根据我们保存的订单数据,判断用户订单是否已存在
- 如果已存在,则不能下单,返回错误信息
- 如果不存在,则继续下单,获取优惠券
- 初步代码
DIFF
复制成功
|
|
存在问题
:还是和之前一样,如果这个用户故意开多线程抢优惠券,那么在判断库存充足之后,执行一人一单逻辑之前,在这个区间如果进来了多个线程,还是可以抢多张优惠券的,那我们这里使用悲观锁来解决这个问题- 初步代码,我们把一人一单逻辑之后的代码都提取到一个
createVoucherOrder
方法中,然后给这个方法加锁 - 不管哪一个线程(例如线程A),运行到这个方法时,都要检查有没有其它线程B(或者C、 D等)正在用这个方法(或者该类的其他同步方法),有的话要等正在使用synchronized方法的线程B(或者C 、D)运行完这个方法后再运行此线程A,没有的话,锁定调用者,然后直接运行。
JAVA
|
|
- 但是这样加锁,锁的细粒度太粗了,在使用锁的过程中,控制锁粒度是一个非常重要的事情,因为如果锁的粒度太大,会导致每个线程进来都会被锁住,现在的情况就是所有用户都公用这一把锁,串行执行,效率很低,我们现在要完成的业务是
一人一单
,所以这个锁,应该只加在单个用户上,用户标识可以用userId
JAVA
|
|
- 由于toString的源码是new String,所以如果我们只用
userId.toString()
拿到的也不是同一个用户,需要使用intern()
,如果字符串常量池中已经包含了一个等于这个string对象的字符串(由equals(object)方法确定),那么将返回池中的字符串。否则,将此String对象添加到池中,并返回对此String对象的引用。
JAVA
|
|
- 但是以上代码还是存在问题,问题的原因在于当前方法被Spring的事务控制,如果你在内部加锁,可能会导致当前方法事务还没有提交,但是锁已经释放了,这样也会导致问题,所以我们选择将当前方法整体包裹起来,确保事务不会出现问题
JAVA
|
|
- 但是以上做法依然有问题,因为你调用的方法,其实是this.的方式调用的,事务想要生效,还得利用代理来生效,所以这个地方,我们需要获得原始的事务对象, 来操作事务,这里可以使用
AopContext.currentProxy()
来获取当前对象的代理对象,然后再用代理对象调用方法,记得要去IVoucherOrderService
中创建createVoucherOrder
方法
JAVA
|
|
- 但是该方法会用到一个依赖,我们需要导入一下
XML
|
|
- 同时在启动类上加上
@EnableAspectJAutoProxy(exposeProxy = true)
注解
JAVA
|
|
- 重启服务器,再次使用Jmeter测试,200个线程并发,但是只能抢到一张优惠券,目的达成