完整代码
package com.hmdp.service.impl;
import com.hmdp.dto.Result;
import com.hmdp.dto.UserDTO;
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 seckillVoucherService;
@Resource
private RedisIdWorker redisIdWorker;
@Override
public Result seckillVoucher(Long voucherId) {
// 1 查询优惠卷信息
SeckillVoucher seckillVoucher = seckillVoucherService.getById(voucherId);
// 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("库存不足");
}
Long userId = UserHolder.getUser().getId();
// 5 创建订单
// 5.2 加锁
synchronized (userId.toString().intern()){
// 获取代理对象(事务)
IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
return proxy.createVoucherOrder(voucherId);
}
}
@Transactional
public Result createVoucherOrder(Long voucherId) {
// 5.1 判断用户是否已经购买过
Long userId = UserHolder.getUser().getId();
// 5.3 查询订单
int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
if (count > 0) {
// 用户已经购买过
return Result.fail("用户已经购买过");
}
// 5.4 扣减库存
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");
voucherOrder.setId(orderId);
// 6.2 用户id
voucherOrder.setUserId(userId);
// 6.3 优惠券id
voucherOrder.setVoucherId(voucherId);
save(voucherOrder);
// 7 返回订单id
return Result.ok(orderId);
}
}
关键代码
synchronized (userId.toString().intern()){
// 获取代理对象(事务)
IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
return proxy.createVoucherOrder(voucherId);
}
对单个用户加锁
将创建订单的部分提取到createVoucherOrder()方法中,通过 synchronized 块对用户 ID 加锁,使用 userId.toString().intern() 作为锁对象,确保相同用户 ID 的字符串对象在 JVM 字符串池中唯一,intern() 方法保证不同线程中相同用户 ID 的字符串引用指向同一内存地址,实现精确锁定。保证同一用户只能创建一个订单。
使用代理对象确保事务正常
自调用问题:
如果直接在 synchronized 块中调用 this.createVoucherOrder(),由于 this 指向的是原始对象而非代理对象,事务注解 @Transactional 将失效(Spring 的事务基于代理对象实现)
AopContext.currentProxy()的解决方案:
获取当前代理对象 AopContext.currentProxy() 返回当前执行代码的代理对象,确保后续方法调用通过代理执行
启用代理暴露
- 启动类配置:
需在 Spring Boot 启动类添加@EnableAspectJAutoProxy(exposeProxy = true),允许通过AopContext暴露代理对象java @SpringBootApplication @EnableAspectJAutoProxy(exposeProxy = true) public class Application { ... }
总结
代码通过 synchronized 解决了单机环境下的线程安全问题,但事务完整性仍需依赖锁与事务的边界对齐。若需高并发场景下的严格安全,建议采用分布式锁或调整事务边界。对于生产环境,优先选择分布式锁(如 Redisson)并保持默认隔离级别,以平衡性能与安全性