一、全局唯一ID
1.1 知识点介绍
每个店铺都可以发布优惠券,而每张优惠券都是唯一的。当用户抢购时,就会生成订单并保存到 tb_voucher_order 这张表中,而订单表如果使用数据库自增 ID 就存在一些问题:
- id 的规律太明显。如果 id 规律太明显,用户就能够根据 id 猜测出一些信息。比方说,某用户第一天下了一单,此时 id 为 10,第二天同一时刻,该用户又下了一单,此时 id 为 100,那么用户就能够推断出昨天一天店家卖出了 90 单,这就将一些信息暴露给用户。
- 受单表数据量的限制。订单的一个特点就是数据量比较大,只要用户不停地产生购买行为,就会不停地产生新的订单。如果网站做到一定的规模,用户量达到数百万,这时候每天都会产生数十万甚至近百万的订单,一年下来就会达到数千万的订单,那么两年三年不断累积下来,订单量就会越来越庞大,此时单张表就无法保存这么多的订单数据,就需要将单张表拆分成多张表。MySQL 的每张表会自己计算自己的自增长,如果每张表都使用自增长,订单 id 就一定会重复。
全局 ID 生成器,是一种在分布式系统下用来生成全局唯一 ID 的工具,一般要满足下列特性:
为了增加 ID 的安全性,我们可以不直接使用 Redis 自增的数值,而是拼接一些其它信息: ID 组成部分:
- 符号位:1 bit,永远为 0
- 时间戳:31 bit,以秒为单位,可以使用 69 年
- 序列号:32 bit,秒内的计数器,支持每秒产生 2^32 个不同的 ID
1.2 Redis 实现全局唯一 id
package com.hmdp.utils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
@Component
public class RedisIdWorker {
private static final long BEGIN_TIMESTAMP = 1640995200L;
private static int COUNT_BITS = 32;
@Autowired
private StringRedisTemplate stringRedisTemplate;
public Long nextId(String keyPrefix){
LocalDateTime now = LocalDateTime.now();
long nowSecond = now.toEpochSecond(ZoneOffset.UTC);
long time = nowSecond - BEGIN_TIMESTAMP;
String format = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
Long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + format);
return time << COUNT_BITS | count;
}
public static void main(String[] args) {
LocalDateTime of = LocalDateTime.of(2022, 1, 1, 0, 0, 0);
long l = of.toEpochSecond(ZoneOffset.UTC);
System.out.println(l);
}
}
测试:
@SpringBootTest
class HmDianPingApplicationTests {
@Autowired
private RedisIdWorker redisIdWorker;
private ExecutorService es = Executors.newFixedThreadPool(500);
@Test
void testIdWorker() throws InterruptedException {
CountDownLatch latch = new CountDownLatch(300);
Runnable task = () -> {
for (int i = 0; i < 100; i++) {
Long id = redisIdWorker.nextId("order");
System.out.println("id = " + id);
}
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));
}
}
1.3 总结
全局唯一 ID 生成策略:
- UUID:16进制的字符串ID,可以做唯一ID,但不支持自增
- Redis 自增
- snowflake 雪花算法:long 类型的 64 ID,性能更好,但是比较依赖于时钟,如果时间不准确,可能会出现异常问题
- 数据库自增:单独创建一张表,用于实现自增
Redis 自增 ID 策略:
- 每天一个 key,方便统计订单量
- ID 构造是 时间戳 + 计数器
二、实现优惠券秒杀下单
2.1 案例分析
每个店铺都可以发布优惠券,分为平价券和特价券。平价券可以任意购买,而特价券需要秒杀抢购: 表关系如下:
- tb_voucher:优惠券的基本信息,优惠金额、使用规则等。
- tb_seckill_voucher:优惠券的库存、开始抢购时间,结束抢购时间。特价优惠券才需要填写这些信息。
实现优惠券秒杀的下单功能
下单时需要判断两点:
- 秒杀是否开始或结束,如果尚未开始或已经结束则无法下单
- 库存是否充足,不足则无法下单
2.2 代码实现
VoucherOrderController
@RestController
@RequestMapping("/voucher-order")
public class VoucherOrderController {
@Autowired
private IVoucherOrderService voucherOrderService;
@PostMapping("seckill/{id}")
public Result seckillVoucher(@PathVariable("id") Long voucherId) {
return voucherOrderService.seckillVoucher(voucherId);
}
}
IVoucherOrderService
public interface IVoucherOrderService extends IService<VoucherOrder> {
Result seckillVoucher(Long voucherId);
}
VoucherOrderServiceImpl
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {
@Autowired
private ISeckillVoucherService seckillVoucherService;
@Autowired
private RedisIdWorker redisIdWorker;
@Override
@Transactional
public Result seckillVoucher(Long voucherId) {
SeckillVoucher seckillVoucher = seckillVoucherService.getById(voucherId);
if (seckillVoucher.getBeginTime().isAfter(LocalDateTime.now())) {
Result.fail("秒杀尚未开始!");
}
if (seckillVoucher.getEndTime().isBefore(LocalDateTime.now())) {
Result.fail("秒杀已经结束!");
}
if (seckillVoucher.getStock() < 1) {
Result.fail("库存不足!");
}
boolean success = seckillVoucherService.update().
setSql("stock = stock - 1").
eq("voucher_id", voucherId).update();
if(!success){
return Result.fail("库存不足!");
}
VoucherOrder voucherOrder = new VoucherOrder();
Long orderId = redisIdWorker.nextId("order");
voucherOrder.setVoucherId(voucherId);
Long userId = UserHolder.getUser().getId();
voucherOrder.setUserId(userId);
voucherOrder.setId(orderId);
save(voucherOrder);
return Result.ok(orderId);
}
}
|