黑马点评
三、优惠券秒杀
1、全局ID生成器
utils/RedisIdWorker
@Component
public class RedisIdWorker {
/**
* 开始时间戳
*/
private static final long BEGIN_TIMESTAMP = 1735689600L;
/**
* 序列号的位数
*/
private static final int COUNT_BIT = 32;
private StringRedisTemplate stringRedisTemplate;
public RedisIdWorker(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
//keyPrefix--->前缀,区分不同的业务,不同的自增长
public long nextId(String keyPrefix) {
//1.生成时间戳(当前时间-开始时间 的秒数)
LocalDateTime now = LocalDateTime.now();
//当前时间转换成秒数
long nowSecond = now.toEpochSecond(ZoneOffset.UTC);
long timeStamp = nowSecond - BEGIN_TIMESTAMP;
//2.生成序列号
//2.1获取当前日期,精准到天(一天一个key)
String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
//2.2自增长
long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + date);
//3.拼接并返回
//时间戳左移32位
return timeStamp << COUNT_BIT | count;
}
public static void main(String[] args) {
//time对应的秒数
LocalDateTime time = LocalDateTime.of(2025, 1, 1, 0, 0);
long seconds = time.toEpochSecond(ZoneOffset.UTC);
System.out.println(seconds);
}
}
2、优惠券秒杀下单
VoucherOrderController
@RestController
@RequestMapping("/voucher-order")
public class VoucherOrderController {
@Resource
private IVoucherOrderService iVoucherOrderService;
@PostMapping("seckill/{id}")
public Result seckillVoucher(@PathVariable("id") Long voucherId) {
return iVoucherOrderService.secKillVoucher(voucherId);
}
}
VoucherOrderServiceImpl
@Transactional
public Result secKillVoucher(Long voucherId) {
//1.查询优惠券
SeckillVoucher voucher = iSeckillVoucherService.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.返回订单id
return Result.ok(orderId);
}
乐观锁解决超卖问题
VoucherOrderServiceImpl
@Transactional
public Result secKillVoucher(Long voucherId) {
//1.查询优惠券
SeckillVoucher voucher = iSeckillVoucherService.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")//set stock = stock - 1;
.eq("voucher_id", voucherId).gt("stock", 0)//where id = ? and stock > 0
.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.返回订单id
return Result.ok(orderId);
}
一人一单
VoucherOrderServiceImpl
public Result secKillVoucher(Long voucherId) {
//1.查询优惠券
SeckillVoucher voucher = iSeckillVoucherService.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("库存不足!");
}
//userId.toString(),toString的底层实现new String,不能保证同一个id每次来都是同一个对象(即同一把锁),需要Intern方法
Long userId = UserHolder.getUser().getId();
//锁加在用户上,可能出现用户提交后释放锁,但是数据尚未写入数据库而出现并发安全问题。需要把锁加在函数上。
//事务失效问题:此处调用createVoucherOrder()方法的是VoucherOrderServiceImpl对象,但是事务起作用应该是spring管理的VoucherOrderServiceImpl代理对象
synchronized(userId.toString().intern()) {
//拿到当前对象的代理对象 ps:aspectjweaver依赖 启动类注解@EnableAspectJAutoProxy(exposeProxy = true)
IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
return proxy.createVoucherOrder(voucherId);
}
}
@Transactional
//synchronized加在方法上,所有用户共用一把锁,串行执行,性能差。把锁加在用户上。
public Result createVoucherOrder(Long voucherId) {
//5.一人一单
//5.1查询订单
int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
//5.2判断是否存在
if(count > 0) {
//用户已经购买过了
return Result.fail("用户已经购买过一次!");
}
//6.扣减库存
//乐观锁:判断操作前后库存是否相同
boolean success = seckillVoucherService.update()
.setSql("stock = stock - 1")//set stock = stock - 1;
.eq("voucher_id", voucherId).gt("stock", 0)//where id = ? and stock > 0
.update();
if(!success) {
//扣减失败
return Result.fail("库存不足!");
}
//7.创建订单
VoucherOrder voucherOrder = new VoucherOrder();
//7.1订单id
long orderId = redisIdWorker.nextId("order");
voucherOrder.setId(orderId);
//7.2用户id
Long userId = UserHolder.getUser().getId();
voucherOrder.setUserId(userId);
//7.3代金券id
voucherOrder.setVoucherId(voucherId);
save(voucherOrder);
//8.返回订单id
return Result.ok(orderId);
}
3、分布式锁
当存在多个JVM时会存在多个锁监视器
必须使用一个多个JVM都可以看到的监视器
第一版
utils/ILock
package com.hmdp.utils;
public interface ILock {
/**
* 尝试获取锁
* @param timeoutSec 锁持有的超时时间,过期后自动释放
* @return true表示获取锁成功,false表示获取失败
*/
boolean tryLock(long timeoutSec);
/**
* 释放锁
*/
void unlock();
}
utils/SimpleRedisLock
public class SimpleRedisLock implements ILock {
//一个业务一个锁名
private String name;
private StringRedisTemplate stringRedisTemplate;
private static final String KEY_PREFIX = "lock:";
public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
this.name = name;
this.stringRedisTemplate = stringRedisTemplate;
}
@Override
public boolean tryLock(long timeoutSec) {
//获取线程标识
long threadId = Thread.currentThread().getId();
//获取锁
Boolean success = stringRedisTemplate.opsForValue()
.setIfAbsent(KEY_PREFIX + name, threadId + "", timeoutSec, TimeUnit.SECONDS);
return Boolean.TRUE.equals(success);
}
@Override
public void unlock() {
stringRedisTemplate.delete(KEY_PREFIX + name);
}
}
VoucherOrderServiceImpl
public Result secKillVoucher(Long voucherId) {
//1.查询优惠券
SeckillVoucher voucher = iSeckillVoucherService.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("库存不足!");
}
//userId.toString(),toString的底层实现new String,不能保证同一个id每次来都是同一个对象(即同一把锁),需要Intern方法
Long userId = UserHolder.getUser().getId();
//创建锁对象
SimpleRedisLock lock = new SimpleRedisLock("order:" + userId, stringRedisTemplate);
//获取锁
boolean isLock = lock.tryLock(1200);
//判断是否获取锁成功
if(isLock) {
// 获取锁失败,返回错误或重试
return Result.fail("不允许重复下单!");
}
try {
//获取代理对象(事务)
IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
return proxy.createVoucherOrder(voucherId);
} finally {
lock.unlock();
}
}
存在问题:业务阻塞,导致锁提前释放;线程1、2、3是同一个用户发出的请求
第二版(解决误删问题)
获取锁时存入线程标识,释放锁时判断锁标识是否是自己的。
utils/SimpleRedisLock
public class SimpleRedisLock implements ILock {
//一个业务一个锁名
private String name;
private StringRedisTemplate stringRedisTemplate;
private static final String KEY_PREFIX = "lock:";
private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";
public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
this.name = name;
this.stringRedisTemplate = stringRedisTemplate;
}
@Override
public boolean tryLock(long timeoutSec) {
//获取线程标识
String threadId = ID_PREFIX + Thread.currentThread().getId();
//获取锁
Boolean success = stringRedisTemplate.opsForValue()
.setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);
return Boolean.TRUE.equals(success);
}
@Override
public void unlock() {
//获取线程标识
String threadId = ID_PREFIX + Thread.currentThread().getId();
//获取锁中的标识
String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
//判断标识是否一致
if(threadId.equals(id)) {
//释放锁
stringRedisTemplate.delete(KEY_PREFIX + name);
}
}
}
VoucherOrderServiceImpl
public Result secKillVoucher(Long voucherId) {
//1.查询优惠券
SeckillVoucher voucher = iSeckillVoucherService.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("库存不足!");
}
//userId.toString(),toString的底层实现new String,不能保证同一个id每次来都是同一个对象(即同一把锁),需要Intern方法
Long userId = UserHolder.getUser().getId();
//创建锁对象
SimpleRedisLock lock = new SimpleRedisLock("order:" + userId, stringRedisTemplate);
//获取锁
boolean isLock = lock.tryLock(1200);
//判断是否获取锁成功
if(isLock) {
// 获取锁失败,返回错误或重试
return Result.fail("不允许重复下单!");
}
try {
//获取代理对象(事务)
IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
return proxy.createVoucherOrder(voucherId);
} finally {
lock.unlock();
}
}
存在问题:在判断完锁标识之后发生阻塞,误删。必须确保判断锁标识和释放锁两个动作的原子性
第三版
unlock.lua
--获取锁中的线程标识 get key
--比较线程标识与锁中的标识是否一致
if(redis.call('get', KEYS[1]) == ARGV[1]) then
--释放锁 del key
return redis.call('del', KEYS[1])
end
return 0
utils/SimpleRedisLock
//静态常量和静态代码块,类一加载就初始化
private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
static {
UNLOCK_SCRIPT = new DefaultRedisScript<>();
UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
UNLOCK_SCRIPT.setResultType(Long.class)
}
public class SimpleRedisLock implements ILock {
//一个业务一个锁名
private String name;
private StringRedisTemplate stringRedisTemplate;
private static final String KEY_PREFIX = "lock:";
private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";
public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
this.name = name;
this.stringRedisTemplate = stringRedisTemplate;
}
@Override
public boolean tryLock(long timeoutSec) {
//获取线程标识
String threadId = ID_PREFIX + Thread.currentThread().getId();
//获取锁
Boolean success = stringRedisTemplate.opsForValue()
.setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);
return Boolean.TRUE.equals(success);
}
@Override
public void unlock() {
//调用lua脚本
stringRedisTemplate.execute(
UNLOCK_SCRIPT,
Collections.singletonList(KEY_PREFIX + name),
ID_PREFIX + Thread.currentThread().getId());
}
}
Redisson
config/RedissonConfig
@Configuration
public class RedissonConfig {
@Bean
public RedissonClient redissonClient() {
//配置
Config config = new Config();
config.useSingleServer().setAddress("redis://10.8.11.69:6379").setPassword("123456");
//创建RedissonClient对象
return Redisson.create(config);
}
}
VoucherOrderServiceImpl
//注入RedissonClient对象
@Resource
private RedissonClient redissonClient;
public Result secKillVoucher(Long voucherId) {
//1.查询优惠券
SeckillVoucher voucher = iSeckillVoucherService.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("库存不足!");
}
Long userId = UserHolder.getUser().getId();
//创建锁对象
//SimpleRedisLock lock = new SimpleRedisLock("order:" + userId, stringRedisTemplate);
RLock lock = redissonClient.getLock("lock:order:" + userId);
//获取锁
boolean isLock = lock.tryLock();
if (!isLock) {
//获取锁失败,返回错误或重试
return Result.fail("不允许重复下单!");
}
try {
//获取代理对象(事务)
IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
return proxy.createVoucherOrder(voucherId);
}finally {
lock.unlock();
}
}
秒杀优化
效率低!
需求一:保存优惠券信息到Redis中
VoucherServiceImpl
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
@Transactional
public void addSeckillVoucher(Voucher voucher) {
// 保存优惠券
save(voucher);
// 保存秒杀信息到数据库
SeckillVoucher seckillVoucher = new SeckillVoucher();
seckillVoucher.setVoucherId(voucher.getId());
seckillVoucher.setStock(voucher.getStock());
seckillVoucher.setBeginTime(voucher.getBeginTime());
seckillVoucher.setEndTime(voucher.getEndTime());
seckillVoucherService.save(seckillVoucher);
//保存秒杀库存到Redis
stringRedisTemplate.opsForValue().set(SECKILL_STOCK_KEY + voucher.getId(), voucher.getStock().toString());
}
需求二:一人一单秒杀资格判断(基于lua脚本)
seckill.lua
--1.参数列表
--1.1优惠券id
local voucherId = ARGV[1]
--1.2用户id
local userId = ARGV[2]
--2.数据key
--2.1库存key
local stockKey = "seckill:stock" .. voucherId
--2.2订单key
local orderKey = "seckill:order" .. voucherId
--脚本业务
--3.1判断库存是否充足
if(tomember(redis.call('get', stockKey)) <= 0) then
--库存不足,返回1
return 1
end
--3.2判断用户是否下单 SISMEMBER orderKey userId
if(redis.call('sismember', orderKey, userId) == 1) then
--存在,说明是重复下单,返回2
return 2
end
--3.3扣库存 incrby stockKey - 1
reids.call('incryby', stockKey, -1)
--3.4下单(保存用户)sadd orderKey userId
redis.call('sadd', orderKey, userId)
return 0
VoucherOrderServiceImpl
private static final DefaultRedisScript<Long> SECKILL_SCRIPT;
static {
SECKILL_SCRIPT = new DefaultRedisScript<>();
SECKILL_SCRIPT.setLocation(new ClassPathResource("seckill.lua"));
SECKILL_SCRIPT.setResultType(Long.class);
}
@Override
public Result secKillVoucher(Long voucherId) {
//获取用户
Long userId = UserHolder.getUser().getId();
//1.执行lua脚本
Long result = stringRedisTemplate.execute(
SECKILL_SCRIPT,
Collections.emptyList(),
voucherId.toString(), userId.toString()
);
//2.判断结果是否为0
int r = result.intValue();
if(r != 0){
//2.1 不为0,没有购买资格
return Result.fail(r == 1 ? "库存不足" : " 不能重复下单");
}
//2.2 为0,有购买资格,把下单信息保存到阻塞队列
long orderId = redisIdWorker.nextId("order");
//TODO 保存阻塞队列
//3.返回订单id
return Result.ok(orderId);
}
需求三、四:
private static final DefaultRedisScript<Long> SECKILL_SCRIPT;
static {
SECKILL_SCRIPT = new DefaultRedisScript<>();
SECKILL_SCRIPT.setLocation(new ClassPathResource("seckill.lua"));
SECKILL_SCRIPT.setResultType(Long.class);
}
//创建阻塞队列
private BlockingQueue<VoucherOrder> orderTasks = new ArrayBlockingQueue<>(1024 * 1024);
//创建线程池
private static final ExecutorService SECKILL_ORDER_EXCUTOR = Executors.newSingleThreadExecutor();
//注解@PostConstruct:在当前类初始化完毕后执行
@PostConstruct
private void init() {
SECKILL_ORDER_EXCUTOR.submit(new VoucherOrderHandler());
}
//线程任务
private class VoucherOrderHandler implements Runnable{
@Override
public void run() {
while(true) {
try {
//1.获取队列中的订单信息
VoucherOrder voucherOrder = orderTasks.take();
//2.创建订单
handleVoucherOrder(voucherOrder);
} catch (Exception e) {
log.error("处理订单异常", e);
}
}
}
}
private void handleVoucherOrder(VoucherOrder voucherOrder) {
//1.获取用户
Long userId = voucherOrder.getUserId();
//2.创建锁对象
RLock lock = redissonClient.getLock("lock:order:" + userId);
//获取锁
boolean isLock = lock.tryLock();
if (!isLock) {
//获取锁失败,返回错误或重试
log.error("不允许重复下单");
return;
}
try {
proxy.createVoucherOrder(voucherOrder);
}finally {
lock.unlock();
}
}
private IVoucherOrderService proxy;
@Override
public Result secKillVoucher(Long voucherId) {
//获取用户
Long userId = UserHolder.getUser().getId();
//1.执行lua脚本
Long result = stringRedisTemplate.execute(
SECKILL_SCRIPT,
Collections.emptyList(),
voucherId.toString(), userId.toString()
);
//2.判断结果是否为0
int r = result.intValue();
if(r != 0){
//2.1 不为0,没有购买资格
return Result.fail(r == 1 ? "库存不足" : " 不能重复下单");
}
//2.2 为0,有购买资格,把下单信息保存到阻塞队列
VoucherOrder voucherOrder = new VoucherOrder();
//2.3订单id
long orderId = redisIdWorker.nextId("order");
voucherOrder.setId(orderId);
//2.4用户id
voucherOrder.setUserId(userId);
//2.5代金券id
voucherOrder.setVoucherId(voucherId);
//2.6创建阻塞队列
orderTasks.add(voucherOrder);
//3获取代理对象
proxy = (IVoucherOrderService) AopContext.currentProxy();
//4.返回订单id
return Result.ok(orderId);
}
@Transactional
public void createVoucherOrder(VoucherOrder voucherOrder) {
//5.一人一单
Long userId = voucherOrder.getUserId();
//5.1查询订单
int count = query().eq("user_id", userId).eq("voucher_id", voucherOrder.getVoucherId()).count();
//5.2判断是否存在
if (count > 0) {
//用户已经购买过
log.error("用户已经购买过一次!");
return;
}
//6.扣减库存
boolean success = iSeckillVoucherService.update()
.setSql("stock = stock - 1") //set stock = stock - 1
.eq("voucher_id", voucherOrder.getVoucherId()).gt("stock", 0) // where id = ? and stock > 0
.update();
if (!success) {
log.error("库存不足!");
return;
}
//7.创建订单
save(voucherOrder);
}
JVM内存限制,高并发情况下,大量订单出现可能会超出JVM的上限。
数据存储在内存中,出现宕机或者重启,阻塞队列中的数据都会丢失。
Redis消息队列
1、基于List结构模拟消息队列
2、基于PubSub(发布订阅)
3、基于Stream的消息队列
最后一点没看下去。。。。天呐这一部分好多,还没有消化。继续加油!!