Bamboo 调度系统 OceanBase 适配实战:存储过程迁移踩过的三个大坑

Bamboo 调度系统 OceanBase 适配实战:存储过程迁移踩过的三个大坑

本文记录了分布式任务调度平台 Bamboo(竹节) 从 MySQL 适配 OceanBase 的完整实战过程。 由于 Bamboo 的调度引擎完全由 MySQL 存储过程 + 数据库事件驱动,这次适配的"主战场"就在存储过程上。 踩过三个大坑:锁、临时表、子查询 BUG,逐一复盘现象、根因与最终方案,希望帮后来者少走弯路。


一、背景:为什么 Bamboo 适配 OceanBase 这么"痛"?

先简单介绍下 Bamboo:一个自研的分布式任务调度平台,核心设计理念是 Simple is beautiful,架构上与 XXL-JOB、DolphinScheduler 完全不同:

  • 调度引擎不在 Java 里,而在 MySQL 存储过程 + 数据库事件里。时间窗预生成、Cron 匹配、任务展开、流程树状态机、超时取消、负载均衡派发,全部由 13 个存储过程(12 个业务过程 + 1 个加锁模板)+ 5 个数据库事件完成;Java 侧只负责数据 CRUD 和展示。
  • 执行器是 Pull 模式,主动到库里拉自己的任务,天然支持多实例、免内网穿透。
  • 所有变更走 工单审批,审批通过前不碰在用数据,企业级合规管控。

一句话:存储过程就是 Bamboo 的发动机。

所以当我们在国产数据库 OceanBase 上跑这套 SQL 时,MySQL 上一切正常的过程,直接暴露出了三个方向的兼容性问题。

1.1 测试环境说明

项 MySQL OceanBase
版本 5.7.44(社区版) 5.7.25-OceanBase_CE-v4.5.0.0(社区版,MySQL 兼容模式)
接入方式 默认 3306 直连 OB 内核的 MySQL 协议端口 2881(未经过 ODP/obproxy 代理)
客户端 Navicat / MySQL CLI ODC(OceanBase Developer Center)

特别说明:本文结论均基于**直连 OB 内核(2881)**的场景实测。社区里不少 OceanBase 兼容性问题的成因与 ODP(obproxy)代理层配置相关,走代理的同学需要另行验证。


二、坑一:GET_LOCK / RELEASE_LOCK 用不了

2.1 现象

Bamboo 的存储过程由数据库事件每 10 秒 / 30 秒循环触发,多节点部署时靠 MySQL 的咨询锁(advisory lock)函数互斥,防止两个节点同时跑同一个过程:

sql 复制代码
SELECT GET_LOCK(v_proc_name, 0) INTO lock_flag;   -- 抢锁
... 业务逻辑 ...
SELECT RELEASE_LOCK(v_proc_name);                 -- 释放锁

这套代码在 MySQL 5.7/8.0 上运行良好。切到 OceanBase CE 4.5.0.0(直连 2881)后,GET_LOCK/RELEASE_LOCK 直接报错(提示该特性/函数不支持),调度链整体瘫痪。

说明:OceanBase 官方文档虽在锁函数清单中列出了这两个函数,但社区大量反馈其可用性与版本、接入方式(是否走 ODP)相关。对我们来说结论只有一个:作为跨数据库产品,不能把调度互斥押在一个"可能修好"的函数上,彻底绕开才是正解。

2.2 解决方案:用一张表做分布式锁

我们设计了一张 bs_lock 表,用「主键冲突」来模拟抢锁互斥:

sql 复制代码
-- 分布式锁表:lock_name 即锁名,主键唯一
CREATE TABLE `bs_lock` (
  `lock_name`   VARCHAR(128) NOT NULL COMMENT '锁名称,对应 v_proc_name',
  `lock_time`   datetime(6)  NOT NULL COMMENT '本次加锁时间',
  `expire_time` datetime(6)  NOT NULL COMMENT '锁过期时间,超过时间自动释放',
  PRIMARY KEY (`lock_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

统一加锁模板(每个带锁过程开头的固定动作):

sql 复制代码
DECLARE v_begin_time datetime(6) DEFAULT now(6);   -- 本次锁的时间戳(加锁前先取好)

-- 抢锁:先清理过期锁,再插入本过程的锁
delete from bs_lock where lock_name = v_proc_name and expire_time < v_begin_time;
insert into bs_lock(lock_name, lock_time, expire_time)
    value(v_proc_name, v_begin_time, DATE_ADD(v_begin_time, INTERVAL 120 SECOND));
-- 主键冲突 → insert 直接异常 → 进入 EXIT HANDLER

释放锁的代码与异常处理配套设计,这是整个方案的精髓所在:

sql 复制代码
DECLARE lock_flag INT DEFAULT 0;

-- 异常统一收口:先尝试释放本次锁,再写日志、抛错
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
    -- 只能删除本次锁:用 lock_time 加以限定,绝不误删别人的锁
    delete from bs_lock where lock_name = v_proc_name and lock_time = v_begin_time;
    SET lock_flag = ROW_COUNT();

    IF lock_flag = 1 THEN
        set v_message_detail = CONCAT_WS(' ', v_message_detail, '执行失败,已释放锁');
    ELSE
        set v_message_detail = CONCAT_WS(' ', v_message_detail, '执行失败,获取锁失败, 可能存在并发任务');
    END IF;
    insert into bs_bsp_log(begin_time, proc_name, msg_code, msg_detail)
        value (v_begin_time, v_proc_name, '99999', v_message_detail);
    SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = v_message_detail;
END;

2.3 三个关键设计点(及两个边界)

  1. 主键冲突即抢锁失败 :INSERT 主键冲突会抛异常,天然进入 EXIT HANDLER,不需要 IF 判断,代码极简。
  2. 用 lock_time 区分"我删的是谁的锁" :异常处理器里无条件执行一次 DELETE ... WHERE lock_name = ? AND lock_time = v_begin_time,再用 ROW_COUNT() 判别------
    • 删到 1 行:说明锁是自己加的,业务逻辑中途出错,锁已释放;
    • 删到 0 行:说明锁根本是别人的(抢锁那一刻就冲突了),日志里明确写出"获取锁失败,可能存在并发任务"。 一个小技巧解决了"异常是发生在抢锁前还是抢锁后"这个判断难题。
  3. 过期锁自愈 :过程持有锁时进程被杀(比如节点宕机),锁会残留。下一次抢锁前先 delete ... expire_time < now,过期锁自动清理,不需要看门狗线程。

两个需要知道的边界:

  • TTL 语义差异 :MySQL 的 GET_LOCK 无过期时间,bs_lock 带 120 秒 TTL------单次执行超过 TTL 的过程会失去互斥保护。Bamboo 的业务过程都是秒级到几十秒的批量 UPDATE,实测不受影响;如果你的过程可能长时间运行,要么延长 TTL,要么做续期。
  • 删到 0 行的另一种可能:理论上"自己的锁已过期被别的节点清走"也会表现为删 0 行,此时日志会误报为"获取锁失败"。实操中 Bamboo 过程远短于 TTL,不会触发,但排查问题时值得知道。

这套模板覆盖了全部 9 个带锁业务过程(另有一个 safe_procedure 作为新过程的复制起点),在 MySQL 和 OceanBase 上行为一致。


三、坑二:CREATE TEMPORARY TABLE ... ENGINE=MEMORY 创建失败

3.1 现象

Bamboo 的过程里有不少中间结果集处理(比如流程树状态机里"找出本轮需要收尾的 GROUP"、"统计每个 run 下各子任务的完成情况"),MySQL 版本用的是内存临时表:

sql 复制代码
CREATE TEMPORARY TABLE tmp_runs_to_finish (
    id BIGINT PRIMARY KEY,
    new_status INT
) ENGINE=MEMORY;

INSERT INTO tmp_runs_to_finish (id, new_status)
SELECT p.id, ... FROM bs_run_item p ... GROUP BY p.id;

UPDATE bs_run_item r
INNER JOIN tmp_runs_to_finish t ON t.id = r.id
SET r.status = t.new_status;

DROP TEMPORARY TABLE IF EXISTS tmp_runs_to_finish;

切到 OceanBase CE 4.5.0.0 后,CREATE TEMPORARY TABLE ... ENGINE=MEMORY 直接报错 ,而且创建失败后后续所有依赖该临时表的 INSERT/UPDATE 一路抛错,异常处理器里的 DROP TEMPORARY TABLE 清理也随之失效。

说明:OceanBase 对临时表的支持随版本与接入方式差异很大(社区大量反馈与 ODP 代理配置相关,且 OB 的存储引擎是统一的 LSM-Tree,本就没有 ENGINE=MEMORY 概念)。我们没有去逐一验证"临时表本身能不能用",而是做了产品级决策:中间结果集不依赖临时表,一劳永逸。

3.2 解决方案:普通表 + session_id 会话隔离

既然临时表靠不住,我们改用普通物理表 + UUID 会话隔离 :每张中间表加一列 session_id,过程开头 UUID() 生成本次调用的会话 ID,所有读写都带上这个条件,用完即删。多节点并发调用同名字的过程也互不干扰(何况还有坑一的锁兜底)。

sql 复制代码
-- 替代临时表的中间表(bs_bsp_ 前缀,bsp = bamboo stored procedure)
CREATE TABLE bs_bsp_runs_to_finish (
    `session_id` VARCHAR(64) NOT NULL COMMENT 'UUID(),会话隔离',
    id BIGINT,
    new_status INT,
    `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间(排查与残留清理用)'
);

过程内写法:

sql 复制代码
DECLARE v_session_id VARCHAR(64) DEFAULT UUID();

-- 写入:带上本会话 ID
INSERT INTO bs_bsp_runs_to_finish (`session_id`, id, new_status)
SELECT v_session_id, p.id, ...
FROM bs_run_item p ...
GROUP BY p.id;

-- 读取:同样限定会话
UPDATE bs_run_item r
INNER JOIN bs_bsp_runs_to_finish t
        ON t.id = r.id AND t.session_id = v_session_id
SET r.status = t.new_status;

-- 用完清理本会话数据
DELETE FROM bs_bsp_runs_to_finish WHERE session_id = v_session_id;

异常处理器里同样清理,保证任何路径都不残留脏数据:

sql 复制代码
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
    -- 清理 bs_bsp 表(本会话数据)
    DELETE FROM bs_bsp_task_done WHERE session_id = v_session_id;
    DELETE FROM bs_bsp_runs_to_finish WHERE session_id = v_session_id;
    DELETE FROM bs_bsp_circuit_break_runs WHERE session_id = v_session_id;
    ...
END;

极端情况(进程被 kill、HANDLER 未执行)下的残留行,可凭 create_time 排查并定期清理,不影响正确性(每轮读写都限定本会话)。

最终 6 张中间表按这个模式落地:bs_bsp_circuit_break_runs、bs_bsp_runs_to_finish、bs_bsp_task_done、bs_bsp_assign、bs_bsp_batch_load(主键 session_id + instance_id)、bs_bsp_trigger_candidates。

小提示:bs_bsp_batch_load 这类有查询需求的表建议把 session_id 加进主键或索引,避免全表扫描。


四、坑三:JOIN ON 里的子查询/派生表表达式------OceanBase 返回错误结果集(疑似 BUG)

4.1 现象:同一条 SQL,MySQL 和 OceanBase 结果集不一样

这是最隐蔽、排查成本最高的一个坑。bsp_run_proc(调度计划 → 生成执行实例)里有一条核心 INSERT...SELECT,每个计划需要从"已有执行实例的最大触发时间"(续跑点)之后继续生成新实例------避免重复生成历史数据:

sql 复制代码
-- 原写法:JOIN ON 条件里,COALESCE 函数参数内嵌相关子查询
SELECT ep.id, t.time_value
FROM bs_schedule ep
INNER JOIN bs_time t
  ON t.time_value > COALESCE(
       (SELECT GREATEST(MAX(pt.schedule_trigger_time), NOW())
          FROM bs_run pt
         WHERE pt.schedule_id = ep.id),
       NOW())
WHERE ep.status = 1
  AND ep.schedule_type = 2
  AND (ep.second = '*' OR FIND_IN_SET(t.second, REPLACE(ep.second, ' ', '')))
  AND (ep.minute = '*' OR FIND_IN_SET(t.minute, REPLACE(ep.minute, ' ', '')))
  ...;

同一条 SQL、同一份数据,两个库的结果(复现数据中 bs_run 为空表,子查询 MAX() 为 NULL、续跑点回落到"当前时间";为使结果可复现,脚本将基准时间固定为 2026-10-05 09:40:00):

MySQL 5.7.44 ------ 3 行,正确:

ini 复制代码
OB测试A       09:42:00   ✓(second=0, minute=42 均命中)
OB测试A       09:52:00   ✓
深层流程测试   10:00:30   ✓(second=30, minute=0 均命中)

OceanBase 5.7.25-OceanBase_CE-v4.5.0.0 ------ 4 行:多出 2 行错误数据,同时漏掉 1 行正确数据:

ini 复制代码
OB测试A       09:42:00   ✓
OB测试A       09:52:00   ✓
深层流程测试   09:42:00   ✗ 该计划 second='30'、minute='0,30',这行一个条件都不满足
深层流程测试   09:52:00   ✗ 同上
(正确行 10:00:30 反而消失了)

多出来的行连 WHERE 里的 second/minute 匹配条件都不满足 ------这不是兼容性差异能解释的,基本可以判定是 OceanBase 优化器对这种形态的 SQL 做了错误的重写。我们把最小复现脚本整理成中英文两份(doc/ocean-base-bug.sql / doc/ocean-base-bug-en.sql,因OceanBase Issue 在github 上只接受全英文,所以才做了中英两份文档)随项目开源;也曾尝试向 OceanBase 官方提交 issue,因提交平台报错未能发出(截图保留在仓库 doc/ocean-base-issue-create-error.png),欢迎有官方渠道的同学协助反馈跟进。

4.2 排查过程:连"标准答案"都是错的

第一反应当然是教科书式的改写------把子查询改成派生表 LEFT JOIN:

sql 复制代码
-- 方式 2:LEFT JOIN 聚合派生表,函数参数里已经没有子查询了
LEFT JOIN (
    SELECT schedule_id, MAX(schedule_trigger_time) AS max_trigger_time
    FROM bs_run
    GROUP BY schedule_id
) pt_max ON pt_max.schedule_id = ep.id
INNER JOIN bs_time t
  ON t.time_value > COALESCE(GREATEST(pt_max.max_trigger_time, NOW()), NOW())

结果:MySQL 结果集与方式 1 一致,OceanBase 依然返回错误结果集 。说明问题不在"子查询"本身,而是 OceanBase 优化器对「JOIN ON 条件里的函数表达式 + 内联视图(派生表)列」这一整类形态的处理有 BUG。

继续试方式 3------把聚合结果物化成一张真实的表:

sql 复制代码
-- 方式 3:聚合结果先落到普通表,再 JOIN 比较
DELETE FROM bs_bsp_schedule_max;
INSERT INTO bs_bsp_schedule_max(id, max_trigger_time)
SELECT s.id, COALESCE(GREATEST(pt_max.max_trigger_time, NOW()), NOW())
FROM bs_schedule s
LEFT JOIN (
    SELECT schedule_id, MAX(schedule_trigger_time) AS max_trigger_time
    FROM bs_run
    GROUP BY schedule_id
) pt_max ON pt_max.schedule_id = s.id;

SELECT ep.id, t.time_value
FROM bs_schedule ep
INNER JOIN bs_bsp_schedule_max pt_max ON pt_max.id = ep.id
INNER JOIN bs_time t
  ON t.time_value > COALESCE(GREATEST(pt_max.max_trigger_time, NOW()), NOW())
WHERE ...;

这次 MySQL 与 OceanBase 的结果集完全一致 。问题定位:只要把复杂表达式的计算从"内联视图列"上挪开,OceanBase 就正常了 。注意方式 3 的 JOIN ON 条件里仍然有 COALESCE(GREATEST(...)) 函数表达式,但此时列来自物理表 bs_bsp_schedule_max(聚合值已在物化阶段算好,这里的 COALESCE 只是冗余保险),OceanBase 处理正常------说明触发 BUG 的形态是「函数表达式 + 内联视图列」,而不是"函数表达式"本身。

4.3 最终改法:三类已验证的替代路径

基于这个结论,我们把全部 5 处同形态写法逐一改造(commit 9e5711d "ocean base left join not work" 及后续收尾,最终代码见 sql/init-create.sql):

① 聚合结果物化成表 (bsp_run_proc):即上面方式 3,bs_bsp_schedule_max 物化续跑点,JOIN 只做列比较。这张表是全程持锁的单例物化表 ------每次运行全量重建即可,不需要 session_id 会话隔离(坑一的 bs_lock 锁保证了同一时刻只有一个过程在跑)。

② 能取列就不算表达式 (bsp_dispatch_proc,负载均衡派发):原写法是 ORDER BY (子查询算已有负载) + COALESCE((SELECT extra_load ...), 0),改为 LEFT JOIN 批次负载物理表后直接取列:

sql 复制代码
SELECT r.`id` INTO v_best_instance
FROM `bs_executor_registry` r
LEFT JOIN `bs_bsp_batch_load` bl
    ON bl.`instance_id` = r.`id` AND bl.`session_id` = v_session_id
WHERE r.`executor_id` = v_eid AND r.`status` = 1
ORDER BY (
    SELECT COUNT(*) FROM `bs_run_item` t2
    WHERE t2.`executor_instance_id` = r.`id`
      AND t2.`handler_id` = v_hid
      AND t2.`status` IN (1, 2, 3, 4)
) + COALESCE(bl.`extra_load`, 0) ASC,   -- 直接取物理表列,不再在函数参数里写子查询
r.`update_time` DESC
LIMIT 1;

③ 配置值顶部 SELECT INTO 变量 (bsp_run_item_proc):原写法在 UPDATE 的 CASE 表达式里 COALESCE((SELECT config_value FROM bs_global_config ...), 300),改为过程开头一次性取到局部变量:

sql 复制代码
DECLARE v_misfire_window INT;

SELECT `config_value` INTO v_misfire_window
FROM `bs_global_config`
WHERE `config_key` = 'misfire_window' AND `status` = 1;
IF v_misfire_window IS NULL THEN SET v_misfire_window = 300; END IF;

-- 后面直接引用变量
UPDATE bs_run SET status = 6, cancel_reason = 'MISFIRE'
WHERE ...
  AND TIMESTAMPDIFF(SECOND, schedule_trigger_time, NOW()) >
      CASE WHEN misfire_window > 0 THEN misfire_window   -- 列:bs_run 自身的失火窗口
           ELSE v_misfire_window END;                     -- 变量:全局配置兜底值

4.4 经验法则(按形态实测结论)

写法形态 OceanBase 实测 结论
函数参数(COALESCE/IFNULL/GREATEST 等)内写子查询 ❌ 结果集错误 禁用
JOIN ON 条件里「函数表达式 + 内联视图(派生表)列」 ❌ 结果集错误 禁用(最隐蔽,务必记住)
JOIN ON 条件里「函数表达式 + 物理表列」 ✅ 正确 可用(聚合值先物化再引用)
ORDER BY / SELECT 列表中的标量子查询 ✅ 正确 可用(见 §4.3 ②)
顶部 SELECT ... INTO 变量 后引用 ✅ 正确 配置类取值首选

给所有往 OceanBase 迁移存储过程的同学的忠告:不要在函数参数里写任何子查询,也尽量不要把函数表达式和内联视图列一起放进 JOIN ON 条件;复杂计算提前物化成表、物理表取列或 SELECT INTO 局部变量 。这些替代写法不仅规避了 BUG,对 bsp_run_proc 这类逐行求值的 JOIN ON 场景性能还更好------原来的写法每一行都要重复执行相关子查询。


五、适配心得总结

回顾整个适配过程,三个坑的共性很有意思:

坑 现象 根因 最终方案
锁 GET_LOCK/RELEASE_LOCK 报错 咨询锁函数在目标环境不可用(可用性与版本/接入方式相关) bs_lock 表锁:主键冲突抢锁 + lock_time 限定解锁 + 过期自愈
临时表 CREATE TEMPORARY TABLE ... ENGINE=MEMORY 创建失败 临时表支持随版本/接入方式差异大,ENGINE=MEMORY 无对应概念 普通表 + session_id=UUID() 会话隔离,EXIT HANDLER 双保险清理
子查询 同一 SQL 结果集不同,多出错误行、漏掉正确行 OceanBase 优化器对「函数表达式 + 内联视图列」形态疑似重写 BUG 物化表 / 物理表取列 / SELECT INTO 变量

几点体会及建议:

  1. "完全兼容 MySQL" 要打引号 。OceanBase 的 MySQL 兼容模式覆盖了大部分 SQL 语法,但存储过程是重灾区:咨询锁、临时表、优化器重写行为,都是迁移前不容易想到的暗礁。凡是把核心逻辑写在存储过程里的系统,迁移前务必把过程全量在目标库跑一遍。
  2. "教科书式改写"不一定对。方式 2(LEFT JOIN 派生表)是社区标准答案,在 OceanBase 上照样翻车。遇到结果集不一致,别急着改业务逻辑,先用最小复现锁定数据库差异,逐种替代写法验证。
  3. 把 BUG 复现脚本留在仓库里。我们把中英文复现脚本都提交进了项目仓库(结果集对比记录在英文版脚本中),既方便日后反馈跟进,也成了团队新人最好的"避坑教材"。
  4. OB 前置检查别忘了 :OceanBase 上 event_scheduler 默认是关闭的,需要手动开启(SET GLOBAL event_scheduler = ON);且数据库事件连续失败达到阈值可能被自动停用------迁移完一定要检查调度事件是否真的在跑。
  5. 使用obclient建表和存储过程:亲测在ODC上运行脚本一次建多个存储过程会失败,建议用 obclient 建表和存储过程。命令行样例:obclient -h127.0.0.1 -P2881 -uroot@sys -p'your-password' < /mnt/my_share/share/init-create.sql

六、关于 Bamboo

最后按惯例安利一下项目。Bamboo(竹节)是一个数据库驱动的分布式任务调度平台,主打轻量极简、零额外依赖 :不需要 Redis、ZooKeeper,仅需 MySQL/OceanBase + 一个 Spring Boot 应用(OB 上记得开启 event_scheduler,见上文)即可拥有完整的分布式调度能力。

  • 数据库驱动调度:调度逻辑全部在存储过程 + 事件中,所有调度状态持久化,可直观预览未来时间窗口的全部调度计划;
  • Flow Tree 树形编排:以"阶段串行、同阶段并发"替代复杂 DAG,表单化配置,零学习成本;
  • Pull 模式执行器:执行器自取任务,天然支持多实例、广播分片,Java 侧可做到零依赖;
  • 工单制变更审批:编辑-审批-执行三步隔离,多级审批、变更快照、全程审计,适配生产合规要求;
  • 运行干预双人复核:手动触发、取消、暂停、恢复等操作全部双人复核留痕。

目前已完成 MySQL 5.7/8.0 与 OceanBase CE 4.5 双数据库适配,下一步计划往达梦、人大金仓等国产数据库继续迁移,支持国货。项目基于 Apache 2.0 协议开源,代码结构简单,欢迎 Star、试用、提 PR 共建!

项目地址:gitee.com/winterzhong...


附:参考资料

  • OceanBase 子查询 BUG 最小复现脚本(中文,含两份结果集对比):ocean-base-bug.sql
  • OceanBase 子查询 BUG 最小复现脚本(英文,含两份结果集对比):ocean-base-bug-en.sql
  • 全部存储过程源码:sql/init-all.sql
  • Bamboo 项目介绍:《轻量极简、零额外依赖!自研分布式任务调度 Bamboo 系统深度介绍》
相关推荐
龙亘川2 小时前
数智驱动民政升级 精准守护民生保障
大数据·数据库·人工智能
念何架构之路2 小时前
zap扩展生态与总结
java·前端·数据库
晚安日记wanna3 小时前
MySQL 回表为什么这么慢?5 种优化手段逐个拆解
数据库·mysql·性能优化
第七页独白3 小时前
汽车零件厂如何通过 QMS 真正落地 IATF 16949——QMS软件系统:品质检验-内审稽核-8d客诉管理:全星质量管理软件系统
java·前端·数据库
晚安日记wanna3 小时前
索引建了却不走?7 个失效原因逐层排查
数据库·mysql·性能优化
KID1412号3 小时前
PostgreSQL入门
数据库·postgresql
Elastic 中国社区官方博客3 小时前
使用 Lucene 搜索你的 Bean — 索引
java·大数据·数据库·elasticsearch·搜索引擎·全文检索·lucene
ShineWinsu5 小时前
对于Redis:主从复制的解析
linux·数据库·c++·redis·缓存·面试·主从复制
许彰午5 小时前
03-Linux环境准备依赖包内核参数与用户组
linux·运维·服务器·数据库