【Java项目技术亮点】Redis Lua脚本原子化操作:高并发场景下的终极武器

扣减库存时,先查再减,两个命令之间被其他线程插队------结果库存变成负数。这就是没有原子操作的后果。本文将揭秘Redis Lua脚本如何在高并发场景下实现原子化操作,彻底解决竞态条件问题,让你的代码既安全又高效。

文章目录


一、场景引入:一次库存超卖事故

1.1 真实案例

某电商平台限时抢购活动:

复制代码
事故现场:
库存剩余:1件

并发请求:
线程A:查询库存 → stock = 1 → stock > 0 → 准备扣减
线程B:查询库存 → stock = 1 → stock > 0 → 准备扣减
线程A:执行扣减 → stock = 0
线程B:执行扣减 → stock = -1  ← 超卖!

后果:
- 1000件商品,实际卖出了1200件
- 每件亏500元,直接损失10万
- 用户投诉:"我付了钱,但没收到货"
- 客服忙不过来,退款排队

问题根源:多个Redis命令之间不是原子的,存在竞态条件(Race Condition)。

1.2 传统方案的缺陷

java 复制代码
// ❌ 错误方案:先查后减(非原子操作)
public boolean deductStock(String key) {
    int stock = Integer.parseInt(redisTemplate.opsForValue().get(key));
    if (stock > 0) {
        redisTemplate.opsForValue().set(key, String.valueOf(stock - 1));
        return true;
    }
    return false;
}
// 并发时,多个线程同时读到stock=1,都判断>0,都执行扣减

// ❌ 错误方案:DECR(不能判断是否足够)
public boolean deductStock(String key) {
    Long stock = redisTemplate.opsForValue().decrement(key);
    return stock >= 0;  // 已经减成-1了,判断也没用
}

// ✅ 正确方案:Lua脚本(原子操作)
// 判断 + 扣减 在一个脚本中完成,Redis单线程保证原子性

二、解决方案:Redis Lua脚本

2.1 为什么Lua脚本能解决原子性问题?

复制代码
Redis执行模型:

┌─────────────────────────────────────────────────────────────┐
│                                                              │
│   Redis是单线程执行命令的:                                  │
│                                                              │
│   普通方式(多次调用):                                      │
│   ┌─────────┐  ┌─────────┐  ┌─────────┐                     │
│   │ GET stock│  │ 判断>0  │  │ SET stock│  ← 三个命令之间    │
│   └─────────┘  └─────────┘  └─────────┘     可能被其他命令   │
│                                       插队!               │
│   Lua脚本方式(一次调用):                                   │
│   ┌─────────────────────────────────────────┐               │
│   │ if GET(stock) > 0 then                 │               │
│   │     SET stock = stock - 1              │               │
│   │     return 1                           │  ← 一个原子操作 │
│   │ else                                   │     不可被打断   │
│   │     return 0                           │               │
│   │ end                                    │               │
│   └─────────────────────────────────────────┘               │
│                                                              │
│   核心优势:                                                 │
│   1. 原子性:整个脚本在Redis中一次性执行,不会被打断        │
│   2. 减少网络往返:多个操作合并为一次请求                     │
│   3. 复用性:脚本可以预加载,后续只传参数                    │
│                                                              │
└─────────────────────────────────────────────────────────────┘

2.2 Lua脚本的核心优势

优势 说明 性能提升
原子性 整个脚本在Redis单线程中执行,不会被其他命令打断 彻底解决竞态条件
减少网络IO 多个操作合并为一次请求 网络往返减少N倍
脚本复用 EVALSHA使用SHA1缓存脚本 后续调用只传参数
复杂逻辑 支持条件判断、循环等复杂逻辑 替代多层应用代码

三、实战代码:六大经典场景

3.1 场景一:库存扣减(最经典)

lua 复制代码
-- deduct_stock.lua
-- 原子操作:判断库存 + 扣减 + 记录
-- KEYS[1]: 库存key
-- ARGV[1]: 扣减数量
-- ARGV[2]: 用户ID(防重复购买)
-- ARGV[3]: 已购集合key

local stockKey = KEYS[1]
local deductQty = tonumber(ARGV[1])
local userId = ARGV[2]
local soldSetKey = ARGV[3]

-- 1. 检查是否已购买
local isBought = redis.call('SISMEMBER', soldSetKey, userId)
if tonumber(isBought) == 1 then
    return -2  -- 已购买
end

-- 2. 获取库存
local stock = tonumber(redis.call('GET', stockKey))
if stock == nil then
    return -3  -- 库存不存在
end

-- 3. 判断并扣减
if stock >= deductQty then
    redis.call('DECRBY', stockKey, deductQty)
    redis.call('SADD', soldSetKey, userId)
    return stock - deductQty  -- 返回剩余库存
else
    return -1  -- 库存不足
end
java 复制代码
/**
 * 库存扣减服务
 */
@Service
@Slf4j
public class StockService {
    
    @Autowired
    private StringRedisTemplate redisTemplate;
    
    private static final String DEDUCT_STOCK_SCRIPT =
        "local stockKey = KEYS[1]\n" +
        "local deductQty = tonumber(ARGV[1])\n" +
        "local userId = ARGV[2]\n" +
        "local soldSetKey = ARGV[3]\n" +
        "local isBought = redis.call('SISMEMBER', soldSetKey, userId)\n" +
        "if tonumber(isBought) == 1 then return -2 end\n" +
        "local stock = tonumber(redis.call('GET', stockKey))\n" +
        "if stock == nil then return -3 end\n" +
        "if stock >= deductQty then\n" +
        "    redis.call('DECRBY', stockKey, deductQty)\n" +
        "    redis.call('SADD', soldSetKey, userId)\n" +
        "    return stock - deductQty\n" +
        "else\n" +
        "    return -1\n" +
        "end\n";
    
    private final RedisScript<Long> deductStockScript = 
        new DefaultRedisScript<>(DEDUCT_STOCK_SCRIPT, Long.class);
    
    /**
     * 扣减库存(原子操作)
     */
    public StockResult deductStock(Long activityId, Long userId, int quantity) {
        String stockKey = "stock:" + activityId;
        String soldSetKey = "sold:" + activityId;
        
        Long result = redisTemplate.execute(
            deductStockScript,
            Arrays.asList(stockKey, soldSetKey),
            String.valueOf(quantity),
            String.valueOf(userId)
        );
        
        if (result == null) return StockResult.error("系统异常");
        
        switch (result.intValue()) {
            case -1: return StockResult.fail("库存不足");
            case -2: return StockResult.fail("您已购买过");
            case -3: return StockResult.fail("活动不存在");
            default: return StockResult.success(result.intValue());
        }
    }
}

3.2 场景二:分布式锁(防死锁)

lua 复制代码
-- distributed_lock.lua
-- KEYS[1]: 锁key
-- ARGV[1]: 锁值(唯一标识)
-- ARGV[2]: 过期时间(毫秒)

local lockKey = KEYS[1]
local lockValue = ARGV[1]
local expireTime = tonumber(ARGV[2])

-- 尝试加锁(NX:不存在才设置,PX:毫秒过期)
local result = redis.call('SET', lockKey, lockValue, 'NX', 'PX', expireTime)
if result then
    return 1  -- 加锁成功
else
    return 0  -- 加锁失败(锁已被占用)
end
lua 复制代码
-- unlock.lua
-- KEYS[1]: 锁key
-- ARGV[1]: 锁值(必须匹配才能释放,防止释放别人的锁)

local lockKey = KEYS[1]
local lockValue = ARGV[1]

-- 比较锁值,匹配才删除(Lua保证原子性)
if redis.call('GET', lockKey) == lockValue then
    return redis.call('DEL', lockKey)
else
    return 0  -- 不是自己的锁,不能释放
end
java 复制代码
/**
 * Redis分布式锁(Lua脚本实现)
 */
@Component
@Slf4j
public class RedisDistributedLock {
    
    @Autowired
    private StringRedisTemplate redisTemplate;
    
    private static final String LOCK_SCRIPT =
        "local result = redis.call('SET', KEYS[1], ARGV[1], 'NX', 'PX', ARGV[2])\n" +
        "if result then return 1 else return 0 end\n";
    
    private static final String UNLOCK_SCRIPT =
        "if redis.call('GET', KEYS[1]) == ARGV[1] then\n" +
        "    return redis.call('DEL', KEYS[1])\n" +
        "else\n" +
        "    return 0\n" +
        "end\n";
    
    private final RedisScript<Long> lockScript = 
        new DefaultRedisScript<>(LOCK_SCRIPT, Long.class);
    
    private final RedisScript<Long> unlockScript = 
        new DefaultRedisScript<>(UNLOCK_SCRIPT, Long.class);
    
    /**
     * 尝试加锁
     */
    public boolean tryLock(String key, String value, long expireMs) {
        Long result = redisTemplate.execute(
            lockScript,
            Collections.singletonList(key),
            value,
            String.valueOf(expireMs)
        );
        return Long.valueOf(1L).equals(result);
    }
    
    /**
     * 释放锁(只有锁的持有者才能释放)
     */
    public boolean unlock(String key, String value) {
        Long result = redisTemplate.execute(
            unlockScript,
            Collections.singletonList(key),
            value
        );
        return Long.valueOf(1L).equals(result);
    }
}

3.3 场景三:滑动窗口限流

lua 复制代码
-- sliding_window_rate_limit.lua
-- KEYS[1]: 限流key(ZSet)
-- ARGV[1]: 窗口大小(毫秒)
-- ARGV[2]: 窗口内最大请求数
-- ARGV[3]: 当前时间戳(毫秒)
-- ARGV[4]: 请求唯一ID

local key = KEYS[1]
local windowSize = tonumber(ARGV[1])
local limit = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local requestId = ARGV[4]
local windowStart = now - windowSize

-- 1. 清除窗口外的旧记录
redis.call('ZREMRANGEBYSCORE', key, 0, windowStart)

-- 2. 获取当前窗口内的请求数
local current = redis.call('ZCARD', key)

-- 3. 判断是否超过限制
if tonumber(current) < limit then
    -- 4. 添加当前请求
    redis.call('ZADD', key, now, requestId)
    -- 5. 设置过期时间(防止内存泄漏)
    redis.call('PEXPIRE', key, windowSize)
    return 1  -- 允许通过
else
    return 0  -- 限流
end
java 复制代码
/**
 * 滑动窗口限流器
 */
@Component
public class SlidingWindowRateLimiter {
    
    @Autowired
    private StringRedisTemplate redisTemplate;
    
    private static final String RATE_LIMIT_SCRIPT =
        "local key = KEYS[1]\n" +
        "local windowSize = tonumber(ARGV[1])\n" +
        "local limit = tonumber(ARGV[2])\n" +
        "local now = tonumber(ARGV[3])\n" +
        "local requestId = ARGV[4]\n" +
        "local windowStart = now - windowSize\n" +
        "redis.call('ZREMRANGEBYSCORE', key, 0, windowStart)\n" +
        "local current = redis.call('ZCARD', key)\n" +
        "if tonumber(current) < limit then\n" +
        "    redis.call('ZADD', key, now, requestId)\n" +
        "    redis.call('PEXPIRE', key, windowSize)\n" +
        "    return 1\n" +
        "else\n" +
        "    return 0\n" +
        "end\n";
    
    private final RedisScript<Long> rateLimitScript = 
        new DefaultRedisScript<>(RATE_LIMIT_SCRIPT, Long.class);
    
    /**
     * 尝试获取许可
     */
    public boolean tryAcquire(String key, long windowMs, int limit) {
        Long result = redisTemplate.execute(
            rateLimitScript,
            Collections.singletonList(key),
            String.valueOf(windowMs),
            String.valueOf(limit),
            String.valueOf(System.currentTimeMillis()),
            UUID.randomUUID().toString()
        );
        return Long.valueOf(1L).equals(result);
    }
}

3.4 场景四:延迟队列(ZSet实现)

lua 复制代码
-- delayed_queue.lua
-- KEYS[1]: 队列key(ZSet)
-- ARGV[1]: 当前时间戳
-- ARGV[2]: 数量限制

local key = KEYS[1]
local now = tonumber(ARGV[1])
local limit = tonumber(ARGV[2])

-- 获取到期的任务(score <= 当前时间)
local tasks = redis.call('ZRANGEBYSCORE', key, 0, now, 'LIMIT', 0, limit)

if #tasks > 0 then
    -- 批量删除已取出的任务
    redis.call('ZREM', key, unpack(tasks))
end

return tasks
java 复制代码
/**
 * Redis延迟队列
 */
@Component
@Slf4j
public class RedisDelayQueue {
    
    @Autowired
    private StringRedisTemplate redisTemplate;
    
    private static final String POLL_SCRIPT =
        "local key = KEYS[1]\n" +
        "local now = tonumber(ARGV[1])\n" +
        "local limit = tonumber(ARGV[2])\n" +
        "local tasks = redis.call('ZRANGEBYSCORE', key, 0, now, 'LIMIT', 0, limit)\n" +
        "if #tasks > 0 then\n" +
        "    redis.call('ZREM', key, unpack(tasks))\n" +
        "end\n" +
        "return tasks\n";
    
    private final RedisScript<List> pollScript = 
        new DefaultRedisScript<>(POLL_SCRIPT, List.class);
    
    /**
     * 添加延迟任务
     */
    public void addTask(String queue, String taskId, long delayMs) {
        long executeTime = System.currentTimeMillis() + delayMs;
        redisTemplate.opsForZSet().add(queue, taskId, executeTime);
    }
    
    /**
     * 拉取到期任务
     */
    public List<String> pollTasks(String queue, int limit) {
        List<String> tasks = redisTemplate.execute(
            pollScript,
            Collections.singletonList(queue),
            String.valueOf(System.currentTimeMillis()),
            String.valueOf(limit)
        );
        return tasks != null ? tasks : Collections.emptyList();
    }
}

3.5 场景五:排行榜实时更新

lua 复制代码
-- rank_update.lua
-- KEYS[1]: 排行榜key(ZSet)
-- ARGV[1]: 成员ID
-- ARGV[2]: 分数

local key = KEYS[1]
local memberId = ARGV[1]
local score = tonumber(ARGV[2])

-- 更新分数
redis.call('ZADD', key, score, memberId)

-- 获取当前排名
local rank = redis.call('ZREVRANK', key, memberId)

-- 获取Top 10
local top10 = redis.call('ZREVRANGE', key, 0, 9, 'WITHSCORES')

return {rank + 1, top10}

3.6 场景六:幂等性校验

lua 复制代码
-- idempotent_check.lua
-- KEYS[1]: 幂等key
-- ARGV[1]: 请求ID
-- ARGV[2]: 过期时间(秒)

local key = KEYS[1]
local requestId = ARGV[1]
local expireTime = tonumber(ARGV[2])

-- 检查是否已处理
local exists = redis.call('SET', key, requestId, 'NX', 'EX', expireTime)

if exists then
    return 1  -- 首次处理
else
    -- 检查是否是同一个请求
    local value = redis.call('GET', key)
    if value == requestId then
        return 0  -- 重复请求,但幂等
    else
        return -1  -- 冲突请求
    end
end

四、高级进阶:Lua脚本最佳实践

4.1 脚本预加载(EVALSHA)

java 复制代码
/**
 * Lua脚本管理器
 * 预加载脚本,后续使用SHA1调用,提升性能
 */
@Component
@Slf4j
public class LuaScriptManager {
    
    @Autowired
    private StringRedisTemplate redisTemplate;
    
    // 脚本缓存:脚本名 → SHA1
    private final ConcurrentHashMap<String, String> scriptShaCache = new ConcurrentHashMap<>();
    
    /**
     * 注册脚本(预加载到Redis)
     */
    public void registerScript(String name, String script) {
        String sha1 = redisTemplate.execute(
            (RedisCallback<String>) connection -> 
                connection.scriptLoad(script.getBytes())
        );
        scriptShaCache.put(name, sha1);
        log.info("✅ Lua脚本注册成功: name={}, sha1={}", name, sha1);
    }
    
    /**
     * 执行已注册的脚本(使用EVALSHA,性能更高)
     */
    public <T> T executeScript(String name, Class<T> returnType, 
                                 List<String> keys, String... args) {
        String sha1 = scriptShaCache.get(name);
        if (sha1 == null) {
            throw new IllegalArgumentException("脚本未注册: " + name);
        }
        
        RedisScript<T> script = new DefaultRedisScript<>(sha1, returnType);
        return redisTemplate.execute(script, keys, args);
    }
    
    @PostConstruct
    public void init() {
        // 预加载所有脚本
        registerScript("deduct_stock", DEDUCT_STOCK_SCRIPT);
        registerScript("lock", LOCK_SCRIPT);
        registerScript("unlock", UNLOCK_SCRIPT);
        registerScript("rate_limit", RATE_LIMIT_SCRIPT);
    }
}

4.2 Lua脚本调试技巧

java 复制代码
/**
 * Lua脚本调试工具
 */
@Component
@Slf4j
public class LuaScriptDebugger {
    
    @Autowired
    private StringRedisTemplate redisTemplate;
    
    /**
     * 调试Lua脚本(在Redis中执行并返回详细结果)
     */
    public Object debugScript(String script, List<String> keys, String... args) {
        log.info("=== Lua脚本调试开始 ===");
        log.info("脚本:\n{}", script);
        log.info("KEYS: {}", keys);
        log.info("ARGV: {}", Arrays.asList(args));
        
        long start = System.currentTimeMillis();
        
        try {
            Object result = redisTemplate.execute(
                new DefaultRedisScript<>(script, Object.class),
                keys,
                args
            );
            
            long cost = System.currentTimeMillis() - start;
            log.info("结果: {}", result);
            log.info("耗时: {}ms", cost);
            log.info("=== Lua脚本调试结束 ===");
            
            return result;
            
        } catch (Exception e) {
            log.error("❌ Lua脚本执行失败", e);
            return null;
        }
    }
}

五、预判问题与解答

Q1:Lua脚本会不会阻塞Redis?

A

复制代码
Lua脚本执行是原子的,会阻塞Redis:

1. 执行时间短(< 5ms):
   - 对Redis影响很小
   - 绝大多数Lua脚本都在这个范围内

2. 执行时间长(> 5ms):
   - 会阻塞其他命令
   - 影响Redis吞吐量

3. 最佳实践:
   - Lua脚本尽量简短
   - 避免在Lua中执行循环(特别是大循环)
   - 避免在Lua中调用KEYS命令(改用SCAN)
   - 使用EVALSHA减少脚本传输时间

4. Redis 7.0+:
   - 支持Lua脚本中断(SCRIPT KILL)
   - 支持函数(Function)替代脚本

Q2:Lua脚本和Redis事务(MULTI/EXEC)有什么区别?

A

特性 Lua脚本 MULTI/EXEC
原子性 ✅ 原子执行 ✅ 原子执行
条件判断 ✅ 支持 ❌ 不支持
循环 ✅ 支持 ❌ 不支持
网络往返 1次 N次
复用性 EVALSHA缓存 每次都要传
错误处理 返回错误值 整个事务回滚

结论:需要条件判断或复杂逻辑时用Lua脚本,简单的原子操作可以用MULTI/EXEC。

Q3:Lua脚本怎么处理错误?

A

lua 复制代码
-- Lua脚本错误处理
local stockKey = KEYS[1]

-- 方式1:pcall捕获异常
local ok, stock = pcall(redis.call, 'GET', stockKey)
if not ok then
    return {err = "GET失败: " .. tostring(stock)}
end

-- 方式2:返回错误码
if stock == nil then
    return {code = -1, msg = "库存不存在"}
end

-- 方式3:使用redis.error_reply
if tonumber(stock) < 0 then
    return redis.error_reply("库存不能为负数")
end

Q4:Lua脚本中可以使用哪些Redis命令?

A

复制代码
可用命令:
- 所有Redis命令都可以在Lua中使用
- 但有一些限制:

限制:
1. 不能使用SCRIPT命令(递归调用脚本)
2. 不能使用MULTI/EXEC(脚本本身就是原子的)
3. 不能使用DEBUG命令
4. 不能使用RANDOMKEY(因为脚本需要确定性)

推荐使用的命令:
- GET/SET/DEL(基础操作)
- ZADD/ZREM/ZRANGEBYSCORE(有序集合)
- SADD/SREM/SISMEMBER(集合)
- INCR/DECR/INCRBY(计数器)
- EXPIRE/PEXPIRE/TTL(过期时间)
- HSET/HGET/HGETALL(哈希)

Q5:Lua脚本在集群模式下有什么限制?

A

复制代码
集群模式限制:

1. 所有KEY必须在同一个槽位:
   - 使用{hashtag}确保KEY在同一个节点
   - 如:KEYS[1] = "order:{123}",KEYS[2] = "stock:{123}"

2. 不能跨槽操作:
   - Lua脚本中的所有KEY必须在同一个节点
   - 否则报错:CROSSSLOT Keys in request don't hash to the same slot

3. 最佳实践:
   - 使用{hashtag}前缀
   - 在KEY命名时统一加上业务前缀
   - 如:seckill:{activityId}:stock 和 seckill:{activityId}:sold

六、面试高频考点

考点1:为什么Lua脚本能保证原子性?

参考答案

复制代码
原子性原理:

1. Redis单线程模型:
   - Redis使用单线程处理命令
   - 一次只能执行一个命令
   - Lua脚本作为一个整体被执行

2. 脚本执行期间不会被中断:
   - Redis在执行Lua脚本时
   - 不会处理其他客户端的命令
   - 直到脚本执行完成

3. 等价于MULTI/EXEC:
   - 但比MULTI/EXEC更强大
   - 支持条件判断和循环

4. 注意事项:
   - 脚本执行时间不宜过长
   - 否则会阻塞其他命令

考点2:库存扣减的Lua脚本怎么写?

参考答案

复制代码
核心逻辑:

1. 检查是否已购买(SISMEMBER)
2. 获取当前库存(GET)
3. 判断库存是否足够
4. 扣减库存(DECRBY)
5. 记录已购买用户(SADD)
6. 返回剩余库存

关键点:
- 使用SISMEMBER防止重复购买
- 先判断再扣减,避免库存变为负数
- 整个操作在一个Lua脚本中完成

考点3:EVAL和EVALSHA的区别?

参考答案

复制代码
EVAL:
- 每次都发送完整脚本
- Redis编译脚本后执行
- 适合一次性使用

EVALSHA:
- 只发送脚本的SHA1
- Redis从缓存中查找已编译的脚本
- 适合重复使用
- 性能更高(减少网络传输)

最佳实践:
- 首次使用EVAL加载脚本
- 后续使用EVALSHA调用
- 应用启动时预加载所有脚本

考点4:Lua脚本在Redis集群中有什么限制?

参考答案

复制代码
核心限制:
- Lua脚本中的所有KEY必须在同一个槽位(同一个节点)
- 不能跨节点操作

解决方案:
- 使用{hashtag}确保KEY在同一个节点
- 如:KEYS[1] = "user:{123}:info",KEYS[2] = "user:{123}:orders"
- {123}部分会被用来计算槽位
- 所有包含相同{hashtag}的KEY都会在同一个节点

七、总结与最佳实践

7.1 核心要点回顾

复制代码
Redis Lua脚本核心知识:

┌─────────────────────────────────────────────────────────────┐
│  1. 原子性保证                                              │
│     ├── Redis单线程执行Lua脚本                               │
│     ├── 不会被其他命令打断                                   │
│     └── 彻底解决竞态条件                                    │
│                                                              │
│  2. 性能优化                                                │
│     ├── 减少网络往返(N次→1次)                              │
│     ├── EVALSHA缓存脚本                                      │
│     └── 脚本尽量简短(< 5ms)                                │
│                                                              │
│  3. 六大经典场景                                            │
│     ├── 库存扣减(判断+扣减+记录)                           │
│     ├── 分布式锁(加锁+解锁,防死锁)                        │
│     ├── 滑动窗口限流(ZSet + 时间窗口)                      │
│     ├── 延迟队列(ZSet + 到期拉取)                         │
│     ├── 排行榜更新(ZSet + 实时排名)                        │
│     └── 幂等性校验(SET NX + 过期)                         │
│                                                              │
│  4. 最佳实践                                                │
│     ├── 使用{hashtag}兼容集群模式                           │
│     ├── 应用启动时预加载脚本                                 │
│     └── 脚本出错时返回错误码而非抛异常                       │
└─────────────────────────────────────────────────────────────┘

7.2 性能提升数据

某电商平台实测数据:

指标 优化前(多次调用) 优化后(Lua脚本) 提升
库存扣减延迟 3-5ms(3次网络往返) 0.5-1ms(1次) 5倍↑
并发安全性 有竞态条件 完全原子 100%↑
超卖率 0.1% 0% 100%↓
Redis QPS 1万(多次调用) 5万(Lua脚本) 5倍↑

八、参考与拓展


互动讨论:你在项目中用过Redis Lua脚本吗?用来解决什么问题?欢迎在评论区分享!

如果本文对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,持续获取更多Java后端技术干货!

相关推荐
swg3213211 小时前
Redis实现主从选举
java·前端·redis
Java 码思客1 小时前
【ElasticSearch 从入门到架构师】第6章_分词器与文本检索
java·elasticsearch
Flittly1 小时前
【AgentScope Java新手村系列】(6)Hook与Middleware
java·spring boot·笔记·spring·ai
向量引擎1 小时前
AI API 正在进入“请求生命周期治理”阶段:从模型迁移、Agent 接入到成本与安全排错的工程化方法
java·人工智能·python·aigc·ai编程·ai写作·gpu算力
IT策士1 小时前
Redis 从入门到精通:分布式锁 —— 从 SETNX 到 Redlock
数据库·redis·分布式
许彰午1 小时前
34_Java设计模式之单例模式
java·单例模式·设计模式
摇滚侠1 小时前
MyBatis 入门到项目实战 IDEA 配置模板 20-22
java·intellij-idea·mybatis
技术小结-李爽1 小时前
【工具】Maven二进制包目录结构说明
java·maven
zyl837211 小时前
前后端高并发解决方案
java·redis