简介:这份学习资料围绕OceanBase数据库应用开发基础展开,面向使用或准备使用OceanBase的开发者、DBA及后端工程师,帮助读者快速建立从SQL操作、索引设计到分布式事务与并发控制的核心认知。内容系统覆盖OceanBase基础架构、标准SQL用法、B-tree与哈希索引等索引类型选择、ACID事务模型、行锁与表锁等并发控制机制,并介绍ODBC/JDBC驱动、命令行工具以及Python、Java、PHP等多语言客户端库,同时说明其基于运行时性能数据自动优化执行计划的自适应能力。资料为单份PDF文档,压缩包大小14.27MB,便于通读或随查随用,适合作为入门学习与日常开发参考;已有127人学习下载。对于希望理解分布式关系型数据库开发要点、规避索引误用和并发冲突的读者,这份PDF能够提供较为完整的知识框架与实践指引。
1. OceanBase 应用开发基础:从 PDF 到能跑通的第一个事务
拿到《OceanBase数据库应用开发基础》的人,多半已经在单机数据库上写过业务,正被分布式数据库的“新玩法”卡住。这份 PDF 的价值,不在于罗列函数和语法,而在于把租户、分区、全局索引、分布式事务这些概念,跟实际的建表、写入、查询代码一一对应起来。当你需要把一个订单系统从 MySQL 迁到 OceanBase,或是从零给新业务选型,照着这份资料走一遍,能少走几个月的弯路。适合正在做选型评估、应用迁移,或者刚接手 OceanBase 项目维护的应用开发者。内容不厚,但每个章节都冲着解决具体问题去。
2. 先理清架构再写代码:存储模型、事务边界与连接配置
2.1 从 MySQL 到 OceanBase:哪些经验能平移,哪些得推倒重来
大多数人接触 OceanBase 的第一反应是“这不就是 MySQL 吗”,因为它的 SQL 语法高度兼容,JDBC 驱动也沿用 MySQL 协议,很多老代码换个连接串就能跑。这个印象一半对一半错。SQL 层面确实能平移,但数据库内核是另一套东西:底层用 LSM-Tree 组织存储,写入先进内存再异步落盘,读路径要考虑多个副本版本;事务是全局时钟推进的,不再是一个单点数据库的本地锁。我一般会把“语法能平移”和“性能模型不能平移”这两件事拆开看,前者决定你能多快跑起来,后者决定你什么时候翻车。
能平移的部分:SQL 语法、JDBC 接口、事务的 ACID 语义、常见的数据类型,以及大部分 ORM 框架(MyBatis、Hibernate 这类只要不依赖数据库私有方言,基本不用改)。如果你的业务 SQL 是标准的 INSERT/SELECT/UPDATE/DELETE 加上普通索引,迁移成本很低。
不能平移的部分:性能调优思路。单机 MySQL 里加个索引往往立竿见影,OceanBase 里索引要不要生效,还取决于分区键、全局索引与本地索引的差别。另一个典型是分页查询,MySQL 里 offset 大了无非慢一点,OceanBase 里如果没按分区键过滤,深分页会触发全分区扫描,慢到让你怀疑集群挂了。这不是 SQL 写错了,而是对分布式执行引擎的惯性误判。
所以读这份 PDF 时,我建议先把“架构差异”那一章看两遍,再动手写代码。你在 MySQL 上的经验是资产,但资产里也藏着包袱。
2.2 租户、库、表三层模型:应用连上的到底是个什么“数据库”
OceanBase 的逻辑结构是租户、库、表三层,理解租户是理解一切的起点。一个集群里可以创建多个租户,每个租户相当于一个独立的数据库实例,资源(CPU、内存)在租户间隔离。租户内部再去建库建表。你写应用时连接的数据库,实际上是某个租户下的某个库,而不是整个集群。
第一次接触时很容易把“租户”类比成 MySQL 的 schema,但两者隔离级别差很远。租户之间资源是物理隔离的,一个租户把 CPU 打满,另一个租户不太受牵连;schema 之间则完全共享资源。我在帮别人排查问题时就遇到过,一个团队把多个应用塞进同一个租户,结果某个报表任务把资源吃光,交易链路跟着抖,最后只能拆租户。这个问题完全可以在设计阶段避免。
连接时还有一个容易忽略的点:租户是区分模式的。OceanBase 有 MySQL 模式和 Oracle 模式两种租户,SQL 语法、数据类型、系统视图都不一样。PDF 里的示例多数跑在 MySQL 模式租户下,如果你是 Oracle 模式,函数和系统视图名称要改。建租户时就要定好模式,后面很难改。检查一下你手上的业务代码,如果已经在用 MySQL 的 limit 语法,选择 MySQL 模式更顺手。
2.3 客户端接入:JDBC 驱动、连接串与三个容易忽略的参数
PDF 里给了完整的连接方式,这里挑实战里最常用的 Java 接入讲。OceanBase 兼容 MySQL 协议,所以既可以用 OceanBase 官方驱动,也可以用 MySQL Connector/J。我一般建议用官方驱动,因为它在协议兼容层做了额外适配,比如 OB 特有的路由信息透传,是普通 MySQL 驱动不具备的。
连接串的写法大致长这样:
String url = "jdbc:oceanbase://192.168.1.10:2881/test_db?useUnicode=true&characterEncoding=utf8&connectTimeout=3000&socketTimeout=5000&rewriteBatchedStatements=true"; String user = "user@tenant1#cluster_name"; String password = "your_password"; Connection conn = DriverManager.getConnection(url, user, password);这里的用户名比较特别:OceanBase 的用户名是user@tenant#cluster三段式,分别是用户名、租户名、集群名。如果漏掉@tenant,连接会直接报错,让你以为密码错了。connectTimeout和socketTimeout建议显式设置,默认值在弱网环境下会导致应用卡死很久才报错。rewriteBatchedStatements=true在这里先埋个伏笔,后面的批处理章节会专门讲,没有这个参数,批量插入性能可能差十倍以上。
如果你的应用跑在云上或者通过 OBProxy 访问,连接串的端口就是 OBProxy 的端口,不再是直连 observer 的 2881。这个细节排障时特别容易坑到人。
3. 照着 PDF 走一遍:建库、写入、查询与事务控制
3.1 建库建表:主键、分区键和全局索引一起定
很多人在 OceanBase 上建表,习惯性地照搬 MySQL 的建表语句,只改一下引擎关键字,结果表建出来了,跑起查询却慢得离谱。问题往往出在没考虑分区策略。OceanBase 是分布式数据库,数据在内部被拆成多个分区,分散在不同节点上。分区的目的是把数据访问的物理范围缩小,让查询只扫需要的那部分数据。
一个合理的建表语句长这样:
CREATE TABLE t_order ( id BIGINT NOT NULL, user_id BIGINT NOT NULL, order_no VARCHAR(64) NOT NULL, amount DECIMAL(10, 2) NOT NULL, status TINYINT NOT NULL, create_time DATETIME NOT NULL, PRIMARY KEY (id, user_id) ) PARTITION BY HASH(user_id) PARTITIONS 16;这里我把user_id放进了主键,同时用它作为分区键。在 OceanBase 里,分区键必须是主键的子集,否则建表直接报错。这个限制刚开始写很容易踩:比如主键只有id,却想按user_id分区,MySQL 里没问题,OceanBase 会拒绝执行。
分区键选谁,要看业务查询的过滤条件。最常见的查询是WHERE user_id = ?,那按 user_id 哈希分区就能把一次查询路由到单个分区,执行代价最小。如果最常见的查询是WHERE create_time BETWEEN ? AND ?,那就应该用范围分区按时间分。分区键选错,后续所有查询都变成跨分区扫描,索引再强也救不回来。
建表时还有一个关联决策:用本地索引还是全局索引。OceanBase 里默认的索引是本地索引,它只覆盖当前分区内的数据;全局索引则跨所有分区,效果更像 MySQL 的普通索引。查询条件里没有分区键时,本地索引帮不上忙,必须建全局索引。比如按order_no查询订单:
CREATE UNIQUE INDEX uk_order_no ON t_order(order_no) GLOBAL;不加GLOBAL的话,这个唯一索引只在分区内唯一,跨分区就无法保证唯一性,语义直接错了。这是建全局索引最常见的原因之一。
3.2 写入提速:批量插入与事务边界
应用开发里最常见的写入场景是批量落库。有人用一条一条 insert,也有人用 MyBatis 的 foreach 拼一条大 SQL,还有人用addBatch提交。这三种写法在 OceanBase 上的表现差异很大。
先说最不推荐的逐条 insert。每一条 SQL 都是一次网络往返加一次事务提交,耗时主要花在通信和提交上,数据量稍大就慢到没法看。一次插入一千条,光事务提交就要一千次,吞吐完全上不去。
推荐的写法是 PreparedStatement 的批量模式:
String sql = "INSERT INTO t_order (id, user_id, order_no, amount, status, create_time) VALUES (?, ?, ?, ?, ?, ?)"; PreparedStatement ps = conn.prepareStatement(sql); ps.setFetchSize(500); for (OrderDO order : orderList) { ps.setLong(1, order.getId()); ps.setLong(2, order.getUserId()); ps.setString(3, order.getOrderNo()); ps.setBigDecimal(4, order.getAmount()); ps.setInt(5, order.getStatus()); ps.setTimestamp(6, order.getCreateTime()); ps.addBatch(); // 每 500 条提交一次,避免事务太长 if (batchCount % 500 == 0) { ps.executeBatch(); conn.commit(); } } ps.executeBatch(); conn.commit();这里有两个关键点。第一,JDBC 连接串里必须带上rewriteBatchedStatements=true,否则 addBatch 只是循环发送单条 SQL,没有真正合并,性能提升很有限。第二,事务不要开太大。每 500 条到 1000 条提交一次是常见做法,既能减少提交次数,又不会让事务持有锁的时间过长。如果一次性插入十万条再 commit,事务执行时间可能超过 OceanBase 的事务超时阈值,被强制回滚,前功尽弃。
3.3 查询与分页:深分页为什么慢,JOIN 该怎么办
查询这一章,PDF 里花了不小篇幅讲分页。业务系统里最常见的分页写法是LIMIT offset, pageSize,这套写法在 MySQL 里没问题,数据量小也没问题。但一旦表数据到了千万级别,offset 一深,查询就原形毕露:前面的页也要从头扫一遍,扫完再丢掉,代价全耗在无效读上。
在 OceanBase 上,深分页更严重,因为它涉及跨节点数据汇聚。LIMIT 100000, 20这种 SQL,每个分区都要先把自己内部的数据按排序字段排好,取前 100020 条,再送到上层节点做全局归并,最后才取那 20 条。分区越多,参与归并的数据量越大,延迟就越高。
常见做法是改用键集分页,也叫 seek 方法。先拿到上一页最后一条记录的排序键值,下一页查询直接以此为起点:
SELECT id, user_id, amount, create_time FROM t_order WHERE user_id = 123 AND id < ? ORDER BY id DESC LIMIT 20;游标法有两个好处:第一,每次查询的数据范围都是确定的,不随页数加深而变大;第二,它天然利用了主键索引,执行路径稳定。PDF 里建议:如果应用必须支持跳页,就老老实实用全局索引加普通分页;如果只支持上下翻页,键集分页是更好的选择。
JOIN 的写法也有一些注意点。在单机数据库里,关联查询随便写,反正执行器会自己选驱动表。在 OceanBase 里,如果两张表的分区键维度不一致,JOIN 会在传输层做大量数据重分布,慢到不可接受。比如订单表和用户表关联,订单表按 user_id 分区,用户表也按 user_id 分区,JOIN 就可以在分区内完成,速度飞快。如果两张表分区键不一样,尽量让右侧小表先物化成临时表,或者在应用侧先查出小表再拼装,避免分布式 JOIN 的代价。
3.4 事务隔离级别:读已提交与可重复读在业务里的差别
OceanBase 默认的事务隔离级别是读已提交(READ COMMITTED),这是和 MySQL 默认的可重复读(REPEATABLE READ)不一样的。这个差异会导致应用行为变化,尤其是涉及“同一事务内多次查询要看到一致的快照”之类的逻辑。
MySQL 下,一个事务内两次相同的 SELECT 结果是一致的,因为可重复读隔离级别下,事务第一次读就定了快照。换到 OceanBase 默认的读已提交,同一个事务里两次查询可能看到不同的数据,因为每次读都是最新已提交的快照。如果业务代码依赖可重复读的语义(比如先查库存再扣减,中间不允许其他事务改数据),要么在事务里显式加锁,要么把会话隔离级别改成可重复读:
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;从实际经验看,大多数互联网业务用读已提交反而更合适,锁冲突更少,并发更高。PDF 里面也建议:除非业务有明确的快照一致性需求,不然保持默认即可。真正需要关注的是那些从前依赖 MySQL 可重复读来掩盖逻辑问题的老代码,这些代码迁移后会出现偶发的不一致,属于“改数据库才发现业务逻辑有缺陷”的典型情况。
事务还有一个隐藏成本:单事务里写入的数据量越大,内存开销越高。OceanBase 通过内存事务来保证性能,一个大事务会把大量数据放进内存,一旦超过阈值,就会触发落盘或者报错。所以请把大事务拆小,这是 OceanBase 应用开发里一条铁律。
4. 避坑清单:OceanBase 应用开发里的高频翻车点
4.1 事务超时:日志报transaction timeout,数据没生效
现象:应用日志里频繁出现transaction timeout或lock wait timeout,事务被回滚,业务数据缺失,重试后才行。
原因:事务执行时间超过了 OceanBase 的事务超时阈值,默认是 100 秒左右(参数ob_trx_timeout)。常见诱因有两个:一个是大批量写入一个事务不提交,比如一次插十万条数据;另一个是事务里混了耗时的外部调用,比如查完数据库又调下游系统接口,接口超时十几秒,整个事务跟着超时。
解决:把长事务拆成小事务。批量写入按几百条一次提交;事务里不要夹带远程调用。外部调用放到事务开始前或者提交后,用本地消息表或者异步任务解耦。如果业务确实需要长事务,调整租户参数ob_trx_timeout是最后的办法,但那是延长风险,不解决根因。
4.2 连接池参数照搬 MySQL,应用一上线就报连接耗尽
现象:应用启动后运行一段时间,日志抛Can't connect to OceanBase server,后端连接数不够,请求排队,整体延迟飙升。
原因:连接池的maxActive还是 MySQL 时代的几十上百,但 OceanBase 一个 observer 节点的可用连接数受内存限制,单租户默认的总连接数远低于 MySQL。连接池开太大,每个连接占的内存叠加起来,直接把租户内存打满。
解决:连接池大小一般建议设置为核心线程数的 2 到 4 倍,再加一条兜底:连接获取超时时间设短一点(比如 3000ms),宁可快速失败报错,也别让请求排队等连接。同时打开连接池的testOnBorrow或空闲连接检测,避免网络闪断后连接池里的死连接被继续复用。
4.3 分区键选错,查询变成全分区扫描
现象:表数据量不大,但查询特别慢。看执行计划发现扫描的分区数是全量,明明是等值查询,却扫了几个或者几十个分区。
原因:查询条件里没带分区键,数据库没法裁剪分区。比如订单表按 user_id 分区,应用却经常按 status 和 create_time 查询,每次查询都要扫全部分区,节点越多,性能越差。
解决:建分区表之前,先列出业务的核心查询条件。如果高频查询总是命中某个字段,就把那个字段设为分区键。如果多个字段都有高频查询,只能建全局索引,但全局索引维护成本高,要控制在少量必要的场景。避免上来就分区,得想好访问模式。
4.4 批处理不生效,慢在单条网络往返
现象:用 PreparedStatement 的addBatch批量写入,一万条数据入库耗时是按分钟算的,完全达不到预期。
原因:JDBC 连接串里没有开启rewriteBatchedStatements=true。没有这个参数,addBatch只是把 SQL 暂存在客户端,发送时仍然一条一条发,性能没有本质提升,反而多了一层本地组装的开销。
解决:连接串加上rewriteBatchedStatements=true,批处理才会真正合并成多值 INSERT。这里提醒一句,这个参数是 MySQL JDBC 就有的,OceanBase 同样支持。加了参数之后,再配合按批提交,性能通常是数量级的差距。
4.5 全局索引语义被忽略,唯一性约束失效
现象:业务上要求唯一性的字段出现重复数据,但建表时报错信息又没提示。
原因:建唯一索引时没有加GLOBAL关键字,默认创建的是本地唯一索引。本地唯一索引只在分区内保证唯一,跨分区相同值可以并存,全局唯一性其实没有对账。
解决:所有需要全局唯一性的字段,建索引时必须显式加GLOBAL关键字。建表完成后,用下面这段 SQL 确认索引的属性:
SELECT index_name, index_type, is_global FROM oceanbase.DBA_OB_INDEXES WHERE table_name = 't_order';is_global列显示YES才是全局索引。如果你的应用对数据唯一性有强要求,这个检查建议放进上线模板里,每次发版前过一遍。
5. 再往前一步:用 EXPLAIN 和性能视图验证应用 SQL
5.1 用 EXPLAIN 看执行路径:分区裁剪到底裁没裁
写到这里,你会发现很多坑都跟“执行计划不符合预期”有关。所以我最后想分享一个验证习惯:每条关键 SQL 在预发环境跑一次EXPLAIN,确认它裁剪了正确的分区、走了正确的索引,再进代码评审。
EXPLAIN SELECT id, user_id, amount FROM t_order WHERE user_id = 123 AND create_time >= '2024-01-01' ORDER BY create_time DESC LIMIT 20;看输出里的Partitions字段,如果它显示p16这种单个分区,说明分区裁剪生效了;如果显示类似partitions(0-15)这种范围,说明这个查询扫了全部分区,就得回头想想分区键的选择。EXPLAIN出来的执行算子里,如果看到EXCHANGE OUT、EXCHANGE IN这样的分布式算子,说明数据在节点间发生了移动,这种 SQL 在数据量上来后大概率会拖垮性能。
有同学会问,MySQL 的EXPLAIN只有一张表的结果,OceanBase 的执行计划是不是很复杂?其实不用慌,你只需要关注四个点:访问的分区数、是否走主键还是索引、是否有跨节点数据交换、预估行数是否合理。这四个点看明白了,一条 SQL 的“健康程度”基本就能把握住。
5.2 慢 SQL 定位:应用日志、性能视图与系统表的联动
线上问题排查,我一般按这个顺序来:先从应用日志里捞出 SQL 文本和耗时,再到 OceanBase 的性能视图里找对应会话,最后看执行计划和等待事件。OceanBase 里常用的两处视图,一个是查活跃会话的GV$OB_PROCESSLIST,另一个是查慢查询的视图。当你发现某条 SQL 特别慢,又说不清是应用问题还是数据库问题,用下面这条 SQL 查最近一段时间内该表的访问量:
SELECT db, query_sql, elapsed_time, execute_time FROM oceanbase.GV$OB_SQL_AUDIT WHERE table_name = 't_order' ORDER BY elapsed_time DESC LIMIT 20;elapsed_time是总耗时,execute_time是执行耗时。如果两个值相差很大,说明大部分时间耗在了排队或者网络返回上,问题可能在应用侧或者网络链路;如果执行耗时本身很大,再去针对 SQL 本身做优化。这种“先定位是哪一端的问题”的习惯,能省掉大量盲目加索引的时间。
说实话,我刚开始写 OceanBase 应用时,也犯过拿着单机数据库的思维硬套分布式数据库的错。后来吃过几次亏,才养成一个习惯:每次上线看数据库相关的代码,都强制自己先静态过一遍分区键、连接串参数、批处理和事务大小这四个点,再拿 EXPLAIN 回来看执行路径。现在每接手一个新的数据库项目,这个检查流程都会重新来一遍——听起来机械,但确实帮我挡住了不少线上的雷。希望帮到你。
本文还有配套的精品资源,点击获取