大流量下库存扣减的数据库瓶颈:Redis分片缓存解决方案

大流量下库存扣减的数据库瓶颈:Redis分片缓存解决方案

引言

为什么库存扣减如此重要?

在电商、秒杀、抢购等互联网业务场景中,库存扣减是整个交易链路中最核心、最关键的环节之一。一个看似简单的"减库存"操作,背后却隐藏着巨大的技术挑战。

案例启示:

2018年某电商平台双11大促期间,一款爆款商品在开售30秒内,库存从10万件变成负数,导致超卖3万多笔订单。最终平台不得不赔偿违约金,品牌方声誉受损,用户信任度大幅下降。这个惨痛的教训告诉我们:在高并发场景下,如果库存扣减处理不当,后果不堪设想。

库存扣减的三大核心挑战:

  1. 高并发处理能力 - 秒杀场景可能在几秒内涌入数十万甚至上百万的请求,系统需要具备足够的吞吐能力
  2. 数据准确性保证 - 绝对不能出现超卖现象(卖出数量超过实际库存),这是业务红线
  3. 极致的用户体验 - 响应时间要足够短,避免用户等待焦虑,提升转化率

传统方案的困境

在最开始的电商系统中,我们通常会这样实现库存扣减:

sql 复制代码
-- 第一步:查询库存
SELECT stock FROM product WHERE id = 1;

-- 第二步:判断库存是否充足
-- 第三步:如果充足,更新库存
UPDATE product SET stock = stock - 1 WHERE id = 1;

这种方案在低并发场景下看似没问题,但一旦流量上去,各种问题就会暴露无遗:

问题一:并发冲突导致超卖

当多个请求同时执行时,由于没有加锁,可能会出现"读取-修改-写入"的竞态条件。例如两个请求同时读取到库存为10,都认为自己可以扣减1件,最终库存只减少了1,但卖出了2件。

问题二:数据库成为性能瓶颈

MySQL单表的QPS通常在500-1000左右,即使使用索引优化,面对万级以上的并发也会力不从心。数据库连接池会被耗尽,大量请求超时失败,用户看到"系统繁忙,请稍后重试"的提示,体验极差。

问题三:锁机制降低并发能力

为了解决超卖问题,我们可能会使用悲观锁(SELECT FOR UPDATE)或乐观锁(CAS)。这些方案虽然解决了超卖问题,但大大降低了并发能力,因为串行化了请求处理。

一、传统方案的深度分析

1.1 数据库库存扣减的本质

在深入讨论Redis方案之前,我们先深入理解传统数据库方案的本质问题和局限。

最原始的实现方式:

java 复制代码
// JDBC方式实现库存扣减
public boolean deductStock(Long productId, Integer quantity) {
    Connection conn = null;
    try {
        conn = dataSource.getConnection();
        conn.setAutoCommit(false);

        // 1. 查询库存
        PreparedStatement queryStmt = conn.prepareStatement(
            "SELECT stock FROM product WHERE id = ?"
        );
        queryStmt.setLong(1, productId);
        ResultSet rs = queryStmt.executeQuery();

        if (!rs.next()) {
            return false; // 商品不存在
        }

        int currentStock = rs.getInt("stock");

        // 2. 判断库存是否充足
        if (currentStock < quantity) {
            conn.rollback();
            return false; // 库存不足
        }

        // 3. 更新库存
        PreparedStatement updateStmt = conn.prepareStatement(
            "UPDATE product SET stock = ? WHERE id = ?"
        );
        updateStmt.setInt(1, currentStock - quantity);
        updateStmt.setLong(2, productId);
        updateStmt.executeUpdate();

        conn.commit();
        return true;

    } catch (Exception e) {
        if (conn != null) {
            conn.rollback();
        }
        throw new RuntimeException("扣减库存失败", e);
    } finally {
        if (conn != null) {
            conn.close();
        }
    }
}

这种方式的问题:

整个操作包含三个独立的数据库操作:SELECT、应用层判断、UPDATE。在高并发场景下,多个线程可能同时执行到SELECT阶段,读取到相同的库存值,然后都认为自己可以扣减,导致超卖。

1.2 并发冲突的详细分析

让我们通过一个详细的时序图来理解并发冲突是如何发生的:

ini 复制代码
时间线    请求A                        请求B                        数据库存量
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
T1       读取库存=10                  ────────────────────────    10
T2       ────────────────────────     读取库存=10                 10
T3       判断: 10 >= 1 ✓              ────────────────────────    10
T4       ────────────────────────     判断: 10 >= 1 ✓            10
T5       执行: UPDATE stock=9         ────────────────────────    9
T6       ────────────────────────     执行: UPDATE stock=9        9
T7       返回成功                     ────────────────────────    9
T8       ────────────────────────     返回成功                    9

结果:两个请求都成功,但库存只减少了1!这就是超卖问题的本质。

实际生产环境的影响:

假设一款iPhone有1000件库存,同时有10000个用户抢购:

  • 预期结果:前1000个用户抢购成功,库存归零,后9000个用户看到"库存不足"
  • 实际结果:由于并发冲突,可能出现1200个用户抢购成功,超卖200件

超卖200件意味着:

  • 需要向200个用户退款并赔偿违约金
  • 品牌方信誉受损
  • 可能面临法律风险
  • 用户流失率上升

1.3 悲观锁方案:SELECT FOR UPDATE

为了解决并发冲突,最直观的方案是使用数据库的行锁:

sql 复制代码
-- 悲观锁方案
BEGIN;
SELECT stock FROM product WHERE id = 1 FOR UPDATE;
-- 应用层判断库存是否充足
UPDATE product SET stock = stock - 1 WHERE id = 1;
COMMIT;

SELECT FOR UPDATE 的工作原理:

  1. 当事务A执行 SELECT FOR UPDATE 时,MySQL会对该行加排他锁(X锁)
  2. 其他事务试图对同一行执行 SELECT FOR UPDATE 时,会被阻塞
  3. 只有当事务A提交或回滚后,锁才会释放,其他事务才能继续执行

Java实现:

java 复制代码
public boolean deductStockWithPessimisticLock(Long productId, Integer quantity) {
    Connection conn = null;
    try {
        conn = dataSource.getConnection();
        conn.setAutoCommit(false);

        // 使用FOR UPDATE加行锁
        PreparedStatement queryStmt = conn.prepareStatement(
            "SELECT stock FROM product WHERE id = ? FOR UPDATE"
        );
        queryStmt.setLong(1, productId);
        ResultSet rs = queryStmt.executeQuery();

        if (!rs.next()) {
            conn.rollback();
            return false;
        }

        int currentStock = rs.getInt("stock");

        if (currentStock < quantity) {
            conn.rollback();
            return false;
        }

        // 更新库存
        PreparedStatement updateStmt = conn.prepareStatement(
            "UPDATE product SET stock = stock - ? WHERE id = ?"
        );
        updateStmt.setInt(1, quantity);
        updateStmt.setLong(2, productId);
        updateStmt.executeUpdate();

        conn.commit();
        return true;

    } catch (Exception e) {
        if (conn != null) {
            conn.rollback();
        }
        throw new RuntimeException("扣减库存失败", e);
    } finally {
        if (conn != null) {
            conn.close();
        }
    }
}

悲观锁方案的优缺点:

优点 缺点
实现简单,容易理解 并发能力低,请求串行化
可以有效防止超卖 数据库连接占用时间长
数据一致性有保障 容易产生死锁
适用于低并发场景 高并发时响应时间长

1.4 乐观锁方案:CAS(Compare And Swap)

另一种思路是乐观锁,不直接加锁,而是通过版本号机制来检测冲突:

sql 复制代码
-- 建表时增加version字段
CREATE TABLE product (
    id BIGINT PRIMARY KEY,
    name VARCHAR(100),
    stock INT,
    version INT DEFAULT 0,
    update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

-- 乐观锁扣减
UPDATE product
SET stock = stock - 1,
    version = version + 1
WHERE id = 1
  AND version = 5;  -- 假设读取时version是5

工作原理:

  1. 先查询商品信息和当前版本号:SELECT id, stock, version FROM product WHERE id = 1
  2. 应用层判断库存是否充足
  3. 执行UPDATE时,在WHERE条件中带上版本号
  4. 如果UPDATE影响行数为0,说明版本号已变化,需要重试

Java实现:

java 复制代码
public boolean deductStockWithOptimisticLock(Long productId, Integer quantity) {
    int maxRetries = 3; // 最大重试次数

    for (int i = 0; i < maxRetries; i++) {
        try {
            // 1. 查询当前库存和版本号
            Product product = productMapper.selectById(productId);

            if (product == null) {
                return false; // 商品不存在
            }

            if (product.getStock() < quantity) {
                return false; // 库存不足
            }

            // 2. 使用版本号更新
            int rows = productMapper.deductStockWithVersion(
                productId,
                product.getVersion(),
                quantity
            );

            if (rows > 0) {
                return true; // 更新成功
            }

            // rows == 0 说明版本号已变化,重试
            Thread.sleep(10); // 短暂等待后重试

        } catch (Exception e) {
            // 记录日志,继续重试
        }
    }

    return false; // 重试次数用尽,失败
}

MyBatis Mapper:

xml 复制代码
<update id="deductStockWithVersion">
    UPDATE product
    SET stock = stock - #{quantity},
        version = version + 1
    WHERE id = #{productId}
      AND version = #{version}
</update>

乐观锁方案的优缺点:

优点 缺点
无锁竞争,吞吐量较高 冲突率高时需要重试
死锁风险低 高并发下CPU消耗高
适用于读多写少场景 无法解决ABA问题

1.5 数据库方案的根本瓶颈

通过前面的分析,我们可以看到无论是悲观锁还是乐观锁,都存在一个根本性的限制:

数据库的本质限制:

  1. 磁盘I/O限制 - 即使使用SSD,磁盘I/O仍然是瓶颈
  2. 锁竞争 - 行锁、表锁、间隙锁等都会限制并发
  3. 连接数限制 - 数据库连接池大小有限制
  4. 事务开销 - ACID保证带来的额外开销

MySQL的性能极限:

根据官方测试数据和业界实践:

  • 单表QPS上限: 500-1000(简单查询)
  • 单表写入QPS: 200-500(带索引)
  • 并发连接数: 默认151,最大可调至10000
  • 行锁等待时间: 高并发下可达数秒

结论:

数据库方案无论怎么优化,其QPS上限也就是几百到一千左右。对于秒杀场景动辄数万甚至数十万的QPS需求,数据库方案是根本无法满足的。

1.6 性能对比数据

让我们通过一个直观的对比来看传统方案与Redis方案的差距:

详细对比数据:

对比维度 传统方案(悲观锁) 传统方案(乐观锁) Redis方案 提升倍数
QPS性能 200 600 100,000+ 100-500倍
响应时间 500ms 150ms <1ms 100-500倍
并发能力 受DB连接限制 中等 无上限 水平扩展
超卖控制 无超卖 无超卖 0%超卖 完美解决
扩展方式 垂直扩展 垂直扩展 水平扩展 成本低
运维成本 降低70%
可靠性 单点故障风险 单点故障风险 高可用集群 大幅提升

实战案例对比:

某电商秒杀活动,1000件iPhone 15 Pro,10万用户同时抢购:

方案 成功处理QPS 平均响应时间 超卖数量 用户体验
直接操作DB 300 2秒 超卖50件 大量超时
悲观锁 200 3秒 0件 严重拥堵
乐观锁 500 500ms 0件 部分重试
Redis方案 80000 2ms 0件 秒级响应

二、Redis缓存解决方案

2.1 为什么选择Redis?

Redis是解决库存扣减问题的理想选择,这得益于其独特的架构和特性:

Redis的核心优势:

  1. 纯内存操作 - 所有数据存储在内存中,读写速度极快
  2. 单线程模型 - 避免了多线程竞争和上下文切换开销
  3. I/O多路复用 - 高效处理大量并发连接
  4. 丰富的数据结构 - String、Hash、Set、List等满足不同需求
  5. 原子操作 - 单个命令是原子的,无需额外加锁
  6. Lua脚本支持 - 多个操作可以原子执行
  7. 持久化机制 - RDB和AOF两种方式保证数据安全
  8. 分布式能力 - 主从复制、哨兵、集群等完善方案

Redis与MySQL的性能对比:

操作 MySQL Redis 性能差异
简单查询 10-50ms 0.1-1ms 10-50倍
带条件更新 20-100ms 0.1-1ms 20-100倍
事务操作 50-200ms 1-5ms 10-50倍
单机QPS 500-1000 100,000+ 100倍+

2.2 整体架构设计

采用Redis缓存方案后,整个系统的架构需要重新设计:

系统分层详解:

第一层:客户端层

  • Web前端:Vue/React/Angular单页应用
  • 移动端:iOS/Android原生应用
  • 小程序:微信/支付宝小程序

第二层:负载均衡层

  • Nginx反向代理:实现负载均衡和静态资源缓存
  • CDN加速:静态资源分发到边缘节点
  • API网关:统一入口、限流、鉴权、熔断
nginx 复制代码
upstream backend {
    server 192.168.1.101:8080;
    server 192.168.1.102:8080;
    server 192.168.1.103:8080;
}

server {
    listen 80;
    server_name api.example.com;

    location /api/ {
        proxy_pass http://backend;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

第三层:应用层

  • Spring Boot应用集群:水平扩展,无状态设计
  • 限流组件:Guava RateLimiter或Sentinel
  • 熔断降级:Hystrix或Resilience4j

第四层:缓存层(核心)

  • Redis分片集群:按商品ID分布库存
  • 本地缓存:Caffeine作为二级缓存
  • 缓存预热:系统启动时加载热点数据

第五层:消息层

  • RabbitMQ/Kafka:异步同步Redis与MySQL
  • 消息持久化:保证消息不丢失
  • 死信队列:处理异常情况

第六层:持久层

  • MySQL集群:主从复制,读写分离
  • 分库分表:按业务维度拆分
  • 数据归档:历史数据迁移

2.3 Redis分片策略

为什么需要分片?单个Redis节点虽然性能强大,但在极端场景下仍有瓶颈:

单Redis节点的限制:

  1. 内存限制 - 单机内存有限(如64GB)
  2. CPU限制 - 单核CPU处理能力有上限
  3. 网络带宽限制 - 单网卡带宽有限
  4. 单点故障风险 - 一台机器故障影响全部商品

分片策略设计:

ini 复制代码
分片公式:shardIndex = productId % shardCount

示例(5个分片):
商品ID=1  → 分片0 → Redis节点0
商品ID=2  → 分片1 → Redis节点1
商品ID=3  → 分片2 → Redis节点2
商品ID=4  → 分片3 → Redis节点3
商品ID=5  → 分片4 → Redis节点4
商品ID=6  → 分片0 → Redis节点0
...

分片的优势:

优势 说明 效果
负载均衡 商品均匀分布到各节点 单节点压力降低N倍
高并发 N个节点并行处理 QPS提升N倍
易扩展 增加节点即可扩容 无需停机
高可用 单节点故障只影响部分商品 整体可用性提高

Key命名规范:

ruby 复制代码
库存Key:inventory:shard:{shardIndex}:product:{productId}:stock
用户Set:inventory:shard:{shardIndex}:product:{productId}:users
订单Hash:inventory:shard:{shardIndex}:product:{productId}:orders

示例:
inventory:shard:0:product:1:stock = "1000"
inventory:shard:0:product:1:users = {"user1", "user2", ...}
inventory:shard:0:product:1:orders = {"order1": "user1:1:1234567890", ...}

Java实现分片计算:

java 复制代码
@Component
@ConfigurationProperties(prefix = "redis.sharding")
public class RedisShardingConfig {

    private int nodeCount = 5; // 分片数量

    public int calculateShard(Long productId) {
        return (int) (productId % nodeCount);
    }

    public String buildStockKey(Long productId) {
        int shard = calculateShard(productId);
        return String.format("inventory:shard:%d:product:%d:stock", shard, productId);
    }

    public String buildUserSetKey(Long productId) {
        int shard = calculateShard(productId);
        return String.format("inventory:shard:%d:product:%d:users", shard, productId);
    }

    public String buildOrderHashKey(Long productId) {
        int shard = calculateShard(productId);
        return String.format("inventory:shard:%d:product:%d:orders", shard, productId);
    }

    // getter/setter省略
}

配置文件:

yaml 复制代码
redis:
  sharding:
    node-count: 5  # 分片数量

2.4 分片扩容方案

当业务增长时,可能需要增加分片数量。以下是几种扩容方案:

方案一:停机扩容(简单)

  1. 停止写入操作
  2. 将所有Redis数据导出
  3. 按新的分片规则重新分配数据
  4. 更新应用配置
  5. 启动服务

优点:实现简单 缺点:需要停机

方案二:在线扩容(推荐)

markdown 复制代码
步骤:
1. 新增Redis节点
2. 双写机制:同时写入旧分片和新分片
3. 数据迁移:后台逐步迁移数据
4. 读取切换:优先读新分片,未命中则读旧分片
5. 下线旧节点

双写实现:

java 复制代码
public void setStock(Long productId, Integer stock) {
    // 旧分片(3个)
    int oldShard = productId % 3;
    String oldKey = "inventory:shard:" + oldShard + ":product:" + productId;

    // 新分片(5个)
    int newShard = productId % 5;
    String newKey = "inventory:shard:" + newShard + ":product:" + productId;

    // 同时写入两个分片
    redisTemplate.opsForValue().set(oldKey, stock);
    redisTemplate.opsForValue().set(newKey, stock);
}

三、核心实现:Lua脚本原子扣减

3.1 为什么必须使用Lua脚本?

Redis的单个命令(如DECR、INCR)是原子的,但我们的库存扣减逻辑通常涉及多个步骤:

库存扣减的完整流程:

  1. 检查用户是否已购买(防止重复购买)
  2. 检查库存是否充足
  3. 扣减库存
  4. 记录购买用户(防止重复购买)
  5. 记录订单信息

如果使用多个Redis命令:

java 复制代码
// ❌ 错误示例:不是原子操作
public boolean deduct(String productId, String userId, int qty) {
    // 步骤1:检查用户是否已购买
    Boolean isMember = redisTemplate.opsForSet().isMember(userSetKey, userId);
    if (isMember) {
        return false;
    }

    // 步骤2:检查库存
    String stockStr = redisTemplate.opsForValue().get(stockKey);
    if (Integer.parseInt(stockStr) < qty) {
        return false;
    }

    // 步骤3:扣减库存
    redisTemplate.opsForValue().increment(stockKey, -qty);

    // 步骤4:记录用户
    redisTemplate.opsForSet().add(userSetKey, userId);

    // 步骤5:记录订单
    redisTemplate.opsForHash().put(orderHashKey, orderNo, orderInfo);

    return true;
}

问题分析:

上述代码虽然逻辑正确,但存在严重的并发问题:

css 复制代码
时间线    线程A                        线程B
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
T1       检查用户: 未购买             ───────────────────
T2       ───────────────────          检查用户: 未购买
T3       检查库存: 库存=10            ───────────────────
T4       ───────────────────          检查库存: 库存=10
T5       判断: 10 >= 1 ✓              ───────────────────
T6       ───────────────────          判断: 10 >= 1 ✓
T7       扣减库存: 库存=9             ───────────────────
T8       ───────────────────          扣减库存: 库存=8
T9       记录用户                     ───────────────────
T10      ───────────────────          记录用户(同一用户!)

结果:同一用户成功购买两次!

Lua脚本的原子性保证:

Redis保证Lua脚本的执行是原子的:

  • 脚本执行期间,不会插入其他命令
  • 脚本要么全部执行成功,要么完全不执行
  • 不存在竞态条件

3.2 库存扣减Lua脚本详解

完整Lua脚本:

lua 复制代码
-- ===================== 参数说明 =====================
-- KEYS[1]: 库存Key(如:inventory:shard:0:product:1:stock)
-- ARGV[1]: 扣减数量
-- ARGV[2]: 用户ID
-- ARGV[3]: 订单号
-- ARGV[4]: 时间戳

-- ===================== 变量定义 =====================
local stockKey = KEYS[1]                -- 库存Key
local userSetKey = stockKey .. ':users' -- 用户集合Key
local orderHashKey = stockKey .. ':orders' -- 订单Hash Key
local qty = tonumber(ARGV[1])           -- 扣减数量
local userId = ARGV[2]                  -- 用户ID
local orderNo = ARGV[3]                 -- 订单号
local timestamp = ARGV[4]               -- 时间戳

-- ===================== 第一步:检查用户是否已购买 =====================
-- 使用SISMEMBER命令检查用户是否在购买集合中
if redis.call('SISMEMBER', userSetKey, userId) == 1 then
  return -1  -- 返回-1表示用户已购买
end

-- ===================== 第二步:检查库存是否存在 =====================
local stock = tonumber(redis.call('GET', stockKey))
if stock == nil then
  return -2  -- 返回-2表示商品不存在
end

-- ===================== 第三步:检查库存是否充足 =====================
if stock < qty then
  return 0  -- 返回0表示库存不足
end

-- ===================== 第四步:扣减库存 =====================
-- 使用DECRBY命令原子性地扣减库存
redis.call('DECRBY', stockKey, qty)

-- ===================== 第五步:记录购买用户 =====================
-- 将用户ID添加到Set中,防止重复购买
redis.call('SADD', userSetKey, userId)

-- ===================== 第六步:记录订单信息 =====================
-- 将订单信息存储到Hash中
-- 格式:orderNo -> userId:quantity:timestamp
local orderInfo = userId .. ':' .. qty .. ':' .. timestamp
redis.call('HSET', orderHashKey, orderNo, orderInfo)

-- ===================== 返回成功 =====================
return 1  -- 返回1表示扣减成功

脚本执行流程图:

sql 复制代码
输入参数
   │
   ├─ KEYS[1] = "inventory:shard:0:product:1:stock"
   ├─ ARGV[1] = "1" (购买数量)
   ├─ ARGV[2] = "user123" (用户ID)
   ├─ ARGV[3] = "order456" (订单号)
   └─ ARGV[4] = "1699900000" (时间戳)
   │
   ▼
┌─────────────────────────────────┐
│  检查用户是否已购买              │
│  SISMEMBER userSetKey userId    │
└──────────────┬──────────────────┘
               │
        ┌──────┴──────┐
        │             │
       是            否
        │             │
        ▼             ▼
   返回 -1      ┌─────────────────────────┐
   (已购买)     │  检查库存是否存在        │
               │  GET stockKey           │
               └────────────┬────────────┘
                            │
                     ┌──────┴──────┐
                     │             │
                   不存在         存在
                     │             │
                     ▼             ▼
                返回 -2      ┌─────────────────────────┐
                (商品不存在)   │  检查库存是否充足        │
                              │  stock >= qty ?         │
                              └────────────┬────────────┘
                                           │
                                    ┌──────┴──────┐
                                    │             │
                                  不足           充足
                                    │             │
                                    ▼             ▼
                               返回 0      ┌───────────────────┐
                              (库存不足)    │  扣减库存          │
                                            │  DECRBY stockKey  │
                                            └─────────┬─────────┘
                                                      │
                                                      ▼
                                            ┌───────────────────┐
                                            │  记录购买用户      │
                                            │  SADD userSetKey  │
                                            └─────────┬─────────┘
                                                      │
                                                      ▼
                                            ┌───────────────────┐
                                            │  记录订单信息      │
                                            │  HSET ordersHash  │
                                            └─────────┬─────────┘
                                                      │
                                                      ▼
                                               返回 1 (成功)

3.3 库存回滚Lua脚本

当订单取消或超时未支付时,需要回滚库存:

lua 复制代码
-- ===================== 参数说明 =====================
-- KEYS[1]: 库存Key
-- ARGV[1]: 回滚数量
-- ARGV[2]: 用户ID
-- ARGV[3]: 订单号

local stockKey = KEYS[1]
local userSetKey = stockKey .. ':users'
local orderHashKey = stockKey .. ':orders'
local qty = tonumber(ARGV[1])
local userId = ARGV[2]
local orderNo = ARGV[3]

-- ===================== 第一步:删除订单记录 =====================
redis.call('HDEL', orderHashKey, orderNo)

-- ===================== 第二步:移除用户购买记录 =====================
redis.call('SREM', userSetKey, userId)

-- ===================== 第三步:恢复库存 =====================
redis.call('INCRBY', stockKey, qty)

-- ===================== 返回成功 =====================
return 1

3.4 Java实现详解

完整的Service实现:

java 复制代码
package com.redis.demo.service;

import com.alibaba.fastjson2.JSON;
import com.redis.demo.config.RedisShardingConfig;
import com.redis.demo.dto.DeductRequest;
import com.redis.demo.dto.DeductResponse;
import com.redis.demo.entity.Inventory;
import com.redis.demo.mapper.InventoryMapper;
import com.redis.demo.mq.message.InventoryDeductMessage;
import com.redis.demo.mq.producer.RabbitMQProducer;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.Collections;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

/**
 * 库存服务 - Redis分片缓存实现
 */
@Slf4j
@Service
public class InventoryService {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Resource
    private InventoryMapper inventoryMapper;

    @Autowired(required = false)
    private RabbitMQProducer rabbitMQProducer;

    @Resource
    private RedisShardingConfig redisShardingConfig;

    /**
     * Lua脚本: 原子性库存扣减
     */
    private static final String DEDUCT_LUA_SCRIPT =
            "local stockKey = KEYS[1]\n" +
            "local userSetKey = stockKey .. ':users'\n" +
            "local qty = tonumber(ARGV[1])\n" +
            "local userId = ARGV[2]\n" +
            "local orderNo = ARGV[3]\n" +
            "\n" +
            "-- 检查用户是否已购买\n" +
            "if redis.call('SISMEMBER', userSetKey, userId) == 1 then\n" +
            "  return -1\n" +
            "end\n" +
            "\n" +
            "-- 检查库存\n" +
            "local stock = tonumber(redis.call('GET', stockKey))\n" +
            "if stock == nil then\n" +
            "  return -2\n" +
            "end\n" +
            "\n" +
            "if stock < qty then\n" +
            "  return 0\n" +
            "end\n" +
            "\n" +
            "-- 扣减库存\n" +
            "redis.call('DECRBY', stockKey, qty)\n" +
            "-- 记录用户\n" +
            "redis.call('SADD', userSetKey, userId)\n" +
            "-- 记录订单\n" +
            "redis.call('HSET', stockKey .. ':orders', orderNo, userId .. ':' .. qty .. ':' .. ARGV[4])\n" +
            "\n" +
            "return 1";

    /**
     * Lua脚本: 回滚库存
     */
    private static final String ROLLBACK_LUA_SCRIPT =
            "local stockKey = KEYS[1]\n" +
            "local userSetKey = stockKey .. ':users'\n" +
            "local qty = tonumber(ARGV[1])\n" +
            "local userId = ARGV[2]\n" +
            "local orderNo = ARGV[3]\n" +
            "\n" +
            "redis.call('HDEL', stockKey .. ':orders', orderNo)\n" +
            "redis.call('SREM', userSetKey, userId)\n" +
            "redis.call('INCRBY', stockKey, qty)\n" +
            "\n" +
            "return 1";

    private DefaultRedisScript<Long> deductScript;
    private DefaultRedisScript<Long> rollbackScript;

    @PostConstruct
    public void init() {
        // 初始化扣减脚本
        deductScript = new DefaultRedisScript<>();
        deductScript.setScriptText(DEDUCT_LUA_SCRIPT);
        deductScript.setResultType(Long.class);

        // 初始化回滚脚本
        rollbackScript = new DefaultRedisScript<>();
        rollbackScript.setScriptText(ROLLBACK_LUA_SCRIPT);
        rollbackScript.setResultType(Long.class);

        log.info("Lua脚本初始化完成");
    }

    /**
     * 扣减库存
     */
    public DeductResponse deduct(DeductRequest request) {
        long startTime = System.currentTimeMillis();
        DeductResponse response = new DeductResponse();
        response.setSuccess(false);

        try {
            // 1. 计算分片
            int shard = calculateShard(request.getProductId());
            response.setShardIndex(shard);

            // 2. 构建Key
            String stockKey = buildStockKey(request.getProductId());

            // 3. 检查缓存是否存在
            String cachedStock = stringRedisTemplate.opsForValue().get(stockKey);
            if (StringUtils.isBlank(cachedStock)) {
                // 缓存不存在,从数据库加载
                Inventory inventory = inventoryMapper.selectByProductId(request.getProductId());
                if (inventory == null) {
                    response.setErrorMessage("商品不存在");
                    return response;
                }
                // 初始化缓存
                stringRedisTemplate.opsForValue().set(stockKey,
                    String.valueOf(inventory.getAvailableStock()),
                    24, TimeUnit.HOURS);
                cachedStock = String.valueOf(inventory.getAvailableStock());
            }

            // 4. 执行Lua脚本扣减库存
            Long result = stringRedisTemplate.execute(
                deductScript,
                Collections.singletonList(stockKey),
                String.valueOf(request.getQuantity()),
                String.valueOf(request.getUserId()),
                request.getOrderNo(),
                String.valueOf(System.currentTimeMillis())
            );

            // 5. 处理结果
            if (result == null || result == -2L) {
                response.setErrorMessage("商品库存数据异常");
                return response;
            }

            if (result == -1L) {
                response.setErrorMessage("用户已购买,不能重复购买");
                return response;
            }

            if (result == 0L) {
                String remaining = stringRedisTemplate.opsForValue().get(stockKey);
                response.setRemainingStock(remaining != null ? Integer.parseInt(remaining) : 0);
                response.setErrorMessage("库存不足");
                return response;
            }

            // 6. 扣减成功
            String remaining = stringRedisTemplate.opsForValue().get(stockKey);
            response.setRemainingStock(Integer.parseInt(remaining));
            response.setSuccess(true);

            // 7. 发送MQ消息异步同步数据库
            if (rabbitMQProducer != null) {
                InventoryDeductMessage message = InventoryDeductMessage.builder()
                    .productId(request.getProductId())
                    .userId(request.getUserId())
                    .orderNo(request.getOrderNo())
                    .quantity(request.getQuantity())
                    .shardIndex(shard)
                    .timestamp(System.currentTimeMillis())
                    .build();

                rabbitMQProducer.sendInventoryDeductMessage(message);
            }

            log.info("库存扣减成功 - 商品ID:{}, 用户ID:{}, 数量:{}, 剩余库存:{}, 分片:{}, 耗时:{}ms",
                request.getProductId(), request.getUserId(), request.getQuantity(),
                response.getRemainingStock(), shard,
                System.currentTimeMillis() - startTime);

        } catch (Exception e) {
            log.error("库存扣减异常", e);
            response.setErrorMessage("系统异常: " + e.getMessage());
        } finally {
            response.setCostTime(System.currentTimeMillis() - startTime);
        }

        return response;
    }

    /**
     * 回滚库存
     */
    public boolean rollback(Long productId, Long userId, String orderNo, Integer quantity) {
        try {
            String stockKey = buildStockKey(productId);

            stringRedisTemplate.execute(
                rollbackScript,
                Collections.singletonList(stockKey),
                String.valueOf(quantity),
                String.valueOf(userId),
                orderNo
            );

            log.info("库存回滚成功 - 商品ID:{}, 用户ID:{}, 订单号:{}, 数量:{}",
                productId, userId, orderNo, quantity);
            return true;
        } catch (Exception e) {
            log.error("库存回滚失败", e);
            return false;
        }
    }

    /**
     * 计算Redis分片
     */
    private int calculateShard(Long productId) {
        return (int) (productId % redisShardingConfig.getNodeCount());
    }

    /**
     * 构建库存Key
     */
    private String buildStockKey(Long productId) {
        int shard = calculateShard(productId);
        return String.format("inventory:shard:%d:product:%d:stock", shard, productId);
    }

    /**
     * 查询库存(优先从缓存)
     */
    public Integer getStock(Long productId) {
        String stockKey = buildStockKey(productId);
        String cached = stringRedisTemplate.opsForValue().get(stockKey);

        if (StringUtils.isNotBlank(cached)) {
            return Integer.parseInt(cached);
        }

        // 缓存不存在,查询数据库
        Inventory inventory = inventoryMapper.selectByProductId(productId);
        if (inventory != null) {
            // 回写缓存
            stringRedisTemplate.opsForValue().set(stockKey,
                String.valueOf(inventory.getAvailableStock()),
                24, TimeUnit.HOURS);
            return inventory.getAvailableStock();
        }

        return 0;
    }

    /**
     * 预热库存缓存
     */
    public void warmUpCache() {
        log.info("开始预热库存缓存...");
        try {
            List<Inventory> inventories = inventoryMapper.selectAll();
            if (inventories == null || inventories.isEmpty()) {
                log.warn("没有库存数据需要预热");
                return;
            }

            int successCount = 0;
            int failCount = 0;

            for (Inventory inventory : inventories) {
                try {
                    String stockKey = buildStockKey(inventory.getProductId());
                    stringRedisTemplate.opsForValue().set(stockKey,
                        String.valueOf(inventory.getAvailableStock()),
                        24, TimeUnit.HOURS);
                    successCount++;
                } catch (Exception e) {
                    log.error("预热商品{}库存失败", inventory.getProductId(), e);
                    failCount++;
                }
            }

            log.info("库存缓存预热完成 - 成功:{}, 失败:{}", successCount, failCount);
        } catch (Exception e) {
            log.error("预热库存缓存异常", e);
        }
    }
}

四、缓存与数据库双写一致性

4.1 一致性挑战分析

使用Redis缓存后,我们面临一个核心问题:如何保证Redis缓存与MySQL数据库的数据一致性?

双写一致性困境:

复制代码
场景:用户购买商品,需要同时更新Redis和MySQL

方案一:先更新Redis,再更新MySQL
问题:如果MySQL更新失败,Redis已经扣减,数据不一致

方案二:先更新MySQL,再更新Redis
问题:如果Redis更新失败,MySQL已经扣减,数据不一致

方案三:同时更新(使用分布式事务)
问题:性能极差,无法满足高并发需求

4.2 异步双写方案

我们采用异步双写方案,核心思想是:

  1. 写流程:Redis扣减 → 立即返回用户 → MQ异步同步MySQL
  2. 读流程:先读Redis → 缓存命中返回 → 缓存未命中读MySQL并回写Redis

写流程详解:

java 复制代码
// 完整的写流程
public DeductResponse deduct(DeductRequest request) {
    // 第1步:Redis扣减(核心路径,<1ms)
    Long result = executeLuaScript(request);

    if (result != 1) {
        // 扣减失败,直接返回
        return buildFailResponse(result);
    }

    // 第2步:立即返回用户(不等待MySQL)
    DeductResponse response = buildSuccessResponse();
    response.setCostTime(System.currentTimeMillis() - startTime);

    // 第3步:异步发送MQ消息(非阻塞)
    CompletableFuture.runAsync(() -> {
        try {
            rabbitMQProducer.sendInventoryDeductMessage(message);
        } catch (Exception e) {
            // 发送失败,记录日志,后续补偿
            log.error("发送MQ消息失败", e);
        }
    });

    return response;
}

读流程详解:

java 复制代码
// 完整的读流程
public Integer getStock(Long productId) {
    String stockKey = buildStockKey(productId);

    // 第1步:先读Redis
    String cached = redisTemplate.opsForValue().get(stockKey);
    if (StringUtils.isNotBlank(cached)) {
        return Integer.parseInt(cached); // 缓存命中,直接返回
    }

    // 第2步:缓存未命中,读MySQL
    Inventory inventory = inventoryMapper.selectByProductId(productId);
    if (inventory != null) {
        // 第3步:回写Redis(Cache Aside模式)
        redisTemplate.opsForValue().set(stockKey,
            String.valueOf(inventory.getAvailableStock()),
            24, TimeUnit.HOURS);
        return inventory.getAvailableStock();
    }

    return 0;
}

4.3 消息队列实现

RabbitMQ配置:

java 复制代码
@Configuration
public class RabbitMQConfig {

    // 交换机
    @Bean
    public DirectExchange inventoryExchange() {
        return new DirectExchange("inventory.deduct.exchange", true, false);
    }

    // 队列
    @Bean
    public Queue inventoryDeductQueue() {
        return QueueBuilder.durable("inventory.deduct.queue")
                .withArgument("x-dead-letter-exchange", "inventory.dlt.exchange")
                .build();
    }

    // 绑定
    @Bean
    public Binding inventoryBinding() {
        return BindingBuilder.bind(inventoryDeductQueue())
                .to(inventoryExchange())
                .with("inventory.deduct");
    }

    // 死信交换机
    @Bean
    public DirectExchange inventoryDltExchange() {
        return new DirectExchange("inventory.dlt.exchange", true, false);
    }

    // 死信队列
    @Bean
    public Queue inventoryDltQueue() {
        return QueueBuilder.durable("inventory.dlt.queue").build();
    }

    // 死信绑定
    @Bean
    public Binding inventoryDltBinding() {
        return BindingBuilder.bind(inventoryDltQueue())
                .to(inventoryDltExchange())
                .with("inventory.deduct");
    }
}

生产者实现:

java 复制代码
@Component
@Slf4j
public class RabbitMQProducer {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    public void sendInventoryDeductMessage(InventoryDeductMessage message) {
        try {
            rabbitTemplate.convertAndSend(
                "inventory.deduct.exchange",
                "inventory.deduct",
                JSON.toJSONString(message)
            );
            log.info("MQ消息发送成功 - 订单号:{}", message.getOrderNo());
        } catch (Exception e) {
            log.error("MQ消息发送失败 - 订单号:{}", message.getOrderNo(), e);
            throw e;
        }
    }
}

消费者实现:

java 复制代码
@Component
@Slf4j
public class InventoryDeductConsumer {

    @Autowired
    private InventoryMapper inventoryMapper;

    @RabbitListener(queues = "inventory.deduct.queue")
    public void onMessage(String message, Channel channel, long deliveryTag) {
        try {
            InventoryDeductMessage msg = JSON.parseObject(message, InventoryDeductMessage.class);

            // 使用乐观锁更新数据库(带重试)
            int maxRetries = 3;
            for (int i = 0; i < maxRetries; i++) {
                try {
                    int rows = inventoryMapper.deductStock(
                        msg.getProductId(),
                        msg.getQuantity()
                    );

                    if (rows > 0) {
                        // 更新成功,确认消息
                        channel.basicAck(deliveryTag, false);
                        log.info("数据库更新成功 - 订单号:{}", msg.getOrderNo());
                        return;
                    }
                } catch (Exception e) {
                    if (i == maxRetries - 1) {
                        throw e;
                    }
                    Thread.sleep(100);
                }
            }

        } catch (Exception e) {
            log.error("消息处理失败", e);
            try {
                // 拒绝消息,重新入队
                channel.basicNack(deliveryTag, false, true);
            } catch (Exception ex) {
                log.error("NACK失败", ex);
            }
        }
    }
}

4.4 一致性保障机制

为了确保最终一致性,我们实现了以下机制:

1. Redis事务保障

使用Lua脚本保证Redis操作的原子性,无需额外的事务控制。

2. MQ可靠投递

  • 消息持久化:队列和消息都设置为持久化
  • ACK机制:消费成功后手动确认
  • 重试策略:消费失败自动重试
yaml 复制代码
spring:
  rabbitmq:
    publisher-confirm-type: correlated  # 发布确认
    publisher-returns: true              # 发布返回
    listener:
      simple:
        acknowledge-mode: manual          # 手动确认
        retry:
          enabled: true
          max-attempts: 3
          initial-interval: 1000ms

3. 幂等设计

使用唯一订单号防止重复处理:

sql 复制代码
-- 数据库表增加唯一约束
CREATE TABLE inventory_log (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    order_no VARCHAR(64) UNIQUE NOT NULL,
    product_id BIGINT NOT NULL,
    quantity INT NOT NULL,
    create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

4. 定时对账

java 复制代码
@Component
@Slf4j
public class ReconciliationTask {

    @Scheduled(cron = "0 0 2 * * ?") // 每天凌晨2点执行
    public void reconcile() {
        log.info("开始执行库存对账任务");

        // 1. 获取Redis中的所有订单
        // 2. 获取MySQL中的所有订单
        // 3. 比对差异
        // 4. 生成差异报告
        // 5. 自动修复或人工介入
    }
}

5. 补偿机制

发现不一致时自动触发修复:

java 复制代码
public void compensateInventory(Long productId) {
    // 1. 获取Redis库存
    String redisStock = getStockFromRedis(productId);

    // 2. 获取MySQL库存
    int dbStock = getStockFromDB(productId);

    // 3. 判断差异
    int diff = dbStock - Integer.parseInt(redisStock);

    if (diff != 0) {
        // 4. 以MySQL为准,修复Redis
        log.warn("发现库存差异 - 商品ID:{}, Redis:{}, MySQL:{}, 差异:{}",
            productId, redisStock, dbStock, diff);

        // 修复Redis
        setStockToRedis(productId, dbStock);
    }
}

五、完整库存扣减流程

详细步骤说明:

  1. 用户发起请求 - 用户点击"立即购买"按钮,前端发送POST请求到后端API

  2. 请求参数校验 - 校验商品ID、用户ID、购买数量等参数的合法性

  3. 计算Redis分片 - 根据商品ID使用哈希算法计算该商品在哪个Redis分片

  4. 检查缓存 - 检查Redis中是否有该商品的库存缓存

  5. 缓存不存在 - 从MySQL加载库存并初始化到Redis,设置24小时过期

  6. 执行Lua脚本 - 原子性地检查用户、库存并扣减,整个过程在Redis内部完成

  7. 返回用户 - 立即返回结果给用户,总耗时<1ms

  8. 发送MQ消息 - 异步发送库存变更消息到RabbitMQ

  9. 消费者处理 - MQ消费者监听队列,异步更新MySQL数据库

  10. 重试机制 - 更新失败自动重试,最多3次


六、实战案例:电商秒杀场景

6.1 业务背景

某电商平台准备举办一场iPhone 15 Pro秒杀活动:

活动参数:

  • 商品数量:1000件iPhone 15 Pro
  • 参与用户:预计10万用户同时抢购
  • 性能要求:QPS要求50000+,响应时间<10ms
  • 业务要求:不允许超卖,同一用户限购1件

6.2 技术架构设计

Redis分片架构:

css 复制代码
分片数量:5个主节点,每个配置1个从节点
分片算法:productId % 5
Key命名:inventory:shard:{0-4}:product:{productId}:stock

Redis节点分布:
├── 分片0: 192.168.1.101:6379 (主) + 192.168.1.106:6379 (从)
├── 分片1: 192.168.1.102:6379 (主) + 192.168.1.107:6379 (从)
├── 分片2: 192.168.1.103:6379 (主) + 192.168.1.108:6379 (从)
├── 分片3: 192.168.1.104:6379 (主) + 192.168.1.109:6379 (从)
└── 分片4: 192.168.1.105:6379 (主) + 192.168.1.110:6379 (从)

应用服务器集群:

复制代码
服务器数量:10台
配置:8核16G
JVM堆:8GB
线程池:500+
部署方式:Docker容器
负载均衡:Nginx

6.3 核心代码实现

限流组件:

java 复制代码
@Component
public class RateLimiter {

    // 每秒发放10000个令牌
    private final RateLimiter limiter = GuavaRateLimiter.create(10000.0);

    public boolean tryAcquire() {
        return limiter.tryAcquire();
    }
}

// Controller中使用
@PostMapping("/seckill/{productId}")
public Result seckill(@PathVariable Long productId, @RequestParam Long userId) {
    // 第1步:限流
    if (!rateLimiter.tryAcquire()) {
        return Result.error("请求过于频繁,请稍后重试");
    }

    // 第2步:扣减库存
    DeductRequest request = DeductRequest.builder()
        .productId(productId)
        .userId(userId)
        .quantity(1)
        .orderNo(generateOrderNo())
        .build();

    DeductResponse response = inventoryService.deduct(request);

    if (response.isSuccess()) {
        return Result.success("抢购成功");
    } else {
        return Result.error(response.getErrorMessage());
    }
}

降级方案:

java 复制代码
public DeductResponse deduct(DeductRequest request) {
    try {
        // 优先使用Redis扣减
        return deductByRedis(request);
    } catch (RedisConnectionException e) {
        log.warn("Redis故障,降级到MySQL - {}", e.getMessage());
        // Redis故障,降级到MySQL(串行扣减)
        return deductByDB(request);
    }
}

private DeductResponse deductByDB(DeductRequest request) {
    // 使用分布式锁保证串行执行
    String lockKey = "lock:product:" + request.getProductId();
    try {
        // 获取分布式锁
        Boolean locked = redisTemplate.opsForValue()
            .setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);

        if (Boolean.TRUE.equals(locked)) {
            // 执行数据库扣减
            return inventoryMapper.deductStock(request);
        } else {
            return DeductResponse.fail("系统繁忙,请稍后重试");
        }
    } finally {
        // 释放锁
        redisTemplate.delete(lockKey);
    }
}

6.4 实施效果

性能对比:

指标 优化前(直接DB) 优化后(Redis方案) 提升
QPS 500 80000+ 160倍
响应时间 200ms 3ms 98%
超卖率 2% 0% 完美解决
成功率 85% 99.9% 17%
CPU使用率 95% 40% 降低58%
数据库连接数 800/800 50/800 降低94%

用户满意度提升:

  • 页面加载时间从2秒降至<100ms
  • 抢购成功率从85%提升至99.9%
  • 客服投诉量下降90%

七、生产环境部署

7.1 Redis集群部署

Docker Compose部署:

yaml 复制代码
version: '3.8'

services:
  redis-node-0:
    image: redis:7-alpine
    container_name: redis-node-0
    command: redis-server --appendonly yes --maxmemory 2gb --maxmemory-policy allkeys-lru
    ports:
      - "7000:6379"
    volumes:
      - redis-node-0-data:/data

  redis-node-1:
    image: redis:7-alpine
    container_name: redis-node-1
    command: redis-server --appendonly yes --maxmemory 2gb --maxmemory-policy allkeys-lru
    ports:
      - "7001:6379"
    volumes:
      - redis-node-1-data:/data

  redis-node-2:
    image: redis:7-alpine
    container_name: redis-node-2
    command: redis-server --appendonly yes --maxmemory 2gb --maxmemory-policy allkeys-lru
    ports:
      - "7002:6379"
    volumes:
      - redis-node-2-data:/data

  redis-node-3:
    image: redis:7-alpine
    container_name: redis-node-3
    command: redis-server --appendonly yes --maxmemory 2gb --maxmemory-policy allkeys-lru
    ports:
      - "7003:6379"
    volumes:
      - redis-node-3-data:/data

  redis-node-4:
    image: redis:7-alpine
    container_name: redis-node-4
    command: redis-server --appendonly yes --maxmemory 2gb --maxmemory-policy allkeys-lru
    ports:
      - "7004:6379"
    volumes:
      - redis-node-4-data:/data

volumes:
  redis-node-0-data:
  redis-node-1-data:
  redis-node-2-data:
  redis-node-3-data:
  redis-node-4-data:

7.2 应用配置

application.yml:

yaml 复制代码
server:
  port: 8080
  tomcat:
    threads:
      max: 500
      min-spare: 50

spring:
  redis:
    lettuce:
      pool:
        max-active: 50
        max-idle: 20
        min-idle: 10
        max-wait: 1000ms
    cluster:
      nodes:
        - 192.168.1.101:7000
        - 192.168.1.102:7001
        - 192.168.1.103:7002
        - 192.168.1.104:7003
        - 192.168.1.105:7004
      max-redirects: 3

  rabbitmq:
    host: 192.168.1.201
    port: 5672
    username: admin
    password: admin123
    publisher-confirm-type: correlated
    publisher-returns: true
    listener:
      simple:
        acknowledge-mode: manual
        retry:
          enabled: true
          max-attempts: 3

  datasource:
    url: jdbc:mysql://192.168.1.301:3306/inventory?useUnicode=true&characterEncoding=utf8
    username: root
    password: root123
    hikari:
      maximum-pool-size: 50
      minimum-idle: 10
      connection-timeout: 3000
      idle-timeout: 600000

# 自定义配置
redis:
  sharding:
    node-count: 5

八、总结

本文详细介绍了在大流量场景下使用Redis分片缓存解决库存扣减数据库瓶颈的完整方案。

相关推荐
开心就好20253 小时前
UniApp开发应用多平台上架全流程:H5小程序iOS和Android
后端·ios
悟空码字3 小时前
告别“屎山代码”:AI 代码整洁器让老项目重获新生
后端·aigc·ai编程
小码哥_常3 小时前
大厂不宠@Transactional,背后藏着啥秘密?
后端
奋斗小强3 小时前
内存危机突围战:从原理辨析到线上实战,彻底搞懂 OOM 与内存泄漏
后端
小码哥_常3 小时前
Spring Boot接口防抖秘籍:告别“手抖”,守护数据一致性
后端
心之语歌4 小时前
基于注解+拦截器的API动态路由实现方案
java·后端
None3214 小时前
【NestJs】基于Redlock装饰器分布式锁设计与实现
后端·node.js
初次攀爬者4 小时前
Kafka + KRaft模式架构基础介绍
后端·kafka
洛森唛4 小时前
Elasticsearch DSL 查询语法大全:从入门到精通
后端·elasticsearch