3 应对秒杀

3.1 全局唯一ID



public class RedisIdWorker {private static final long START_TIME = LocalDateTime.of(2022, 1, 1, 0, 0, 0).toEpochSecond(ZoneOffset.UTC);private static final int COUNT_BITS = 32;@Autowiredprivate StringRedisTemplate stringRedisTemplate;public long nextId(String keyPrefix){// 时间戳long now = LocalDateTime.now().toEpochSecond(ZoneOffset.UTC);now = now - START_TIME;// 生成序列号String key = "icr:" + keyPrefix + ":" + LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));long count = stringRedisTemplate.opsForValue().increment(key);// 拼接并返回return now << COUNT_BITS | count;}public static void main(String[] args) {int i = 0 << 32 | 100;System.out.println(i);}}

3.2 秒杀 超卖



seckillVoucher.setStock((seckillVoucher.getStock() - 1));boolean update = seckillVoucherService.update().setSql("stock = stock -1").eq("voucher_id",voucherId).gt("stock",0).update();if (!update) {return Result.fail("优惠卷已售空");}
@Transactionalpublic Result createUserOrderVoucher(Long voucherId, SeckillVoucher seckillVoucher, LocalDateTime now) {Long id = UserHolder.getUser().getId();// 对id一样的字符串常量池加锁 不影响 this 这把钥匙的使用权// 因为使用的是 防止同一个人下单两次synchronized (id.toString().intern()) {int count = query().eq("user_id", id).eq("voucher_id", voucherId).count();if (count > 0) {return Result.fail("您只能下一单");}seckillVoucher.setStock((seckillVoucher.getStock() - 1));boolean update = seckillVoucherService.update().setSql("stock = stock -1").eq("voucher_id", voucherId).gt("stock", 0).update();if (!update) {return Result.fail("优惠卷已售空");}VoucherOrder voucherOrder = new VoucherOrder();voucherOrder.setId(idWorker.nextId("order"));voucherOrder.setUserId(id);voucherOrder.setVoucherId(voucherId);voucherOrder.setStatus(1);voucherOrder.setUpdateTime(now);voucherOrder.setCreateTime(now);voucherOrder.setPayType(1);save(voucherOrder);return Result.ok("下单成功! 请尽快支付!");}}
微服务情况

3.3 分布式锁




SimpleRedisLock redisLock = new SimpleRedisLock("order:"+id, stringRedisTemplate); // 自定义的锁对象实现boolean isLock = redisLock.tryLock(10, TimeUnit.SECONDS);if (!isLock) {// TODO 注意 这里的锁对象是不同 进程 同一个 用户 的id 不同用户不会产生争抢锁现象 只会判断 where stock > 0return Result.fail("不允许重复下单");}try {int count = query().eq("user_id", id).eq("voucher_id", voucherId).count();if (count > 0) {return Result.fail("您只能下一单");}// ......return Result.ok("下单成功! 请尽快支付!");//}} finally {redisLock.unlock();}
有时效性key的失效导致key误删问题

## 图解:- 线程1 获取到锁 但是由于业务执行时间过长导致锁Key失效 直接会使抢夺锁的线程2 获取到锁并且生成key* 于此同时线程1执行业务完成 直接删除线程2 对应的key 导致线程3 在线程2 执行业务时同时获取到了key**导致了一系列数据不一致问题**

根据存入的value(Thread.id) 判断释放的线程id是否一致
public class SimpleRedisLock implements ILock {private final static String LOCK_PREFIX = "lock:";// 每个服务的jvm初始化放在字符串常量池的 中的UUID 作为唯一标识前缀private static final String ID_PREFIX = UUID.randomUUID().toString(true);private final String name;private final StringRedisTemplate stringRedisTemplate;public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {this.name = name;this.stringRedisTemplate = stringRedisTemplate;}@Overridepublic boolean tryLock(long timeoutSec,TimeUnit unit) {String threadId = ID_PREFIX + Thread.currentThread();Boolean ownLock = stringRedisTemplate.opsForValue().setIfAbsent(LOCK_PREFIX + name, threadId,timeoutSec,unit);return Boolean.TRUE.equals(ownLock);}@Overridepublic void unlock() {String threadId = ID_PREFIX + Thread.currentThread();String id = stringRedisTemplate.opsForValue().get(LOCK_PREFIX + name);// 判断将要执行删除锁的服务线程是否为加锁线程 解决锁误删问题if (! threadId.equals(id)) {throw new BusinessLock("释放锁异常");}stringRedisTemplate.delete(LOCK_PREFIX + name);}class BusinessLock extends RuntimeException {private String msg;public BusinessLock(String msg) {super(msg);}public BusinessLock(Throwable cause, String msg) {super(msg,cause);}}}
阻塞释放问题

# 这里是因为线程1 `释放阻塞` 由于前面对比的版本/id是 value 线程1释放是已经判断过的 不会对比value直接删除 `key` 而# 此时 `key` 中存放的是线程2 的锁value 导致线程3乘虚而入# 这里 key 一致是要保证分布式多个服务之间互斥 value是防止误删
所以要保证获取锁和释放锁是**原子性操作**
if (! threadId.equals(id)) {throw new BusinessLock("释放锁异常");}stringRedisTemplate.delete(LOCK_PREFIX + name);// 这一段执行 `equals` `delete` 是组合操作 很难保证原子性
lua 保证原子性


127.0.0.1:6379> EVAL "return redis.call('mset',KEYS[1],ARGV[1],KEYS[2],ARGV[2])" 2 k1 k2 v1 v2127.0.0.1:6379> EVAL "return redis.call('hset',KEYS[1],AVGS[1],AVGS[2])" h1 name zs
local id = redis.call('GET',KEYS[1])if(id == ARGV[1]) thenreturn redis.call('DEL',KEYS[1])endreturn 0



public class SimpleRedisLock implements ILock {private final static String LOCK_PREFIX = "lock:";// 每个服务的jvm初始化放在字符串常量池的 中的UUID 作为唯一标识前缀private static final String ID_PREFIX = UUID.randomUUID().toString(true);private final String name;private final StringRedisTemplate stringRedisTemplate;private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {this.name = name;this.stringRedisTemplate = stringRedisTemplate;}static {// 加载脚本UNLOCK_SCRIPT = new DefaultRedisScript<>();UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));UNLOCK_SCRIPT.setResultType(Long.class);}@Overridepublic boolean tryLock(long timeoutSec, TimeUnit unit) {String threadId = ID_PREFIX + Thread.currentThread();Boolean ownLock = stringRedisTemplate.opsForValue().setIfAbsent(LOCK_PREFIX + name, threadId, timeoutSec, unit);return Boolean.TRUE.equals(ownLock);}@Overridepublic void unlock() {Long result = stringRedisTemplate.execute(UNLOCK_SCRIPT,Collections.singletonList(LOCK_PREFIX + name),ID_PREFIX + Thread.currentThread().getId());// todo 缩减为一行代码 优化误删问题 但是归根还是超时问题if (result == 0) {throw new BusinessLock("释放锁异常");}}/*@Overridepublic void unlock() {String threadId = ID_PREFIX + Thread.currentThread();String id = stringRedisTemplate.opsForValue().get(LOCK_PREFIX + name);// 判断将要执行删除锁的服务线程是否为加锁线程 解决锁误删问题if (! threadId.equals(id)) {throw new BusinessLock("释放锁异常");}stringRedisTemplate.delete(LOCK_PREFIX + name);}*/class BusinessLock extends RuntimeException {private String msg;public BusinessLock(String msg) {super(msg);}public BusinessLock(Throwable cause, String msg) {super(msg, cause);}}}
相关业务层
@Transactionalpublic Result createUserOrderVoucher(Long voucherId, SeckillVoucher seckillVoucher, LocalDateTime now) {Long id = UserHolder.getUser().getId();// 对id一样的字符串常量池加锁 不影响 this 这把钥匙的使用权// 因为使用的是 防止同一个人下单两次// synchronized (id.toString().intern()) {// 只锁该对象的下单重复// 多用户的秒杀有 where stock > 0 的原子锁保证SimpleRedisLock redisLock = new SimpleRedisLock("order:"+id, stringRedisTemplate);boolean isLock = redisLock.tryLock(10, TimeUnit.SECONDS);if (!isLock) {// TODO 注意 这里的锁对象是不同 进程 同一个 用户 的id 不同用户不会产生争抢锁现象 只会判断 where stock > 0return Result.fail("不允许重复下单");}try {int count = query().eq("user_id", id).eq("voucher_id", voucherId).count();if (count > 0) {return Result.fail("您只能下一单");}seckillVoucher.setStock((seckillVoucher.getStock() - 1));boolean update = seckillVoucherService.update().setSql("stock = stock -1").eq("voucher_id", voucherId).gt("stock", 0).update();if (!update) {return Result.fail("优惠卷已售空");}VoucherOrder voucherOrder = new VoucherOrder();voucherOrder.setId(idWorker.nextId("order"));voucherOrder.setUserId(id);voucherOrder.setVoucherId(voucherId);voucherOrder.setStatus(1);voucherOrder.setUpdateTime(now);voucherOrder.setCreateTime(now);voucherOrder.setPayType(1);save(voucherOrder);return Result.ok("下单成功! 请尽快支付!");//}} finally {redisLock.unlock();}}
