当前位置: 首页 > news >正文

JAVA-实战8 Redis实战项目—雷神点评(3)订单

夢の未来 君と僕のLIVE&LIFE

雷神点评——订单

订单ID

当用户抢购时,就会生成订单并保存到tb_voucher_order这张表中,而订单表如果使用数据库自增ID就存在一些问题:

id的规律性太明显
受单表数据量的限制

全局ID生成器

全局ID生成器,是一种在分布式系统下用来生成全局唯一ID的工具,一般要满足下列特性:

唯一性
高性能
高可用
递增性
安全性

Redis实现全局唯一ID

目前业内主流的分布式全局ID生成算法是雪花算法(Snowflake),这里使用其一种方式:

长度固定64位,每一位取值均为0/1
1位符号位‌:固定为0,表示正数。
31位时间戳‌:以秒为单位,可使用约69年。
32位机器ID‌:秒内计数器,每秒产生\(2^{32}\)个不同ID

获取当前时间(2026年5月4日00:)的时间戳1777852800

LocalDateTime NowTime = LocalDateTime.of(2026,5,4,0,0,0);
long NowSecond = NowTime.toEpochSecond(ZoneOffset.UTC);
System.out.println(NowSecond);

全局ID生成器代码如下,其中使用NowDate防止多次使用同一天造成自增ID过大:

@Component
public class RedisIdWorker {// 起始时间戳private static final long BEGIN_TIMESTAMP = 1777852800L;private static final int COUNT_BITS = 32;@Autowiredprivate StringRedisTemplate stringRedisTemplate;public long NewId(String KeyPrefix){// 获取前31位时间戳LocalDateTime NowTime = LocalDateTime.now();long NowSecond = NowTime.toEpochSecond(ZoneOffset.UTC);long TimeStamp = NowSecond - BEGIN_TIMESTAMP;// 获取后32位序列号String NowDate = NowTime.format(DateTimeFormatter.ofPattern("yyyyMMdd"));long SequenceNumber = stringRedisTemplate.opsForValue().increment("icr:"+KeyPrefix+":"+NowDate);return TimeStamp<<COUNT_BITS | SequenceNumber;}
}

优惠券秒杀下单

每个店铺都可以发布优惠券,分为平价券和特价券。平价券可以任意购买,而特价券需要秒杀抢购。

表关系如下:

tb_voucher:优惠券的基本信息,优惠金额、使用规则等
tb_seckill_voucher:优惠券的库存、开始抢购时间,结束抢购时间。特价优惠券才需要填写这些信息

新增秒杀券

// 控制层方法
@RestController
@RequestMapping("/voucher")
public class VoucherController {@Autowiredprivate VoucherService voucherService;@Autowiredprivate VoucherSecondKillService voucherSecondKillService;@PostMapping("/secondkill")public ResultData AddSecondKillVoucher(@RequestBody VoucherData NewVoucher) {System.out.println(NewVoucher);voucherService.AddVoucher(NewVoucher);voucherSecondKillService.AddVoucherSecondKill(NewVoucher);return ResultData.success();}
}// 服务层方法
@Service
public class VoucherServiceImpl implements VoucherService {@Autowiredprivate VoucherMapper voucherMapper;@Overridepublic void AddVoucher(VoucherData NewVoucher) {voucherMapper.insert(NewVoucher);}
}@Service
public class VoucherSecondKillServiceImpl implements VoucherSecondKillService {@Autowiredprivate VoucherSecondKillMapper voucherSecondKillMapper;@Overridepublic void AddVoucherSecondKill(VoucherData NewVoucher) {VoucherSecondKillData NewVoucherSecondKillData = new VoucherSecondKillData();NewVoucherSecondKillData.setVoucherId(NewVoucher.getId());NewVoucherSecondKillData.setStock(NewVoucher.getStock());NewVoucherSecondKillData.setBeginTime(NewVoucher.getBeginTime());NewVoucherSecondKillData.setBeginTime(NewVoucher.getEndTime());voucherSecondKillMapper.insert(NewVoucherSecondKillData);}
}

优惠券秒杀下单实现

下单时需要判断两点:

秒杀是否开始或结束,如果尚未开始或已经结束则无法下单
库存是否充足,不足则无法下单

流程示意图如下:
image

代码实现如下:

// 控制层方法
@RestController
@RequestMapping("/voucher-order")
public class VoucherOrderController {@Autowiredprivate VoucherOrderService voucherOrderService;@GetMapping("/secondkill/{id}")public ResultData VoucherSecondKill(@PathVariable("id") Long VoucherId) {System.out.println("Now is "+VoucherId);return voucherOrderService.VoucherSecondKill(VoucherId);}
}// 服务层方法
@Slf4j
@Service
public class VoucherOrderServiceImpl implements VoucherOrderService {@Autowiredprivate VoucherSecondKillMapper voucherSecondKillMapper;@Autowiredprivate VoucherOrderMapper voucherOrderMapper;@Autowiredprivate RedisIdWorker redisIdWorker;@Override@Transactional //添加事务注解保证操作原子性和一致性public ResultData VoucherSecondKill(Long VoucherId) {// 获取优惠券信息VoucherSecondKillData Voucher = voucherSecondKillMapper.selectById(VoucherId);// 查看优惠券是否在有效期内if(Voucher.getBeginTime().isAfter(LocalDateTime.now())) {return ResultData.error("The SecondKill dose not start!");}if(Voucher.getEndTime().isBefore(LocalDateTime.now())) {return ResultData.error("The SecondKill has ended!");}// 查看优惠券还有没有库存if(Voucher.getStock()<1) {return ResultData.error("The Stock is not enough!");}// 更新库存LambdaUpdateWrapper<VoucherSecondKillData> lwp = new LambdaUpdateWrapper<VoucherSecondKillData>();lwp.eq(VoucherSecondKillData::getVoucherId,VoucherId);voucherSecondKillMapper.UpdateSecondKillVoucher(lwp,1L);// 存入新订单VoucherOrderData NewVoucherOrder = new VoucherOrderData();Long VoucherOrderId = redisIdWorker.NewId("Order");NewVoucherOrder.setId(VoucherOrderId);Long UserId = Long.valueOf(CurrentHolder.getCurrent().getId());NewVoucherOrder.setUserId(UserId);NewVoucherOrder.setVoucherId(VoucherId);voucherOrderMapper.insert(NewVoucherOrder);return ResultData.success(NewVoucherOrder);}
}// 持久层接口实现 
@Mapper
public interface VoucherSecondKillMapper extends BaseMapper<VoucherSecondKillData> {void UpdateSecondKillVoucher(@Param(Constants.WRAPPER) LambdaUpdateWrapper<VoucherSecondKillData> ew, @Param("amount") Long amount);
}// xml实现
<update id="UpdateSecondKillVoucher">update tb_seckill_voucher set stock = stock-#{amount} ${ew.customSqlSegment}
</update>

效果如下:
image

优惠券超卖问题

实际开发中多线程并发秒杀优惠券的情况下会导致优惠券库存判断错误,可以使用版本号实现乐观锁解决此问题。

一人一单

修改秒杀业务,要求实际开发中同一个优惠券一个用户只能下最多一单
image

实现代码如下:

@Override
@Transactional
public ResultData VoucherSecondKill(Long VoucherId) {VoucherSecondKillData Voucher = voucherSecondKillMapper.selectById(VoucherId);if(Voucher.getBeginTime().isAfter(LocalDateTime.now())) {return ResultData.error("The SecondKill dose not start!");}if(Voucher.getEndTime().isBefore(LocalDateTime.now())) {return ResultData.error("The SecondKill has ended!");}if(Voucher.getStock()<1) {return ResultData.error("The Stock is not enough!");}// 根据用户ID和优惠券ID同时查询购买记录,查询该用户是否已经购买Long UserId = Long.valueOf(CurrentHolder.getCurrent().getId());QueryWrapper<VoucherOrderData> orderlwp = new QueryWrapper<VoucherOrderData>();orderlwp.eq("voucher_id",VoucherId).eq("user_id",UserId).select("count(*) as Number");List<Map<String,Object>> Result = voucherOrderMapper.selectMaps(orderlwp);Long Count = (Long) Result.get(0).get("Number");if(Count>0) {return ResultData.error("You have already purchased this voucher!");}LambdaUpdateWrapper<VoucherSecondKillData> lwp = new LambdaUpdateWrapper<VoucherSecondKillData>();lwp.eq(VoucherSecondKillData::getVoucherId,VoucherId);voucherSecondKillMapper.UpdateSecondKillVoucher(lwp,1L);VoucherOrderData NewVoucherOrder = new VoucherOrderData();Long VoucherOrderId = redisIdWorker.NewId("Order");NewVoucherOrder.setId(VoucherOrderId);NewVoucherOrder.setUserId(UserId);NewVoucherOrder.setVoucherId(VoucherId);voucherOrderMapper.insert(NewVoucherOrder);return ResultData.success(NewVoucherOrder);
}

实际开发中需要添加乐观锁应对多线程并发我已经有了一个绝妙的设计方案但是这里写不下了

测试效果如下:

同样用户第二次购买:
image
image

切换用户:
image
image

http://www.jsqmd.com/news/753449/

相关文章:

  • 图像拼接、AR定位核心技:单应性矩阵的‘四点参数化’到底怎么用?附OpenCV与深度学习两种实现
  • 告别ZooKeeper依赖!用kafbat-ui(原kafka-ui)一站式管理Kafka 3.3.1+ KRaft集群
  • Python 爬虫数据处理:爬取富文本内容清理与格式优化
  • Python Django开发者转向微信小程序:从架构理解到第一行代码的完整准备指南
  • 你不是金鱼——Spring AI 聊天记忆从“重启即失忆”到 MySQL 持久化的生产级改造实录
  • VS2022新手必看:手把手教你搞定EasyX的graphics.h头文件缺失问题
  • python msgpack
  • Python 爬虫数据处理:时序爬取数据趋势分析与展示
  • 手把手图解:Linux 0.11 启动时那场关键的‘内存大搬家’(从 0x10000 到 0x0)
  • Altium Designer 22 新手避坑指南:从原理图到PCB的10个关键设置(附快捷键清单)
  • 3步构建Windows任务栏透明化工具TranslucentTB的容器化开发环境
  • 从UE5的坐标转换函数出发,手把手带你复现一个简易的3D拾取Demo(C++/蓝图)
  • 为什么你的IAsyncEnumerable在Azure Functions中内存暴涨300%?C# 13新配置项AsyncStreamOptions.BufferCapacity正在悄悄改写GC命运
  • 65周作业
  • TTP223触摸模块的5个常见坑与避坑指南:从模式切换、电平匹配到驱动能力详解
  • C#/.NET 6下用NModbus4快速搭建Modbus TCP从站(附完整源码与ModbusPoll测试)
  • 避开MATLAB优化这些坑:fminsearch和fmincon初值设置与全局最优解搜寻指南
  • 2026 全国防水公司 TOP5 权威排名 - 企业资讯
  • 快手网页版扫码登录的Python逆向手记:我是如何‘抓’出那三个关键接口的
  • 为什么92%的C#医疗系统在FHIR 2026适配中卡在Resource Validation?——基于HL7官方Test Server压测的.NET源码级调试日志解密
  • 如何用Python快速接入Taotoken并调用多个大模型API
  • STM32MP257D异构计算模块MYC-LD25X解析与应用
  • 基于MCP协议的邮件设计自动化:AI驱动的高兼容性邮件模板生成
  • 多模态旋转位置编码原理与医疗影像应用实践
  • 企业如何利用多模型聚合能力优化内部知识问答系统
  • AI厨房管家:用Git工作流与LLM打造可复现的智能食谱系统
  • Python 爬虫高级实战:多环境爬虫配置统一管理方案
  • TCGA数据实战:用sva和limma搞定批次效应,附COAD/READ结肠癌数据完整R代码
  • Music Tag Web音乐标签编辑器:从新手到高手的完整使用指南
  • 你的LCD1602 I2C地址不对?手把手教你用Arduino IDE扫描并修复0x27/0x3F地址冲突问题