普通券
我们来看这一张表
里面包含了主键,商铺id,使用规则,时间等内容
可以看到里面没有库存,意味着所有人都可以来购买,所以是普通券
秒杀券
我们看下面这一张表
这是一张秒杀券,里面包含了普通券的所有信息,还有秒杀券独有的特点,比如库存,生效时间,生效时间等信息
VoucherOrderController
package com.hmdp.controller;
import com.hmdp.dto.Result;
import com.hmdp.service.IVoucherOrderService;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@RestController
@RequestMapping("/voucher-order")
public class VoucherOrderController {
@Resource
private IVoucherOrderService voucherOrderService;
@PostMapping("seckill/{id}")
public Result seckillVoucher(@PathVariable("id") Long voucherId) {
return voucherOrderService.seckillVoucher(voucherId);
}
}
package com.hmdp.service;
import com.hmdp.dto.Result;
import com.hmdp.entity.VoucherOrder;
import com.baomidou.mybatisplus.extension.service.IService;
/**
* <p>
* 服务类
* </p>
*/
public interface IVoucherOrderService extends IService<VoucherOrder> {
Result seckillVoucher(Long voucherId);
}
@Slf4j
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {
@Resource
private ISeckillVoucherService seckillVoucherService;
@Resource
private RedisIdWorker redisIdWorker;
@Override
@Transactional //由于使用了2张表,这里加上事务比较好,一旦重新了问题,可以及时回滚
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("库存不足");
}
//扣减库存
//mybatisplus
boolean success=seckillVoucherService.update().setSql("stock = stock - 1").eq("voucher_id", voucherId).update();
if(!success){
return Result.fail("扣减库存失败");
}
//创建订单
VoucherOrder voucherOrder=new VoucherOrder();
//订单id
long ordrId=redisIdWorker.nextId("order");
voucherOrder.setId(ordrId);
//用户id
long userId=UserHolder.getUser().getId();
voucherOrder.setUserId(userId);
//代金券id
voucherOrder.setVoucherId(voucherId);
//把订单写入数据库
save(voucherOrder);
//返回订单id
return Result.ok(ordrId);
}
}
我们使用上面的操作,线程少的话,没问题,可以执行
但是线程多的话,就会发生线程安全问题
于是我们可以使用下面的方法来解决问题
乐观锁(Optimistic Locking)是一种并发控制机制,用于多线程或分布式系统中的数据一致性控制。它假设不会有或尽可能减少冲突,因此不会每次都进行锁冲突的检查。在使用乐观锁时,多个用户可以同时读取数据,但只有在更新数据时才检查版本号等机制来判断数据是否被其他用户修改。 在使用乐观锁时,通常会通过在数据上添加版本号(Version)信息来实现。当用户读取数据时,会将数据的版本号一并读取。当用户提交更新请求时,系统会先检查数据的版本号是否与之前读取的一致。如果一致,则更新成功;如果不一致,则表示数据已被其他用户修改,更新失败。
整体代码都差不多,其实就修改一部分就行了
我们进入VoucherOrderServiceImpl
库存大于0,证明有库存,这样子就行了
对于有些优惠力度比较大的券,为了防止黄牛,我们需要设置一人一张券
VoucherOrderServiceImpl
@Slf4j
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {
@Resource
private ISeckillVoucherService seckillVoucherService;
@Resource
private RedisIdWorker redisIdWorker;
@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("库存不足");
}
Long userId=UserHolder.getUser().getId();
synchronized(userId.toString().intern()) {
//获取代理对象
IVoucherOrderService proxy= (IVoucherOrderService) AopContext.currentProxy();
//代理对象进行调用
return proxy.createVoucherOrder(voucherId);
}
}
//上面操作都是查询,不需要添加@Transactional了
@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("用户已购买过该优惠券");
}
//扣减库存
//mybatisplus
boolean success = seckillVoucherService.update()
.setSql("stock = stock - 1")
.eq("voucher_id", voucherId).gt("stock", 0) //增加对stock值的判断
.update();
if (!success) {
return Result.fail("扣减库存失败");
}
//count< = 0的时候
//创建订单
VoucherOrder voucherOrder = new VoucherOrder();
//订单id
long ordrId = redisIdWorker.nextId("order");
voucherOrder.setId(ordrId);
//用户id
// long userId=UserHolder.getUser().getId();
voucherOrder.setUserId(userId);
//代金券id
voucherOrder.setVoucherId(voucherId);
//把订单写入数据库
save(voucherOrder);
//返回订单id
return Result.ok(ordrId);
}
}
IVoucherOrderService
要使用代理,需要在pom文件中加入下面的代码
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
</dependency>
并且需要在
启动类
上加上注解@EnableAspectJAutoProxy(exposeProxy = true)
来暴露这个代理对象
为什么要加上锁
在该函数中,使用了synchronized关键字加上锁,这是为了确保在多线程环境下,同一时间只有一个线程能够执行该代码块。这样可以避免多个线程同时修改共享资源导致的数据不一致问题。具体来说,在该代码块中,使用了线程的id作为锁,可以确保每个线程都有自己的锁,互不干扰。通过加锁,能够保证在多线程环境下,该代码块的执行是线程安全的。
synchronized(userId.toString().intern()) {
//获取代理对象
IVoucherOrderService proxy= (IVoucherOrderService) AopContext.currentProxy();
//代理对象进行调用
return proxy.createVoucherOrder(voucherId);
}
这段代码里面的获取代理对象有什么用
如果我们不写成 return proxy.createVoucherOrder(voucherId); ,写成 return createVoucherOrder(voucherId); 那么默认的是 this 进行调用 createVoucherOrder(voucherId)
使用 默认的 this 进行调用的话,我们拿到的是当前的 createVoucherOrder(voucherId) ,而不是代理对象
( 这里我们知道,@Transactional要想生效,其实是因为spring对当前类(VoucherOrderServiceImpl) 进行了动态代理
,拿到了代理对象createVoucherOrder , 然后使用createVoucherOrder进行代理 )
我们使用上面的方法进行代理后,事务就可以生效了
上面那段代码的
userId.toString().intern()
有什么用
因为我们希望id值一样的 用的是同一把锁,每次请求的都是不同的对象,对象变了,为了保证值一样,我们使用了tostring()方法
但是实际上我们每调用一次tostring()方法,都传入了一个全新的字符串对象,这样子值还是会发生变化
为了保证值不变,我们需要加上intern()方法
intern()方法是去字符串常量池里面,找到和之前id的值一样的字符串地址,然后进行返回
这样子我们就保证了,只要值一样,不论new了多少个新字符串对象,返回的结果都是一样的
在技术的道路上,我们不断探索、不断前行,不断面对挑战、不断突破自我。科技的发展改变着世界,而我们作为技术人员,也在这个过程中书写着自己的篇章。让我们携手并进,共同努力,开创美好的未来!愿我们在科技的征途上不断奋进,创造出更加美好、更加智能的明天!