首页 / 资讯中心 / 文章详情

OceanBase应用开发避坑指南:从MySQL迁移必懂的分区、索引与事务

OceanBase应用开发避坑指南:从MySQL迁移必懂的分区、索引与事务 ★ FEATURED ARTICLE
简介这份学习资料围绕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?useUnicodetruecharacterEncodingutf8connectTimeout3000socketTimeout5000rewriteBatchedStatementstrue; String user usertenant1#cluster_name; String password your_password; Connection conn DriverManager.getConnection(url, user, password);这里的用户名比较特别OceanBase 的用户名是usertenant#cluster三段式分别是用户名、租户名、集群名。如果漏掉tenant连接会直接报错让你以为密码错了。connectTimeout和socketTimeout建议显式设置默认值在弱网环境下会导致应用卡死很久才报错。rewriteBatchedStatementstrue在这里先埋个伏笔后面的批处理章节会专门讲没有这个参数批量插入性能可能差十倍以上。如果你的应用跑在云上或者通过 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 连接串里必须带上rewriteBatchedStatementstrue否则 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应用一上线就报连接耗尽现象应用启动后运行一段时间日志抛Cant 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 连接串里没有开启rewriteBatchedStatementstrue。没有这个参数addBatch只是把 SQL 暂存在客户端发送时仍然一条一条发性能没有本质提升反而多了一层本地组装的开销。解决连接串加上rewriteBatchedStatementstrue批处理才会真正合并成多值 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 和性能视图验证应用 SQL5.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 回来看执行路径。现在每接手一个新的数据库项目这个检查流程都会重新来一遍——听起来机械但确实帮我挡住了不少线上的雷。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站