Redis全局唯一ID
全局ID生成器
订单表使用数据库自增ID存在一些问题:
- ID的规律性太明显,容易暴露业务数据量
- 分库分表后,不同表可能生成重复ID
- 受单表数据量和单库写入能力的限制
全局ID生成器,是一种在分布式系统下用来生成全局唯一ID的工具,一般要满足下列特性:
- 唯一性
- 高可用
- 高性能
- 递增性
- 安全性
为了增加ID的安全性,不直接使用Redis自增的数值,而是拼接时间戳等其它信息。
ID的组成
64位long类型表示:
- 符号位:1 bit,固定为0
- 时间戳:31 bit,以秒为单位,大约可以使用68年
- 序列号:32 bit,使用Redis自增值生成
0 | 31位时间戳 | 32位序列号生成公式:
return timestamp << COUNT_BITS | count;使用相对时间戳而不是完整时间戳,可以减少时间戳占用的位数。
Redis自增策略
Redis Key按照“业务类型+日期”划分:
icr:order:2026:07:09icr:order:2026:07:10这样可以让不同业务使用不同计数器,并且能够按天统计订单量。
Redis的INCR命令是原子操作,多线程并发执行时不会生成重复序号。
代码中的计数器实际上是按天递增,因此同一个Key在一天内不能超过2^32 - 1。
RedisIdWorker.java
@Componentpublic class RedisIdWorker {
// 开始时间戳 private static final long BEGIN_TIMESTAMP = 1783555200L; // 序列号的位数 private static final int COUNT_BITS = 32;
private StringRedisTemplate stringRedisTemplate;
public RedisIdWorker(StringRedisTemplate stringRedisTemplate) { this.stringRedisTemplate = stringRedisTemplate; }
public long nextId(String keyPrefix) { // 1.生成时间戳 LocalDateTime now = LocalDateTime.now(); long nowSecond = now.toEpochSecond(ZoneOffset.UTC); long timestamp = nowSecond - BEGIN_TIMESTAMP; // 2.生成序列号 // 2.1获取当前日期,精确到日 String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd")); long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + date); // 3.拼接并返回 return timestamp << COUNT_BITS | count; } public static void main(String[] args) { LocalDateTime time = LocalDateTime.of(2026, 7, 9, 0, 0, 0); long second = time.toEpochSecond(ZoneOffset.UTC); System.out.println(second); }}HmDianPingApplicationTests.java
@Resourceprivate RedisIdWorker redisIdWorker;
private ExecutorService es = Executors.newFixedThreadPool(500);
@Testvoid testIDWorker() throws InterruptedException { CountDownLatch latch = new CountDownLatch(300);
Runnable task = () -> { try { for (int i = 0; i < 100; i++) { long id = redisIdWorker.nextId("order"); System.out.println("id= " + id); } } finally { latch.countDown(); } };
long begin = System.currentTimeMillis();
for (int i = 0; i < 300; i++) { es.submit(task); }
latch.await();
long end = System.currentTimeMillis();
System.out.println("time = " + (end - begin));
es.shutdown();}全局ID常见实现还有:
- UUID
- 数据库自增
- Redis自增
- Snowflake雪花算法
优惠券添加
数据库使用两张表保存优惠券信息:
tb_voucher保存优惠券基本信息,例如优惠金额、使用规则、折扣信息和店铺ID。
tb_seckill_voucher保存秒杀库存、秒杀开始时间和秒杀结束时间。只有秒杀优惠券才需要保存这些信息。
VoucherController.java
@RestController@RequestMapping("/voucher")public class VoucherController {
@Resource private IVoucherService voucherService;
/** * 新增秒杀券 * @param voucher 优惠券信息,包含秒杀信息 * @return 优惠券id */ @PostMapping("seckill") public Result addSeckillVoucher(@RequestBody Voucher voucher) { voucherService.addSeckillVoucher(voucher); return Result.ok(voucher.getId()); }}VoucherOrderServiceImpl.java
@Override@Transactionalpublic Result seckillVoucher(Long voucherId) { // 1.查询优惠卷 SeckillVoucher voucher = seckillVoucherService.getById(voucherId); // 2.判断秒杀是否开始 if (voucher.getBeginTime().isAfter(LocalDateTime.now())) { // 尚未开始 return Result.fail("秒杀尚未开始:"); } // 3.判断秒杀时候已经结束 if (voucher.getEndTime().isBefore(LocalDateTime.now())) { return Result.fail("秒杀已经结束:"); } // 4.判断库存是否充足 if (voucher.getStock()<1) { // 库存不足 return Result.fail("库存不足"); } // 5.扣减库存 boolean success = seckillVoucherService.update() .setSql("stock = stock - 1") .eq("voucher_id", voucherId) .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 Long userId = UserHolder.getUser().getId(); voucherOrder.setUserId(userId); // 6.3 代金劵id voucherOrder.setVoucherId(voucherId); save(voucherOrder); // 7 返回订单 return Result.ok(orderId);}商品超卖
原因是查询库存、判断库存和扣减库存不是一个不可分割的原子操作。
悲观锁
悲观锁认为线程安全问题一定会发生,因此操作共享数据前先获取锁,使线程串行执行。
常见实现:
- synchronized
- Lock
- 数据库排他锁
优点是实现简单、安全性直观;缺点是线程需要等待,并发性能相对较低。
乐观锁
乐观锁不直接加锁,而是在更新数据时判断数据是否已经被其他线程修改。
常见方式:
- 版本号法
- CAS法
- 使用库存本身作为判断条件
版本号法:
UPDATE tb_seckill_voucherSET stock = stock - 1, version = version + 1WHERE voucher_id = ? AND version = ?;秒杀库存只需要保证库存不小于0,可以使用stock > 0作为更新条件:
VoucherOrderServiceImpl.java
// 5.扣减库存boolean success = seckillVoucherService.update() .setSql("stock = stock - 1") .eq("voucher_id", voucherId).gt("stock",0) .update();if(!success) { // 扣减失败 return Result.fail("库存不足");}一人一单
需求:同一个用户只能购买同一种优惠券一次。
单机环境加锁
VoucherOrderServiceImpl.java
Long userId = UserHolder.getUser().getId();synchronized(userId.toString().intern()) { // 获取代理对象(事物) IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy(); return proxy.createVoucherOrder(voucherId);}锁对象使用用户ID,能够使同一用户的请求互斥,同时允许不同用户并发下单。
创建订单
VoucherOrderServiceImpl.java
@Transactionalpublic 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("用户已经购买一次"); }
// 5.扣减库存 boolean success = seckillVoucherService.update() .setSql("stock = stock - 1") // set stock = stock - 1 .eq("voucher_id", voucherId).gt("stock",0) // where id = ? and stock = ? .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 返回订单 return Result.ok(orderId);}HmDianPingApplication.java
// 启动类@EnableAspectJAutoProxy(exposeProxy = true)@MapperScan("com.hmdp.mapper")@SpringBootApplicationpublic class HmDianPingApplication {
public static void main(String[] args) { SpringApplication.run(HmDianPingApplication.class, args); }}xml
<dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId></dependency>数据库唯一索引兜底
即使业务代码已经加锁,也建议在数据库中增加唯一索引:
ALTER TABLE tb_voucher_orderADD CONSTRAINT uk_user_voucherUNIQUE (user_id, voucher_id);唯一索引是“一人一单”的最后一道防线,可以防止程序异常、锁失效或重复消息造成重复订单。
集群环境中的锁失效问题
集群环境下,请求可能分别进入JVM1和JVM2。每个JVM都有自己的锁监视器,因此两个线程都可能获得各自JVM中的锁。
synchronized只能保证单个JVM内部互斥,不能解决集群环境中的并发问题。














