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;
@Autowired
private 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("优惠卷已售空");
}
@Transactional
public 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 > 0
return 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;
}
@Override
public 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);
}
@Override
public 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 v2
127.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]) then
return redis.call('DEL',KEYS[1])
end
return 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);
}
@Override
public 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);
}
@Override
public 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("释放锁异常");
}
}
/*@Override
public 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);
}
}
}
相关业务层
@Transactional
public 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 > 0
return 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();
}
}