Redis实现优惠券秒杀功能

全局唯一ID

订单表使用数据库自增ID的缺点:
1、ID规律性太明显。
2、受单表数据量限制,需要分库分表时数据库自增ID失效。
全局ID生成器:在分布式系统中生成全局唯一ID的工具,需要满足以下特性:
1、唯一性
2、高可用
3、高性能
4、递增性
5、安全性
例如Redis的string类型的incr命令。
但是为了ID安全性,不能直接使用Redis自增的数值,而是拼接其他信息。
例如,使用Long类型,占8个字节,64bit位,1bit符号位,前31bit使用时间戳,支持69年使用时间,后32位使用序列号,每秒产生2^32个不同ID。
基于Redis实现全局唯一ID例子:

@Component
public class RedisIdWorker {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    private static final long BEGIN_TIMESTAMP = 1704067200l;

    //序列号位数
    private static final int COUNT_BITS = 32;

    public long nextId(String keyPrefix) {
        //生成时间戳
        LocalDateTime now = LocalDateTime.now();
        long nowSeconds = now.toEpochSecond(ZoneOffset.UTC);
        long timestamp = nowSeconds - BEGIN_TIMESTAMP;
        //生成序列号,每天2^32个ID
        String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
        Long count = stringRedisTemplate.opsForValue().increment("incr:" + keyPrefix + ":" + date);
        return timestamp << COUNT_BITS | count;
    }

    public static void main(String[] args) {
        LocalDateTime time = LocalDateTime.of(2024, 1, 1, 0, 0, 0);
        long seconds = time.toEpochSecond(ZoneOffset.UTC);
        System.out.println(seconds);
    }
}

实现优惠券秒杀下单

优惠券分为平价券和特价券,平价券可以任意购买,特价券需要秒杀抢购。这里针对特价券而言。
tb_voucher包含平价券和特价券,tb_seckill_voucher只描述特价券,包含库存,生效时间,失效时间。
秒杀下单需要判断:
1、秒杀是否开始或者是否已经结束
2、库存是否足够,足够才扣减库存并且创建订单
功能实现后,高并发场景下出现了超卖,剩余库存是负数!原因是多个线程先查询库存,后续进行扣减,整个操作不是原子性的。
解决:
1、加悲观锁
比如synchronized,Lock,或者分布式锁。缺点是开销较大。
2、加乐观锁:
版本号法,每次修改时判断和刚刚查询的版本号是否一致,修改后版本号+1。优点是性能好,缺点是有可能出现少买。
判断库存大于0即可,判断条件为where id = ? and stock > 0; 完美解决。

实现一人一单

同一优惠券,一个用户只能下一单。
先判断订单表中是否存在用户id和优惠券id的记录,如果记录数大于0,则返回失败。
缺点,用户还是能下多单,因为多个线程判断成功后可以继续购买,和后续扣减库存,生成订单不是原子性操作。
这只能使用悲观锁,因为乐观锁只能适用修改,但是这里是新增操作。
这里对用户id加锁,但是为了防止对象变化,使用字符串,并且调用intern方法在字符串常量池中取对象。
锁的范围应该是整个事务提交之后,防止事务还没提交,但是锁被释放其他线程又加锁下单。
事务失效,使用this.createVoucherOrder(),需要拿到当前对象的代理对象。
需要添加依赖AspectJ,启动类添加注解暴露代理对象。

分布式锁

之前的锁都是本地锁,考虑分布式情况下,多个机器的锁不共享,需要使用分布式锁。
分布式锁参考博客
为什么Redisson没有锁自动续期?
这里碰到一个问题,观察分布式锁的ttl时发现没有自动续期,而是过期删除了,这是因为使用Debug模式,因此Redisson没有自动续期。

Redis秒杀优化

原本流程:
1、查询优惠券
2、判断秒杀库存
3、查询订单
4、校验一人一单
5、减库存
6、创建订单
优化:
1、将秒杀库存和订单信息缓存到Redis中
2、保存优惠券id,用户id,订单id到阻塞队列,返回订单id
3、异步读取阻塞队列信息,完成下单
库存使用string类型,订单信息使用set类型,并且使用lua脚本保证原子性。
基于阻塞队列的异步秒杀存在哪些问题?
1、使用的是JVM的阻塞队列,内存有限。
2、如果宕机,内存数据丢失。
解决:使用消息队列。

local voucherId = ARGV[1]
local userId = ARGV[2]
local stockKey = 'seckill:stock:' .. voucherId
local orderKey = 'seckill:order:' .. voucherId

if (tonumber(redis.call('get', stockKey)) <= 0) then
    --库存不足,返回1
    return 1
end

if (redis.call('sismember', orderKey, userId) == 1) then
    --重复下单,返回2
    return 2
end

redis.call('incrby', stockKey, -1)
redis.call('sadd', orderKey, userId)
--成功,返回0
return 0
@Slf4j
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {

    @Autowired
    private ISeckillVoucherService seckillVoucherService;

    @Autowired
    private RedisIdWorker redisIdWorker;

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Autowired
    private RedissonClient redissonClient;

    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_EXECUTOR = Executors.newSingleThreadExecutor();

    @PostConstruct
    private void init() {
        SECKILL_ORDER_EXECUTOR.execute(new VoucherOrderHandler());
    }

    private class VoucherOrderHandler implements Runnable {

        @Override
        public void run() {
            while (true) {
                try {
                    VoucherOrder voucherOrder = orderTasks.take();
                    handlerVoucherOrder(voucherOrder);
                } catch (Exception e) {
                    log.error("处理订单异常", e);
                }
            }
        }
    }

    private void handlerVoucherOrder(VoucherOrder voucherOrder) {
        Long userId = voucherOrder.getUserId();
        RLock lock = redissonClient.getLock("lock:order:" + userId);
        boolean isLock = lock.tryLock();
        if (!isLock) {
            log.error("不允许重复下单");
            return;
        }
        try {
            proxy.createVoucherOrder(voucherOrder);
        } finally {
            lock.unlock();
        }
    }

    //由于有扣减库存和写入订单,因此使用事务
    @Transactional
    public void createVoucherOrder(VoucherOrder voucherOrder) {
        //一人一单
        Long userId = voucherOrder.getUserId();
        Integer count = query().eq("user_id", userId).eq("voucher_id", voucherOrder.getVoucherId()).count();
        if (count > 0) {
            log.error("用户已经购买过一次!");
            return ;
        }
        //扣减库存
        boolean success = seckillVoucherService.update()
                .setSql("stock = stock - 1").eq("voucher_id", voucherOrder.getVoucherId())
                .ge("stock", 1).update();
        if (!success) {
            log.error("库存不足!");
            return ;
        }
        //写入订单
        save(voucherOrder);
    }

    private IVoucherOrderService proxy;

    @Override
    public Result seckillVoucher(Long voucherId) {
        Long userId = UserHolder.getUser().getId();
        Long result = stringRedisTemplate.execute(
                SECKILL_SCRIPT,
                Collections.emptyList(),
                voucherId.toString(), userId.toString()
        );
        int r = result.intValue();
        if (r != 0) {
            return Result.fail(r == 1 ? "库存不足!" : "不能重复下单!");
        }
        //保存信息到阻塞队列
        VoucherOrder voucherOrder = new VoucherOrder();
        //使用全局唯一ID生成订单ID,而不是使用数据库自增ID
        long orderId = redisIdWorker.nextId("order");
        voucherOrder.setId(orderId);
        voucherOrder.setUserId(userId);
        voucherOrder.setVoucherId(voucherId);
        orderTasks.add(voucherOrder);
        proxy = (IVoucherOrderService) AopContext.currentProxy();
        return Result.ok(0);
    }
  }
}

Redis消息队列

1、基于List实现消息队列。
使用lpush结合rpop;或者rpush结合lpop,为了实现阻塞效果,取出时使用blopo和brpop。
2、基于PubSub实现消息队列。
在Redis2.0引入的消息传递模型。消费者可以订阅多个channel。
subcribe channel
publish channel msg
psubscribe pattern
3、基于stream实现消息队列。
在Redis5.0引入的新数据类型,可以实现功能完善的消息队列,并且可以持久化。
xadd key id field value,发送消息
xread count streams key id
缺点:ID为$表示读取最新消息,但是如果多条消息到达,只会读取最新的那条。
消费者组。

使用stream实现消息队列,实现异步秒杀下单
1、创建stream类型的消息队列,名为stream.orders
2、修改lua脚本,向stream.orders中添加消息,包含优惠券id,用户id,订单id
3、项目启动时,开启线程任务,尝试获取stream.orders小夏,完成下单
创建消息队列命令:

XGROUP CREATE stream.orders g1 0 MKSTREAM

lua脚本

local voucherId = ARGV[1]
local userId = ARGV[2]
local orderId = ARGV[3]

local stockKey = 'seckill:stock:' .. voucherId
local orderKey = 'seckill:order:' .. voucherId

if (tonumber(redis.call('get', stockKey)) <= 0) then
    --库存不足,返回1
    return 1
end

if (redis.call('sismember', orderKey, userId) == 1) then
    --重复下单,返回2
    return 2
end

--扣库存
redis.call('incrby', stockKey, -1)
--下单
redis.call('sadd', orderKey, userId)

--发送到消息队列
redis.call("xadd", "stream.orders", "*", "userId", userId, "voucherId", voucherId, "id", orderId)

--成功,返回0
return 0
@Slf4j
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {

    @Autowired
    private ISeckillVoucherService seckillVoucherService;

    @Autowired
    private RedisIdWorker redisIdWorker;

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Autowired
    private RedissonClient redissonClient;

    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 static final ExecutorService SECKILL_ORDER_EXECUTOR = Executors.newSingleThreadExecutor();

    @PostConstruct
    private void init() {
        SECKILL_ORDER_EXECUTOR.execute(new VoucherOrderHandler());
    }

    private class VoucherOrderHandler implements Runnable {
        String queueName = "streams.order";
        @Override
        public void run() {
            while (true) {
                try {
                    List<MapRecord<String, Object, Object>> list = stringRedisTemplate.opsForStream().read(
                            Consumer.from("g1", "c1"),
                            StreamReadOptions.empty().count(1).block(Duration.ofSeconds(2)),
                            StreamOffset.create(queueName, ReadOffset.lastConsumed())
                    );
                    if (list == null || list.isEmpty()) {
                        continue;
                    }
                    MapRecord<String, Object, Object> record = list.get(0);
                    Map<Object, Object> value = record.getValue();
                    VoucherOrder voucherOrder = BeanUtil.fillBeanWithMap(value, new VoucherOrder(), true);
                    handlerVoucherOrder(voucherOrder);
                    stringRedisTemplate.opsForStream().acknowledge(queueName, "g1", record.getId());
                } catch (Exception e) {
                    log.error("处理订单异常", e);
                    handlePendingList();
                }
            }
        }

        private void handlePendingList() {
            while (true) {
                try {
                    List<MapRecord<String, Object, Object>> list = stringRedisTemplate.opsForStream().read(
                            Consumer.from("g1", "c1"),
                            StreamReadOptions.empty().count(1),
                            StreamOffset.create(queueName, ReadOffset.from("0"))
                    );
                    if (list == null || list.isEmpty()) {
                        break;
                    }
                    MapRecord<String, Object, Object> record = list.get(0);
                    Map<Object, Object> value = record.getValue();
                    VoucherOrder voucherOrder = BeanUtil.fillBeanWithMap(value, new VoucherOrder(), true);
                    handlerVoucherOrder(voucherOrder);
                    stringRedisTemplate.opsForStream().acknowledge(queueName, "g1", record.getId());
                } catch (Exception e) {
                    log.error("处理pending-list异常", e);
                    try {
                        Thread.sleep(50);
                    } catch (InterruptedException ex) {
                        ex.printStackTrace();
                    }
                }
            }
        }
    }

    // private BlockingQueue<VoucherOrder> orderTasks = new ArrayBlockingQueue<>(1024*1024);
    //
    // private class VoucherOrderHandler implements Runnable {
    //
    //     @Override
    //     public void run() {
    //         while (true) {
    //             try {
    //                 VoucherOrder voucherOrder = orderTasks.take();
    //                 handlerVoucherOrder(voucherOrder);
    //             } catch (Exception e) {
    //                 log.error("处理订单异常", e);
    //             }
    //         }
    //     }
    // }

    private void handlerVoucherOrder(VoucherOrder voucherOrder) {
        Long userId = voucherOrder.getUserId();
        RLock lock = redissonClient.getLock("lock:order:" + userId);
        boolean isLock = lock.tryLock();
        if (!isLock) {
            log.error("不允许重复下单");
            return;
        }
        try {
            proxy.createVoucherOrder(voucherOrder);
        } finally {
            lock.unlock();
        }
    }

    //由于有扣减库存和写入订单,因此使用事务
    @Transactional
    public void createVoucherOrder(VoucherOrder voucherOrder) {
        //一人一单
        Long userId = voucherOrder.getUserId();
        Integer count = query().eq("user_id", userId).eq("voucher_id", voucherOrder.getVoucherId()).count();
        if (count > 0) {
            log.error("用户已经购买过一次!");
            return ;
        }
        //扣减库存
        boolean success = seckillVoucherService.update()
                .setSql("stock = stock - 1").eq("voucher_id", voucherOrder.getVoucherId())
                .ge("stock", 1).update();
        if (!success) {
            log.error("库存不足!");
            return ;
        }
        //写入订单
        save(voucherOrder);
    }

    private IVoucherOrderService proxy;

    @Override
    public Result seckillVoucher(Long voucherId) {
        Long userId = UserHolder.getUser().getId();
        //使用全局唯一ID生成订单ID,而不是使用数据库自增ID
        long orderId = redisIdWorker.nextId("order");
        Long result = stringRedisTemplate.execute(
                SECKILL_SCRIPT,
                Collections.emptyList(),
                voucherId.toString(), userId.toString(), String.valueOf(orderId)
        );
        int r = result.intValue();
        if (r != 0) {
            return Result.fail(r == 1 ? "库存不足!" : "不能重复下单!");
        }
        proxy = (IVoucherOrderService) AopContext.currentProxy();
        return Result.ok(0);
    }
  }
}
  • 12
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值