Redis实现购物秒杀及分布式锁
全局唯一ID
Redis自增ID策略
ID构造是:时间戳 + 计数器
每天一个key,方便统计订单量
业务实现
获取指定时间的秒数
LocalDateTime timeBegin = LocalDateTime.of(2024, 1, 1, 0, 0, 0);
long second = timeBegin.toEpochSecond(ZoneOffset.UTC);
获取当前时间的秒数
long now = LocalDateTime.now().toEpochSecond(ZoneOffset.UTC);
全局唯一ID业务代码
public Long getID(String key) {
// 获取时间戳
long now = LocalDateTime.now().toEpochSecond(ZoneOffset.UTC);
long timestamp = now - TIMESTAMP_BEGIN;
// 利用redis实现自增长
String date = new SimpleDateFormat("yyyyMMdd").format(new Date());
Long inc = redisTemplate.opsForValue().increment("ID:" + key + ":" + date);
// 拼接并返回
return timestamp << 32 | inc;
}
实现秒杀下单
秒杀下单逻辑流程
业务实现
@Transactional
public String getProduct(Long id) {
// 判断商品是否存在
Product pro = getById(id);
if (pro == null) {
throw new BusinessException(400, "商品不存在");
}
// 判断库存
int num = pro.getNum();
if (num <= 0) {
throw new BusinessException(400, "库存不足");
}
// 库存 - 1
pro.setNum(num - 1);
update(pro, new QueryWrapper<>());
// 下订单
Ordertable order = new Ordertable();
order.setOrderID(idUtil.getID("order").toString());
order.setUserID(123);
ordertableService.save(order);
return order.getOrderID();
}
“超卖”问题
问题复现
在多线程并发会产生问题
使用5000个线程进行测试
20个库存卖了210个订单
原因分析
锁的类型
超卖问题是典型的多线程安全问题,针对这一问题的常见解决方案就是加锁
悲观锁:认为线程安全问题一定会发生,因此在操作数据之前先获取锁,确保线程串行执行。
- 例如Synchronized、Lock都属于悲观锁
乐观锁:认为线程安全问题不一定会发生,因此不加锁,只是在更新数据时去判断有没有其它线程对数据做了修改。
- 如果没有修改则认为是安全的,自己才更新数据。
- 如果已经被其它线程修改说明发生了安全问题,此时可以重试或异常。
乐观锁
乐观锁的关键是判断之前查询得到的数据是否有被修改过。(CAS法)