Lehman–Yao构造了一个供并发进程使用的数据结构。该数据结构是 Wedekind提出的 B*-树的一个简单变体(其基础是 Bayer 和 McCreight定义的 B 树)。 Lehman–Yao B*-树定义如下。
1.1 定义
Each path from the root to any leaf has the same length, h.
内部节点(非 root、非叶子)至少要有 k + 1 个孩子(sons)。 (k is a tree parameter; 2k is the maximum number of elements in a node, neglecting the “high key,” which is explained below.)
x <- scannode(u, A) denotes the operation of examining the tree node in memory block A for value u and returning the appropriate pointer from A (into x).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
procedure search(u) current <- root; A <- get(current); while current is not a leaf do begin current <- scannode(u, A); A <- get(current) end;
while t <- scannode(u, A) = link ptr of A do begin current + t ; A <- get(current) end;
if v is in A then done “success” else done “failure”
将值 u 插入叶子节点时,可能需要对该节点进行分裂(当节点不安全时)。 在这种情况下,我们对节点进行分裂(如图 8 所示),用两个新节点 a’(a 的新版本,写回同一磁盘页面)和 b’ 替换原来的节点 a。节点 a’ 和 b’ 的内容与原节点 a 相同,只是增加了值 u。随后,我们沿着先前记录的搜索路径回溯树的上层,在叶子节点的父节点中插入新节点 b’ 的条目以及 a’ 的新 high key。
死锁的避免是由锁定方案的良序性(well-ordering)所保证的,如下所示。 需要注意的是,当我们沿树向上回溯时,由于节点可能被分裂,我们必须插入新指针的节点可能并不是下降过程中经过的那个节点。换句话说,我们在下降过程中使用的旧节点可能已经被分裂;此时,正确的插入位置就在原预期插入位置右侧的某个节点。我们通过 link 指针 来找到这个节点。
A <- node.insert (A, w, v) denotes the operation of inserting the pointer w and the value v into the node contained in A. u <- allocate(2 newpage for B) denotes the operation of allocating a new page on the disk. The node contained in B will be written onto this page, using the pointer u. “A, B <- rearrange old A, adding ..” denotes the operation of splitting A into two nodes, A and B, in core.
插入(Insert)。该算法负责将一个值 v(以及其关联的记录)插入到树中。当算法结束时,值 v 已成功插入到树中,并且在必要的情况下,算法会在从叶子向上回溯的过程中对相应的节点进行分裂。
procedure insert(v) initialize stack; current <- root; A <- get(current); while current is not a leaf do begin t <- current; current <- scannode( v, A); if new current was not link pointer in A then push(t); A <- get(current); end; lock(current); A <- get(current); move.right; if v is in A then stop “v already exists in tree”; w <- pointer to pages allocated for record associated with v; Doinsertion: if A is safe then begin A <- node.insert(A, w, v); put(A, current); unLock(current); end else begin u <- allocate(1 new page for B); A, B <- rearrange old A, adding v and w, to make 2 nodes, where (link ptr of A, link ptr of B) <- (u, link ptr of old A); y <- max value stored in new A; put(B, u) put(A, current); oldnode <- current; v <- Y; w <- u; current <- pop(stack); lock(current); A <- get(current); move.right; unlock(oldnode); goto Doinsertion end
Move.right. This procedure, which is called by insert, follows link pointers at a given level, if necessary.
1 2 3 4 5 6 7 8
procedure move.right while t <- scannode(u, A) is a link pointer of A do begin lock(t); unlock(current); current <- t; A <- get(current); end
需要注意的是,该过程在向上回溯树时是 逐层进行的。此外,同时最多只会锁定三个节点,而这种情况发生的频率相对较低:仅在插入分裂节点的指针时,需要沿 link 指针向右移动 的情况下才会出现。此时,被锁定的节点包括:
该算法的正确性依赖于以下事实:树结构的任何变化(即任何节点的分裂)都会伴随一个 link 指针。节点分裂时,条目总是被移动到树的右侧,在右侧的新节点可以通过 link 指针被访问到,从而保证树的结构在并发操作下仍然可达且正确。
具体来说,对于任何层级的一个对象(与某个值相关联),我们总能大致知道它的正确插入位置,即我们在该层搜索时经过的“记录节点”。如果该对象的正确插入位置发生了移动,这种移动方式是可预知的:也就是节点分裂向右,留下的 link 指针使得搜索或插入操作仍然能够找到它。因此,从旧的“预期”插入位置开始,始终可以访问到对象的正确插入位置。
注意,在算法中(针对不安全节点),“put(B, u)” 紧接在 “put(A, current)” 之前执行。我们将在下面的引理中证明,这种顺序实际上将两个 put 操作简化为本质上的一次操作。
引理 2. 操作 ‘put(B, u); put(A, current)’ 相当于对树结构的一次修改。 证明. 假设这两个操作分别写入节点 b 和 a。在执行 “put(B, u)” 时,没有其他节点指向正在写入的节点 b,因此该 put 操作对树结构没有影响。现在,当执行 “put(A, current)” 时,该操作修改了 current 指向的节点(节点 a)。修改内容包括将节点 a 的链接指针指向 b。此时,b 已经存在,并且 b 的链接指针指向与 a 的旧版本相同的节点。这样就实现了同时修改 a 并将 b 引入树结构。证毕。
定理 2. 所有 put 操作都能正确地修改树结构。 证明 case 1: 对安全节点执行 “put(A, current)”。该操作只修改树中一个已加锁的节点,因此树的正确性得到保持。 case 2: 对不安全节点执行 “put(B, u)”。该操作不会改变树结构。 case 3: 对不安全节点执行 “put(A, current)”。根据引理,该操作既修改了当前节点(比如 a),又将另一个节点(比如 b,通过 “put(B, u)” 写入)引入树结构。与情况 1 类似,节点 a 在执行 “put(A, current)” 时已加锁。本例的区别在于该节点是不安全的,需要分裂。但根据引理,我们可以通过一次操作完成,保持树结构的正确性。证毕
证明. 考虑进程 P 通过节点 a 的路径。进程 P 在到达节点 a 之前所经过的路径不会被插入进程 I 改变。此外,根据上面的定理 2,进程 I 对树结构所做的任何修改都会产生一个正确的树(well-ording)。因此,进程 P 在 a 节点开始的路径(在时间 t>t′t > t’t>t′)将无论修改如何都能正确执行。证毕。
第 1 部分. 考虑插入进程 I(在时间 t0 修改节点 n)与搜索进程 S(在时间 t′< t0读取节点 n)之间的交互。记 n′ 为修改后的节点。(本节的论证同样适用于另一插入进程 I′与进程 I 交互的情况,且 I′正在执行搜索。)需要考虑的操作顺序是:S 读取节点 n;然后 I 将节点 n 修改为 n′;然后 S 根据 n 的内容继续搜索。 考虑三种插入类型:
Type 1
进程 I 对节点 n 执行一次简单插入。如果 n 是叶子节点,插入进程不会改变任何指针。其结果等同于序列调度中 S 在 I 之前运行的情况。如果 n 是非叶子节点,则在 n 中插入一个指向下一层某节点 m′的指针/值对。假设 m′是通过将 I 分裂为 I′ 和 m′ 创建的。唯一可能的交互是 S 在插入指向 m′ 的指针之前已经获得了指向 I 的指针。此时指向 I 的指针指向了 I′,而 S 将使用 I′中的 link pointer 访问 m′。因此搜索仍然是正确的。
Types 2 and 3
节点 n 在插入过程中被分裂为节点 n1′ 和 n2′。对于叶子节点的情况,搜索在 n 上的结果与在 n1′和 n2′上的结果相同,除了新插入的值 S 无法找到。 如果 n 不是叶子节点,则其下层的某个节点发生了分裂,导致一个新的指针/值对被插入节点 n,从而使 n 本身也分裂。根据归纳法,下层节点的分裂是正确的。根据引理 3,下层节点的搜索也是正确的。因此,我们只需证明节点 n 的分裂是正确的。假设节点 n 分裂为节点 n1′ 和 n2′,它们包含与原节点 n 相同的指针集合,并增加了新插入的节点。则从节点 n 开始搜索,将到达下一层与从 n1′(带有指向 n2′的 link pointer)开始搜索时相同的节点集合。特殊情况是:如果搜索在读取节点 n 时,新插入的指针已经存在,本应沿该指针继续。此时实际跟随的指针位于新指针的左侧,这会将搜索引导到某个节点(假设为 k),它位于新指针指向的节点(假设为 m)左侧。然后沿 k 的 link pointer 最终到达 m,这仍然是正确的结果。(类型 3 的论证与类型 2 相同,只是新条目插入到新创建的半节点中,而不是旧半节点。但这对论证没有影响,因为节点在分裂发生前已被S读取。)
第2部分。接下来我们考虑插入进程 I 与另一个插入进程 I’ 的交互情况。进程 I’ 可能正在搜索用于插入的正确节点、回溯到另一层,或者实际尝试将一个值/指针对插入节点 n。如果 I’ 正在搜索一个用于插入值/指针对的节点,则该搜索行为与普通搜索进程完全相同。因此,证明与上文针对搜索进程的证明相同。
考虑在将某个节点 n 压入栈和回溯时再次访问该节点之间可能发生的情况:该节点可能已经分裂过一次或多次,这些分裂会在节点 n 的“右侧”产生新的节点。
由于节点 n 右侧的所有节点都可以通过 link 指针访问,因此插入算法能够找到适当的位置插入值。
第四部分。如果进程 I’ 试图在节点 n 上进行插入,它会尝试锁定该节点。但进程 I 已经持有节点 n 的锁。最终,I 会释放该锁,I’ 再锁定该节点并将其读入内存。根据上面的引理,该交互是正确的,因为 I’ 的读取发生在 I 的插入之前。节点 n 要么就是插入的正确位置——此时 I’ 会执行插入;要么搜索必须沿节点的 link 指针访问其右兄弟。
LiveLock
我们在此指出,我们的算法并不能完全避免活锁(LiveLock)的可能性(即某个进程无限运行)。 如果一个进程因为不断跟随其他进程创建的 link 指针而无法终止,就可能发生这种情况。 然而,根据以下观察,我们认为在实际实现中这种情况极不可能成为问题。 在多处理器系统中,如果该进程运行在相对非常慢的处理器上,这种情况可能发生。
(1) 在我们所知的大多数系统中,处理器的运行速度大致相当。 (2) 在 B 树中,节点的创建和删除只占很小的比例,因此即使是较慢的处理器,也不太可能因节点的创建或删除而遇到困难(也就是说,它只需要跟随少量的 link 指针)。 (3) 在树的任意给定层上,只能创建固定数量的节点,从而限制了较慢处理器需要“追赶”的量。
一种处理删除操作的简单方法是允许叶子节点中的条目少于 K 个。对于非叶子节点则不需要这样做,因为删除操作仅会移除叶子节点中的键;非叶子节点中的键仅作为其对应指针的上界,它在删除过程中并不会被移除。
因此,为了从叶子节点中删除一个条目,我们对该节点执行的操作与插入操作(尤其是插入情况 1)非常类似。具体来说,我们首先搜索出值 u 所在的节点,然后锁定该节点,将其读入内存,对内存中的副本删除值 u 后再将节点重写回磁盘。有时,这样会导致节点中条目数量少于 K。 该算法的正确性证明与插入操作类似。例如,死锁自由性的证明非常简单,因为删除操作只需要锁定一个节点。对于操作的正确性,如果一个搜索进程在删除值 u 之前读取了节点,它仍然会报告节点中存在 u,这与序列化调度(搜索操作先于删除操作执行)是一致的,因此搜索结果仍然正确。
test=# create table users(id int generated always asidentityprimary key, name text); CREATE TABLE
test=# select oid, relname, relfilenode from pg_class where relname ='users'; oid | relname | relfilenode -------+---------+------------- 40980| users |40980
test=# select c.oid, c.relname, i.indisprimary from pg_class c join pg_index i on c.oid = i.indexrelid where i.indrelid ='users'::regclass and i.indisprimary; oid | relname | indisprimary -------+------------+-------------- 40986| users_pkey | t (1row)
test=# select lp,lp_len,t_data from heap_page_items(get_raw_page('users', 0)); ERROR: block number 0isoutofrangefor relation "users" test=# select*from bt_page_items('users_pkey',1); ERROR: block number 1isoutofrange test=# ^C
test=# select*from bt_page_items('users_pkey',1); itemoffset | ctid | itemlen | nulls | vars | data | dead | htid | tids ------------+-------+---------+-------+------+-------------------------+------+-------+------ 1| (0,1) |16| f | f |0100000000000000| f | (0,1) | 2| (0,2) |16| f | f |0200000000000000| f | (0,2) | (2rows)
To build OceanBase from source code, you need to install the C++ toolchain in your development environment first. If the C++ toolchain is not installed yet, you can follow the instructions in this document for installation.
You can check the mysql_port in ./tools/deploy/single.yaml file to see the listening port. Normally, if you deploy with the root user, the OceanBase server will listen on port 10000, and the examples below are also based on this port.
Connect
You can use the official MySQL client to connect to OceanBase:
1
mysql -uroot -h127.0.0.1-P10000
Alternatively, you can use the obclient to connect to OceanBase:
1
./deps/3rd/u01/obclient/bin/obclient -h127.0.0.1-P10000 -uroot -Doceanbase -A
Click one point on the canvas, then click another point — a straight line will appear.
Select the line → double-click the middle of the line → drag the new point: a curved effect will appear.
Flexible Control Over Line Arrows
When the endpoint of a line (especially an endpoint with an arrow) is connected to a shape, the default mode is “Connected Line” (automatic snapping). In this mode, the endpoint is “locked” to the shape’s boundary or center, and cannot be freely dragged.
Solution
Turn on Magnets
Select the target shape.
Menu Bar → Edit → Magnets → Show Magnets
You will see small red dots (magnets) appear around the shape.
These are the connectable positions.
Drag the endpoint of a line near the red dot, and it will snap/attach to that point.
What is Magnet?
Magnet (magnetic points) are those positions on the shape (Shape) that can attract the end points of the lines. When you draw a line with the Line Tool, endpoints that are close to these magnetic points will “snap” to it.
postgres@lavm-bar1guved6:/root$ pgbench -i test dropping old tables... NOTICE: table "pgbench_accounts" does not exist, skipping NOTICE: table "pgbench_branches" does not exist, skipping NOTICE: table "pgbench_history" does not exist, skipping NOTICE: table "pgbench_tellers" does not exist, skipping creating tables... generating data (client-side)... vacuuming... creating primary keys... done in1.54 s (drop tables 0.11 s, create tables 0.49 s, client-side generate 0.54 s, vacuum 0.18 s, primary keys 0.23 s). postgres@lavm-bar1guved6:/root$ pgbench -T 30 test pgbench (19devel) starting vacuum...end. transaction type: <builtin: TPC-B (sort of)> scaling factor: 1 query mode: simple number of clients: 1 number of threads: 1 maximum number of tries: 1 duration: 30 s number of transactions actually processed: 3522 number of failed transactions: 0 (0.000%) latency average =<strong>8.518</strong> ms initial connection time=4.025 ms tps =<strong>117.400485</strong> (withoutinitial connection time)
postgres@lavm-bar1guved6:/root$ psql test psql (19devel) Type "help" for help. test=# ALTERSYSTEMSET synchronous_commit = off; ALTERSYSTEM test=# SELECT pg_reload_conf(); pg_reload_conf ---------------- t (1row) postgres@lavm-bar1guved6:/root$ pgbench -T 30 test pgbench (19devel) starting vacuum...end. transaction type: <builtin: TPC-B (sort of)> scaling factor: 1 query mode: simple number of clients: 1 number of threads: 1 maximum number of tries: 1 duration: 30 s number of transactions actually processed: 9510 number of failed transactions: 0 (0.000%) latency average =<strong>3.154</strong> ms initial connection time=4.383 ms tps =<strong>317.038330</strong> (withoutinitial connection time)
服务器启动时,第一个启动的进程是 postmaster(新版本为postgres)。postmaster 接着会生成 startup process(启动进程),startup process 负责在发生故障时进行数据恢复。
startup process 是一个短暂的、一次性的进程,它的主要职责是在数据库启动时执行崩溃恢复或归档恢复。它完成它的工作后,就会退出。
1 2 3
postgres@lavm-bar1guved6:/root$ pg_controldata -D /home/postgres/pgdata/ |grep state Database cluster state: in production postgres@lavm-bar1guved6:/root$
postgres@lavm-bar1guved6:/root$ pg_ctl stop -m immediate waiting for server to shut down.... done server stopped postgres@lavm-bar1guved6:/root$ pg_controldata -D /home/postgres/pgdata/ |grep state Database cluster state: in production
当我们启动服务器时,启动进程会检测到之前发生了故障,因而进入恢复模式:
1 2 3 4 5 6 7 8 9 10 11
2025-08-04 10:23:31.860 CST [487414] LOG: starting PostgreSQL 19devel on x86_64-pc-linux-gnu, compiled by gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0, 64-bit 2025-08-04 10:23:31.861 CST [487414] LOG: listening on IPv4 address "127.0.0.1", port 5432 2025-08-04 10:23:31.878 CST [487414] LOG: listening on Unix socket "/tmp/.s.PGSQL.5432" 2025-08-04 10:23:31.920 CST [487420] LOG: database system was interrupted; last known up at 2025-08-01 09:36:11 CST 2025-08-04 10:23:32.098 CST [487420] LOG: database system was not properly shut down; automatic recovery in progress 2025-08-04 10:23:32.125 CST [487420] LOG: redo starts at 0/01B75168 2025-08-04 10:23:32.125 CST [487420] LOG: invalid record length at 0/01B752A8: expected at least 24, got 0 2025-08-04 10:23:32.125 CST [487420] LOG: redo done at 0/01B75270 system usage: CPU: user: 0.00 s, system: 0.00 s, elapsed: 0.01 s 2025-08-04 10:23:32.132 CST [487418] LOG: checkpoint starting: end-of-recovery fast wait 2025-08-04 10:23:32.152 CST [487418] LOG: checkpoint complete: wrote 0 buffers (0.0%), wrote 3 SLRU buffers; 0 WAL file(s) added, 0 removed, 0 recycled; write=0.006 s, sync=0.005 s, total=0.022 s; sync files=2, longest=0.005 s, average=0.003 s; distance=0 kB, estimate=0 kB; lsn=0/01B752A8, redo lsn=0/01B752A8 2025-08-04 10:23:32.155 CST [487414] LOG: database system is ready to accept connections