12306库存校验与扣减实现方案

在高并发的电商或票务系统中,库存校验与扣减是关键业务逻辑之一,尤其在类似于 12306 这样的抢票场景中,如何在高并发条件下保证库存的正确性、避免超卖和确保数据一致性,是非常具有挑战性的。下面我将详细描述库存校验与扣减的逻辑,包括如何在高并发场景中避免数据不一致、如何通过分布式锁来控制并发、以及如何设计有效的系统架构。

1. 库存扣减的基本流程

库存的扣减一般包括两个步骤:库存校验库存扣减,在高并发场景下,这两个步骤需要保持原子性,以防止出现超卖、库存不一致等问题。

流程图:
  1. 用户请求购买某种商品或车票。
  2. 系统检查该商品或车票的库存是否充足。
  3. 如果库存充足,系统扣减库存,并完成购买。
  4. 如果库存不足,返回失败。

但在高并发的情况下,直接使用数据库的 事务 可能会遇到性能瓶颈,因此通常会结合缓存、队列等机制来优化性能。

2. 高并发下的库存扣减问题

在高并发场景下,多个用户可能同时发起相同商品或车票的购买请求。若没有适当的并发控制措施,就可能出现以下几种问题:

  • 超卖:多个请求在判断库存时,都认为库存足够,于是都成功扣减库存,导致超卖。
  • 数据不一致:当多个请求并发操作库存时,可能导致库存信息的不一致或丢失。
  • 性能瓶颈:高并发请求直接访问数据库可能会引发性能瓶颈,导致系统崩溃。

因此,我们需要设计合适的 库存校验扣减 机制,确保在高并发情况下既能保证库存的正确性,又能保证系统的性能和可靠性。

3. 库存校验与扣减的解决方案

3.1 基于 Redis 实现库存的乐观锁

Redis 提供的 乐观锁(Optimistic Locking)机制适合高并发的库存扣减。使用 Redis 中的 watchmulti/exec 事务来实现。

乐观锁的核心思想是:在操作之前,不对库存进行加锁,而是进行校验并尝试扣减库存,如果发现库存已被修改,则操作失败。

实现步骤:
  1. 检查库存:首先检查 Redis 缓存中的库存信息。
  2. 乐观锁控制:使用 WATCH 监视 Redis 中的库存信息(比如车票的余票数)。如果库存信息没有发生变化,则执行扣减操作。
  3. 扣减库存:使用 MULTI 和 EXEC 命令将扣减库存操作提交给 Redis 执行。如果在执行时发现库存已被修改,操作会失败,必须重新进行库存校验和扣减。
代码示例(使用 Redis Watch):
// 获取 Redis 连接
Jedis jedis = jedisPool.getResource();

// 监视库存
jedis.watch("train:12306:train1234:tickets");

// 获取当前库存
int tickets = Integer.parseInt(jedis.get("train:12306:train1234:tickets"));

// 校验库存是否足够
if (tickets > 0) {
    // 使用事务扣减库存
    Transaction tx = jedis.multi();
    tx.decrBy("train:12306:train1234:tickets", 1);  // 扣减库存
    List<Object> result = tx.exec();  // 提交事务

    if (result == null) {
        // 库存已被其他操作修改,重新进行抢票操作
        System.out.println("库存不足,重试!");
    } else {
        System.out.println("抢票成功!");
    }
} else {
    System.out.println("库存不足!");
}

注意:这种方式的关键在于 WATCH 监视的过程。如果在操作过程中库存被其他用户修改了,当前的事务会返回 null,需要重新尝试。乐观锁可以有效减少锁的使用,但需要在 exec 时进行重试机制。

3.2 基于 Redis 实现库存的悲观锁

悲观锁(Pessimistic Locking)是一种在操作前先对数据加锁的机制,适合高并发且要求严格一致性的场景。

在 Redis 中,SETNX(set if not exists)命令可以实现类似悲观锁的效果。利用这个命令可以确保同一时刻只有一个请求能对库存进行操作。

实现步骤:
  1. 获取锁:在操作库存之前,先尝试通过 Redis 的 SETNX 命令加锁,只有获取到锁的请求才能继续扣减库存。
  2. 扣减库存:如果获取到锁,扣减库存并处理业务逻辑。操作完成后释放锁。
  3. 释放锁:操作完成后,释放锁。
代码示例(使用 Redis SETNX 实现悲观锁):
String lockKey = "lock:train:12306:train1234";
String lockValue = UUID.randomUUID().toString();

// 尝试加锁
boolean locked = jedis.setnx(lockKey, lockValue) == 1;
if (locked) {
    try {
        // 扣减库存操作
        int tickets = Integer.parseInt(jedis.get("train:12306:train1234:tickets"));
        if (tickets > 0) {
            jedis.decrBy("train:12306:train1234:tickets", 1);  // 扣减库存
            System.out.println("抢票成功!");
        } else {
            System.out.println("库存不足!");
        }
    } finally {
        // 释放锁
        if (jedis.get(lockKey).equals(lockValue)) {
            jedis.del(lockKey);  // 只有持有锁的线程才能删除锁
        }
    }
} else {
    System.out.println("获取锁失败,稍后再试!");
}

注意:悲观锁的缺点是可能导致死锁,特别是在系统出现异常或线程未正常释放锁时。为防止死锁,可以设置锁的超时机制,确保锁在一定时间内自动释放。

3.3 队列化请求处理

对于极高并发的场景,队列化处理也是一种常见的解决方案。通过 Redis 或消息队列(如 Kafka、RabbitMQ)将用户请求进行排队,逐个处理请求。这样可以将多个并发请求转化为串行化的操作,避免直接在数据库或 Redis 上进行高并发操作。

实现步骤:
  1. 请求排队:将所有抢票请求按照到达顺序放入 Redis 的队列中(可以使用 List 或 Sorted Set)。
  2. 逐个处理:后端系统按顺序处理队列中的请求,每次从队列中取出一个请求,进行库存校验和扣减操作。
代码示例:
// 用户请求加入队列
redisTemplate.opsForList().leftPush("queue:train1234", userId);

// 从队列中逐个处理
String userId = redisTemplate.opsForList().rightPop("queue:train1234");
int tickets = Integer.parseInt(redisTemplate.opsForValue().get("train:12306:train1234:tickets"));
if (tickets > 0) {
    redisTemplate.opsForValue().decrement("train:12306:train1234:tickets", 1);
    System.out.println("抢票成功!");
} else {
    System.out.println("库存不足!");
}

4. 最终一致性方案:异步处理

在一些场景下,可能不需要强一致性,而是可以容忍一些短暂的错误或延迟。此时,可以使用异步处理方式,来将库存扣减的操作放入消息队列中异步执行,最终通过定时任务来同步库存。

这种方式可以减轻系统的实时压力,但需要在后台处理完成后保证最终一致性。

总结:

在高并发的库存校验和扣减场景下,常见的解决方案包括:

  1. 乐观锁:利用 Redis 的 WATCH 和事务机制实现。适合高并发场景,可以减少锁的使用,但需要处理重试逻辑。
  2. 悲观锁:通过 Redis 的 SETNX 实现锁的机制,适合强一致性要求的场景,但可能会遇到死锁问题,需要设置超时机制。
  3. 请求排队:将并发请求排队处理,避免并发冲突,适合流量高峰时使用。
  4. 异步处理:通过消息队列等方式异步处理库存扣减,适合延迟一致性要求较低的场景。

5.lua脚本实现

使用 Lua 脚本 是解决 Redis 中高并发库存查询与扣减问题的另一个有效方案。通过 Lua 脚本的原子性,能够确保在执行查询和库存扣减操作时,不会被其他并发请求打断,从而避免了竞争条件和超卖的问题。

Lua 脚本的优势

  • 原子性:Redis 中的 Lua 脚本是原子执行的,即脚本中的所有操作要么全部成功,要么全部失败,在执行过程中不会被其他命令中断。
  • 性能:Lua 脚本执行是由 Redis 服务器直接处理的,不需要客户端和服务器之间进行多次交互,因此比单独执行多个 Redis 命令更高效。
  • 减少网络延迟:通过将多个操作打包成一个脚本执行,减少了多次请求和响应的网络延迟。

实现思路

通过 Lua 脚本可以同时完成以下操作:

  1. 查询库存是否足够。
  2. 如果库存足够,扣减库存。
  3. 如果库存不足,返回失败信息。

Lua 脚本示例

下面是一个 Lua 脚本示例,该脚本会首先检查 Redis 中的库存,如果库存大于零,就扣减库存;如果库存不足,返回 false 或者 库存不足 的错误信息。

-- Lua 脚本:查询库存并扣减
-- KEYS[1]:库存的 Redis 键
-- ARGV[1]:扣减的数量

local stock = tonumber(redis.call('GET', KEYS[1]))  -- 获取库存数量
local decrement = tonumber(ARGV[1])  -- 扣减数量

if stock >= decrement then
    redis.call('DECRBY', KEYS[1], decrement)  -- 扣减库存
    return true  -- 返回成功
else
    return false  -- 返回失败(库存不足)
end

解释

  • KEYS[1]:代表库存数据在 Redis 中的键名(例如:"train:12306:train1234:tickets")。
  • ARGV[1]:代表扣减的数量(通常为 1,即每次扣减一个票)。
  • redis.call('GET', KEYS[1]):获取当前库存数量。
  • redis.call('DECRBY', KEYS[1], decrement):扣减库存。
  • if stock >= decrement:检查库存是否足够,如果库存大于或等于扣减数量,则继续扣减,否则返回 false,表示库存不足。

如何在 Redis 客户端中调用这个 Lua 脚本

在客户端调用这个 Lua 脚本时,我们需要使用 Redis 提供的 EVAL 命令来执行脚本。

假设我们使用的是 Java 客户端(Jedis)来调用该脚本:

// 使用 Jedis 执行 Lua 脚本
Jedis jedis = jedisPool.getResource();

// 定义 Lua 脚本
String luaScript = 
    "local stock = tonumber(redis.call('GET', KEYS[1])) " +
    "local decrement = tonumber(ARGV[1]) " +
    "if stock >= decrement then " +
    "    redis.call('DECRBY', KEYS[1], decrement) " +
    "    return true " +
    "else " +
    "    return false " +
    "end";

// 库存键名
String stockKey = "train:12306:train1234:tickets";
// 扣减数量
int decrement = 1;

// 执行 Lua 脚本
Object result = jedis.eval(luaScript, 1, stockKey, String.valueOf(decrement));

// 根据脚本返回结果判断是否扣减成功
if (Boolean.TRUE.equals(result)) {
    System.out.println("抢票成功!");
} else {
    System.out.println("库存不足!");
}

解释

  1. jedis.eval(luaScript, 1, stockKey, String.valueOf(decrement)):执行 Lua 脚本,其中:

    • luaScript:包含 Lua 脚本的字符串。
    • 1:表示 Lua 脚本中使用了 1 个键(KEYS[1])。
    • stockKey:库存的 Redis 键名。
    • String.valueOf(decrement):扣减的数量(如 1)。
  2. 返回值

    • 如果脚本返回 true,则表示扣减库存成功,打印 "抢票成功!"
    • 如果脚本返回 false,则表示库存不足,打印 "库存不足!"

多次调用 Lua 脚本的优化

如果你有多次并发请求需要处理,可以将扣减库存的操作封装到 Lua 脚本中,避免在客户端和 Redis 之间进行多次交互。例如,假设你需要同时扣减多个商品或多个车票的库存,可以在 Lua 脚本中循环处理多个商品或票的扣减。

Lua 脚本的更多扩展

  1. 增加库存操作: 如果你需要在库存不足时执行库存补充操作,可以在 Lua 脚本中增加对库存的 INCRBY 操作,动态调整库存。

  2. 过期时间控制: 可以在脚本中增加对库存键的过期时间控制,确保库存的键在一段时间后自动失效,避免长时间存在的过期数据。

  3. 分布式锁: 可以结合 Lua 脚本和分布式锁的策略,比如使用 Redis 的 SETNX 来保证只有一个请求能够进入库存操作,避免死锁。

总结

通过 Lua 脚本实现库存查询与扣减的方案,能够保证操作的原子性和高效性,尤其适用于高并发场景。借助 Redis 脚本,能够将查询库存和扣减库存等操作在单次网络请求中完成,大大减少了潜在的竞争条件,避免了超卖的风险。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值