java 篇: 1.基础地基 2.设计原理 3.项目实战
产品原型分析:



然后就是基础代码构架,执行提供 sql 语句,mp 生成代码,当中字段替换成枚举类。过
实现查询发放中的优惠券接口:


代码实现:

@Override
public List<CouponVO> queryIssuingCoupons() {
//1.查询发放中的优惠券列表
List<Coupon> coupons = lambdaQuery()
.eq(Coupon::getStatus, CouponStatus._ISSUING_)
.eq(Coupon::getObtainWay, ObtainType._PUBLIC_)
.list();
if (CollUtils._isEmpty_(coupons)){
return CollUtils._emptyList_();
}
//2.统计当前用户已经领取的优惠券的消息
List<Long> couponIds = coupons.stream().map(Coupon::getId).collect(Collectors._toList_());
//2.1.查询当前用户已经领取的优惠券的数据
List<UserCoupon> userCoupons = userCouponService.lambdaQuery()
.eq(UserCoupon::getUserId, UserContext._getUser_())
.in(UserCoupon::getCouponId, couponIds)
.list();
//2.2.统计当前用户对优惠券的已经领取数量
Map<Long, Long> issuedMap = userCoupons.stream()
.collect(Collectors._groupingBy_(UserCoupon::getCouponId, Collectors._counting_()));
//2.3.统计当前用户对优惠券的已经领取并且未使用的数量
Map<Long, Long> unusedMap = userCoupons.stream()
.filter(uc -> uc.getStatus() == UserCouponStatus._UNUSED_)
.collect(Collectors._groupingBy_(UserCoupon::getCouponId, Collectors._counting_()));
//3.封装VO结果
List<CouponVO> list = new ArrayList<>(coupons.size());
for (Coupon c : coupons){
//3.1.拷贝PO属性到VO
CouponVO vo = BeanUtils._copyBean_(c, CouponVO.class);
list.add(vo);
//3.2.是否可以领取:已经被领取的数量 < 优惠券总数量 && 当前用户已经领取的数量 < 每人限领数量
vo.setAvailable(
c.getIssueNum() < c.getTotalNum()
&& issuedMap.getOrDefault(c.getId(), 0L) < c.getUserLimit());
//3.3.是否可以使用:当前用户已经领取并且未使用的优惠券数量 > 0
vo.setReceived(unusedMap.getOrDefault(c.getId(), 0L) > 0);
}
return list;
}

这里注意下,然后业务逻辑上注意一下,尽量减少查表的次数。crud 懂
测试:


解决登录拦截放行问题:

解决未登录,不能查看优惠券的问题
回忆一下登录用户获取的整个流程


UserInfoInterceptor:看下有木有用户信息,如果没有就算了,如果有,帮你转为 Long 类型。
LoginAuthInterceptor:登录拦截,根据有木有用户信息,进行判断
那这个登录拦截,拦截哪些路径呢,这就需要去看配置
ResourceInterceptorConfiguration




添加框里这句,就好了
测试一下

成功
实现领取优惠券接口:


代码实现:

package com.tianji.promotion.service.impl;
import com.tianji.common.exceptions.BadRequestException;
import com.tianji.common.utils.UserContext;
import com.tianji.promotion.domain.po.Coupon;
import com.tianji.promotion.domain.po.UserCoupon;
import com.tianji.promotion.mapper.CouponMapper;
import com.tianji.promotion.mapper.UserCouponMapper;
import com.tianji.promotion.service.IUserCouponService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
_/**_
_ * <p>_
_ * 用户领取优惠券的记录,是真正使用的优惠券信息 服务实现类_
_ * </p>_
_ *_
_ * @author 叶小鸡_
_ * @since 2026-05-02_
_ */_
@Service
@RequiredArgsConstructor
public class UserCouponServiceImpl extends ServiceImpl<UserCouponMapper, UserCoupon> implements IUserCouponService {
private final CouponMapper couponMapper;
@Override
@Transactional
public void receiveCoupon(Long couponId) {
//1.查询优惠券
Coupon coupon = couponMapper.selectById(couponId);
if (coupon == null){
throw new BadRequestException("优惠券不存在");
}
//2.校验发放时间
LocalDateTime now = LocalDateTime._now_();
if (now.isBefore(coupon.getIssueBeginTime()) || now.isAfter(coupon.getIssueEndTime())){
throw new BadRequestException("优惠券发放已经结束或尚未开始");
}
//3.校验库存
if (coupon.getIssueNum() >= coupon.getTotalNum()){
throw new BadRequestException("优惠券库存不足");
}
Long userId = UserContext._getUser_();
//4.校验每人限领数量
//4.1.统计当前用户对当前优惠券的已经领取的数量
Integer count = lambdaQuery()
.eq(UserCoupon::getUserId, userId)
.eq(UserCoupon::getCouponId, couponId)
.count();
//4.2.判断
if (count != null && count >= coupon.getUserLimit()){
throw new BadRequestException("领取次数太多");
}
//5.更新优惠券的已经发放的数量 + 1
couponMapper.incrIssueNum(couponId);
//6.新增一个用户券
saveUserCoupon(coupon, userId);
}
private void saveUserCoupon(Coupon coupon, Long userId) {
//1.基本信息
UserCoupon uc = new UserCoupon();
uc.setUserId(userId);
uc.setCouponId(coupon.getId());
//2.有效期信息
LocalDateTime termBeginTime = coupon.getTermBeginTime();
LocalDateTime termEndTime = coupon.getTermEndTime();
if (termBeginTime == null){
termBeginTime = LocalDateTime._now_();
termEndTime = termBeginTime.plusDays(coupon.getTermDays());
}
uc.setTermBeginTime(termBeginTime);
uc.setTermEndTime(termEndTime);
//3.保存
save(uc);
}
}

避免循环依赖


保存部分代码有点长,封装成一个函数。

最后别忘了加上事务注解

测试一下:
先把 user_coupon 表清空,然后把 coupon 表的 issue_num 设置为 0


ok,不过这里我自己没测,过。
兑换码兑换优惠券:


先查 redis,如果已兑换,直接在 redis 里就把请求给拦截了,不会再去访问数据库,防止恶意刷请求去增加数据库的压力

代码实现:

先搭个基础框架

这里涉及到并发安全问题,如果多个线程去用 GET KEY 判断,返回都是 0,那 1 个码兑换了 N 个券。
如果 GET 和 SET 分开来,就会出现这样的问题。所以这里直接用 SETBIT 替代 GETBIT,因为 SETBIT 也是会返回旧值的。保证两个操作的原子性。

那如果标记为 1,下面没有执行成功怎么办,那就手动回滚

@Override
@Transactional
public void exchangeCoupon(String code) {
//1.校验并解析兑换码
long serialNum = CodeUtil._parseCode_(code);
//2.校验是否已经兑换 GETBIT KEY 4 1
boolean exchanged = codeService.updateExchangeMark(serialNum, true);
if (exchanged){
throw new BizIllegalException("兑换码已经兑换过了");
}
try {
//3.查询兑换码
ExchangeCode exchangeCode = codeService.getById(serialNum);
if (exchangeCode == null){
throw new BizIllegalException("兑换码不存在!");
}
//4.是否过期
LocalDateTime now = LocalDateTime._now_();
if (now.isAfter(exchangeCode.getExpiredTime())){
throw new BizIllegalException("兑换码已经过期");
}
//5.校验并生成用户券
//5.1.查询优惠券
Coupon coupon = couponMapper.selectById(exchangeCode.getExchangeTargetId());
//5.2.获取用户
Long userId = UserContext._getUser_();
checkAndCreateUserCoupon(coupon, userId);
//6.更新兑换码状态
codeService.lambdaUpdate()
.set(ExchangeCode::getUserId, userId)
.set(ExchangeCode::getStatus, ExchangeCodeStatus._USED_)
.eq(ExchangeCode::getId, exchangeCode.getId())
.update();
} catch (Exception e) {
//重置兑换的标记 0
codeService.updateExchangeMark(serialNum, false);
throw e;
}
}


这里返回涉及拆箱,需要对空指针进行处理。这里直接传入 serialNum,直接放弃第一个位置,提升性能。
因为.boundValueOps 绑定 KEY 后,没有 setBit 方法,所以还得用 redisTemplate



这个部分跟前面的重复了,直接进行封装,提高代码的复用性。

最后呢,加上事务注解,和抛出异常


测试一下:




并发安全问题超卖问题:

将资料当中的这个,直接拖到 jmeter 左侧栏
然后改下配置


启动前,先去看下数据库当中的优惠券过期时间等信息是否合理。然后启动,如果发现不对,看下后台

启动之后,你就会看到库存超卖的情况


理想情况下:

超卖情况下:


乐观锁实现:

修改相关的代码:




重启服务,恢复数据库数据,重新测试,这个时候线程设为 400 就行



咋就只有了 53 个呢,原先的判断太苛刻了,我们只需要保证 issue_num < total_num 就行了

换回来



经典一人一单问题,悲观锁保证一人一单,乐观锁保证不超卖,配合数据库索引作为兜底
并发安全问题琐失效和锁边界问题:

如果一个人从来没有领过,数据库当中没有他的数据,然后他开启无数个线程,同时攻击,那它就领到了无数张券。

但这里测下来,我并没有出现这个问题,不知道为啥。
那加悲观锁
① 直接在方法上加上 synchronized

直接变成串行执行,那整个业务的性能会受到特别大的影响。
② 将 synchronized 锁的粒度缩小,锁 userId,同步代码块。不同的用户没有影响。同一个用户开启多个线程,才会有影响。

这里 userId 涉黄,方法参数有可能变的

转成字符串常量,这样就好了
测试了一下

怎么还是有问题
原来 toString new 了一个新对象

虽然 userId 相同,但是对象不同,锁就不同

这样就是都获取常量池当中的值了
测试一下,还是 8
理想情况:

事务导致:

线程 1 执行新增后释放锁,实际已经新增了,但是还没有提交事务,事务的隔离级别为读已提交,所以线程 2 看不到,它也新增了。
解决方案:

调换一下提交事务和释放锁的顺序


测试了一下,问题解决了
并发安全问题事务失效问题:
但是这里事务并没有生效,也就是不会回滚,因为是内部调用


一级方法没有添加事务,也就意味着没有被动态代理,也就是原有的 OrderService 调用的,那它内部的方法自然也是它本身调用的,所以不是代理对象调用的,事务不会生效。


要么抛出 RuntimeException 异常,要么就改 rollbackFor 当中的异常类型

本质就是 reduceStock()和原来的不是同个事务,出了异常要回滚,也不相干了。传播行为默认是 REQUIRE ,这里是 REQUIRES_NEW 搞出个新的,看见没

以上就是主要事务失效的原因。
本题属于第二种。
解决方案:

其实这里也可以不引用,因为父类引用过了

启动类添加注解,默认为 false,改为 true
改下实现类

记得抽取一下


把这里事务注解去掉。
ok,搞定。
如果对你有帮助的话,请点赞,关注,收藏。热爱可抵一切!👍 ❤️ 🔥