需求:修改秒杀业务,要求同一个优惠券,一个用户只能下一单
我们只需要在增加订单之前,拿用户id和优惠券id判断订单是否已经存在,如果存在,说明用户已经购买。
代码实现:
package com.hmdp.service.impl;
import com.hmdp.dto.Result;
import com.hmdp.entity.SeckillVoucher;
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.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.time.LocalDateTime;
/**
* <p>
* 服务实现类
* </p>
*
* @author 虎哥
* @since 2021-12-22
*/
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {
@Resource
private ISeckillVoucherService iSeckillVoucherService;
@Resource
private RedisIdWorker redisIdWorker;
@Override
public Result seckillVoucher(Long voucherId) {
//1.获取优惠券信息
SeckillVoucher voucher = iSeckillVoucherService.getById(voucherId);
//2.判断是否已经开始
if (voucher.getBeginTime().isAfter(LocalDateTime.now())){
Result.fail("秒杀尚未开始!");
}
//3.判断是否已经结束
if (voucher.getEndTime().isBefore(LocalDateTime.now())){
Result.fail("秒杀已经结束了!");
}
//4.判断库存是否充足
if (voucher.getStock() < 1) {
Result.fail("库存不充足!");
}
//5.扣减库存
boolean success = iSeckillVoucherService.update()
.setSql("stock = stock-1").eq("voucher_id",voucherId).gt("stock",0)
.update();
if (!success){
Result.fail("库存不充足!");
}
Long userId = UserHolder.getUser().getId();
//6.根据优惠券id和用户id判断订单是否已经存在
//如果存在,则返回错误信息
int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
if (count > 0) {
return Result.fail("用户已经购买!");
}
//7. 创建订单
VoucherOrder voucherOrder = new VoucherOrder();
//7.1添加订单id
Long orderId = redisIdWorker.nextId("order");
voucherOrder.setId(orderId);
//7.2添加用户id
voucherOrder.setUserId(userId);
//7.3添加优惠券id
voucherOrder.setVoucherId(voucherId);
save(voucherOrder);
//8.返回订单id
return Result.ok(orderId);
}
}
但是,还没完,这种代码逻辑,在高并发的情况下还是会出现一个人购买购买多个的情况:
就是同一时间,多个线程来查询数据,都没有查到订单,都去创建了订单(高并发的情况下)
类似超卖问题,所以我们要进行上锁。
这次就用悲观锁。
最简单的实现方法,就是把从查询订单是否存在到保存订单返回订单id这一段代码块进行封装成一个方法,然后在这个方法上加上synchronized关键字和spring事务。
如下:
package com.hmdp.service.impl;
import com.hmdp.dto.Result;
import com.hmdp.entity.SeckillVoucher;
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.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.time.LocalDateTime;
/**
* <p>
* 服务实现类
* </p>
*
* @author 虎哥
* @since 2021-12-22
*/
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {
@Resource
private ISeckillVoucherService iSeckillVoucherService;
@Resource
private RedisIdWorker redisIdWorker;
@Override
public Result seckillVoucher(Long voucherId) {
//1.获取优惠券信息
SeckillVoucher voucher = iSeckillVoucherService.getById(voucherId);
//2.判断是否已经开始
if (voucher.getBeginTime().isAfter(LocalDateTime.now())){
Result.fail("秒杀尚未开始!");
}
//3.判断是否已经结束
if (voucher.getEndTime().isBefore(LocalDateTime.now())){
Result.fail("秒杀已经结束了!");
}
//4.判断库存是否充足
if (voucher.getStock() < 1) {
Result.fail("库存不充足!");
}
//5.扣减库存
boolean success = iSeckillVoucherService.update()
.setSql("stock = stock-1").eq("voucher_id",voucherId).gt("stock",0)
.update();
if (!success){
Result.fail("库存不充足!");
}
return createVoucherOrder(voucherId);
}
@Transactional
public synchronized Result createVoucherOrder(Long voucherId) {
Long userId = UserHolder.getUser().getId();
//6.根据优惠券id和用户id判断订单是否已经存在
//如果存在,则返回错误信息
int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
if (count > 0) {
return Result.fail("用户已经购买!");
}
//7. 创建订单
VoucherOrder voucherOrder = new VoucherOrder();
//7.1添加订单id
Long orderId = redisIdWorker.nextId("order");
voucherOrder.setId(orderId);
//7.2添加用户id
voucherOrder.setUserId(userId);
//7.3添加优惠券id
voucherOrder.setVoucherId(voucherId);
save(voucherOrder);
//8.返回订单id
return Result.ok(orderId);
}
}
但是,这个方法就是使用了悲观锁,锁的对象是整个类对象,所有用户公用一把锁,就会导致串行执行,从而性能大大降低。
我们可以只锁上用户id,让他每个用户获得一把锁。
package com.hmdp.service.impl;
import com.hmdp.dto.Result;
import com.hmdp.entity.SeckillVoucher;
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.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.time.LocalDateTime;
/**
* <p>
* 服务实现类
* </p>
*
* @author 虎哥
* @since 2021-12-22
*/
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {
@Resource
private ISeckillVoucherService iSeckillVoucherService;
@Resource
private RedisIdWorker redisIdWorker;
@Override
public Result seckillVoucher(Long voucherId) {
//1.获取优惠券信息
SeckillVoucher voucher = iSeckillVoucherService.getById(voucherId);
//2.判断是否已经开始
if (voucher.getBeginTime().isAfter(LocalDateTime.now())){
Result.fail("秒杀尚未开始!");
}
//3.判断是否已经结束
if (voucher.getEndTime().isBefore(LocalDateTime.now())){
Result.fail("秒杀已经结束了!");
}
//4.判断库存是否充足
if (voucher.getStock() < 1) {
Result.fail("库存不充足!");
}
//5.扣减库存
boolean success = iSeckillVoucherService.update()
.setSql("stock = stock-1").eq("voucher_id",voucherId).gt("stock",0)
.update();
if (!success){
Result.fail("库存不充足!");
}
Long userId = UserHolder.getUser().getId();
return createVoucherOrder(voucherId);
}
@Transactional
public Result createVoucherOrder(Long voucherId) {
Long userId = UserHolder.getUser().getId();
//6.根据优惠券id和用户id判断订单是否已经存在
synchronized (userId.toString().intern()){
//如果存在,则返回错误信息
int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
if (count > 0) {
return Result.fail("用户已经购买!");
}
//7. 创建订单
VoucherOrder voucherOrder = new VoucherOrder();
//7.1添加订单id
Long orderId = redisIdWorker.nextId("order");
voucherOrder.setId(orderId);
//7.2添加用户id
voucherOrder.setUserId(userId);
//7.3添加优惠券id
voucherOrder.setVoucherId(voucherId);
save(voucherOrder);
//8.返回订单id
return Result.ok(orderId);
}
}
}
这里锁上userid时,除了用toString方法转成字符串,还使用intern方法的原因是:
toString方法的底层原理其实是new一个String对象,然后将其变成字符串,如果只锁上了加toString方法的userid,就有可能出现相同的userid,但是toString底层new出来的String对象不同,而多分了锁。所以使用intern方法来直接判断常量池中的string值是否一致,值一样的共用一把锁,这样就不会导致多分锁了。
但是但是,还没完因为这里我们是加了锁和事务,但是因为这个事务时Spring进行管理的,它会在我们代码块结束后才会去执行事务,也就是我们释放锁的时候,才会执行事务。这个时候,锁放开了,就会有其他线程进来,就很有可能出现事务提交带上了其他线程。
我们可以这样进行改进:在本个方法上进行加锁。
package com.hmdp.service.impl;
import com.hmdp.dto.Result;
import com.hmdp.entity.SeckillVoucher;
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.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.time.LocalDateTime;
/**
* <p>
* 服务实现类
* </p>
*
* @author 虎哥
* @since 2021-12-22
*/
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {
@Resource
private ISeckillVoucherService iSeckillVoucherService;
@Resource
private RedisIdWorker redisIdWorker;
@Override
public Result seckillVoucher(Long voucherId) {
//1.获取优惠券信息
SeckillVoucher voucher = iSeckillVoucherService.getById(voucherId);
//2.判断是否已经开始
if (voucher.getBeginTime().isAfter(LocalDateTime.now())){
Result.fail("秒杀尚未开始!");
}
//3.判断是否已经结束
if (voucher.getEndTime().isBefore(LocalDateTime.now())){
Result.fail("秒杀已经结束了!");
}
//4.判断库存是否充足
if (voucher.getStock() < 1) {
Result.fail("库存不充足!");
}
//5.扣减库存
boolean success = iSeckillVoucherService.update()
.setSql("stock = stock-1").eq("voucher_id",voucherId).gt("stock",0)
.update();
if (!success){
Result.fail("库存不充足!");
}
Long userId = UserHolder.getUser().getId();
synchronized (userId.toString().intern()){
return createVoucherOrder(voucherId);
}
}
@Transactional
public Result createVoucherOrder(Long voucherId) {
Long userId = UserHolder.getUser().getId();
//6.根据优惠券id和用户id判断订单是否已经存在
//如果存在,则返回错误信息
int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
if (count > 0) {
return Result.fail("用户已经购买!");
}
//7. 创建订单
VoucherOrder voucherOrder = new VoucherOrder();
//7.1添加订单id
Long orderId = redisIdWorker.nextId("order");
voucherOrder.setId(orderId);
//7.2添加用户id
voucherOrder.setUserId(userId);
//7.3添加优惠券id
voucherOrder.setVoucherId(voucherId);
save(voucherOrder);
//8.返回订单id
return Result.ok(orderId);
}
}
但是但是但是,还没完。哈哈
我们只给创建订单这个方法(createVoucherOrder)加了事务,但是没给上面判断条件的方法加上事务,而我们锁代码块里执行的方法,其实是this.createVoucherOrder()方法,是没有加事务的方法调用的createVoucherOrder()方法,这个this可不是spring的事务代理对象,这就会导致事务失效。
解决方法就是,我们只需要拿到代理对象,然后通过代理对象调用我们这个加了事务的方法,也就是createVoucherOrder()方法。
使用 AopContext.currentProxy();方法来拿到代理对象
温馨提示 :使用这个方法前要先做两件事~
1. 记得在配置类似加上@EnableAspectJAutoProxy(exposeProxy = true)注解来暴露这个代理对象
2. 加上依赖:
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
</dependency>
完整代码;:
package com.hmdp.service.impl;
import com.hmdp.dto.Result;
import com.hmdp.entity.SeckillVoucher;
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.aop.framework.AopContext;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.time.LocalDateTime;
/**
* <p>
* 服务实现类
* </p>
*
* @author 虎哥
* @since 2021-12-22
*/
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {
@Resource
private ISeckillVoucherService iSeckillVoucherService;
@Resource
private RedisIdWorker redisIdWorker;
@Override
public Result seckillVoucher(Long voucherId) {
//1.获取优惠券信息
SeckillVoucher voucher = iSeckillVoucherService.getById(voucherId);
//2.判断是否已经开始
if (voucher.getBeginTime().isAfter(LocalDateTime.now())){
Result.fail("秒杀尚未开始!");
}
//3.判断是否已经结束
if (voucher.getEndTime().isBefore(LocalDateTime.now())){
Result.fail("秒杀已经结束了!");
}
//4.判断库存是否充足
if (voucher.getStock() < 1) {
Result.fail("库存不充足!");
}
//5.扣减库存
boolean success = iSeckillVoucherService.update()
.setSql("stock = stock-1").eq("voucher_id",voucherId).gt("stock",0)
.update();
if (!success){
Result.fail("库存不充足!");
}
Long userId = UserHolder.getUser().getId();
synchronized (userId.toString().intern()){
IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
return proxy.createVoucherOrder(voucherId);
}
}
@Transactional
public Result createVoucherOrder(Long voucherId) {
Long userId = UserHolder.getUser().getId();
//6.根据优惠券id和用户id判断订单是否已经存在
//如果存在,则返回错误信息
int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
if (count > 0) {
return Result.fail("用户已经购买!");
}
//7. 创建订单
VoucherOrder voucherOrder = new VoucherOrder();
//7.1添加订单id
Long orderId = redisIdWorker.nextId("order");
voucherOrder.setId(orderId);
//7.2添加用户id
voucherOrder.setUserId(userId);
//7.3添加优惠券id
voucherOrder.setVoucherId(voucherId);
save(voucherOrder);
//8.返回订单id
return Result.ok(orderId);
}
}