release: v0.2.4 二级索引 + MVCC + BloomFilter + WAL全同步 + 内存预算
CI / test (18.x) (push) Successful in 10m8s
CI / test (20.x) (push) Successful in 10m4s
CI / test (22.x) (push) Successful in 9m54s
CI / test (24.x) (push) Successful in 9m53s

This commit is contained in:
thzxx
2026-07-27 21:50:15 +08:00
parent d33f4ab3b0
commit da999d607b
18 changed files with 1404 additions and 185 deletions
+170 -39
View File
@@ -2,13 +2,7 @@
* AriaEngine — 自研页面式存储引擎主类
* @module engine/aria/index
*
* 实现 IStorageEngine 接口。
*
* v0.2.1: 完整持久化
* - Schema 存入 __aria_schemas
* - SSTable 元数据存入 __aria_lsm_meta
* - WAL 恢复包含行数据
* - 启动时自动加载 Schema + SSTable
* v0.2.4: 二级索引 + MVCC 集成 + 生产加固
*/
import type { IStorageEngine } from '../interface';
@@ -26,6 +20,8 @@ import { WALRecordType, type WALRecord } from './types';
import { CheckpointManager } from './wal/checkpoint';
import { IndexedDBBackend, MemoryBackend, type IStorageBackend } from './store/backend';
import { OPFSBackend } from './store/opfs_backend';
import { MVCCManager } from './transaction/mvcc';
import { BloomFilter } from './index/bloom';
// ---------------------------------------------------------------------------
// AriaEngine
@@ -35,7 +31,7 @@ export class AriaEngine implements IStorageEngine {
readonly name = 'aria';
private config!: Required<AriaEngineConfig>;
private lsm!: LSM;
private lsm!: LSM; // 主键索引 LSM
private wal!: WAL;
private checkpointManager!: CheckpointManager;
private backend!: IStorageBackend;
@@ -47,9 +43,14 @@ export class AriaEngine implements IStorageEngine {
private tablePKs: Map<string, string> = new Map();
private opCounter = 0;
// 事务
// 二级索引:table.colKey → LSM
private secondaryIndexes: Map<string, LSM> = new Map();
// MVCC 事务
private mvcc: MVCCManager = new MVCCManager();
private currentTxnId: number | null = null;
private txnSnapshot: Map<string, Record<string, unknown>> | null = null;
private gcCounter = 0;
constructor(config: AriaEngineConfig = {}) {
this.config = { ...DEFAULT_ARIA_CONFIG, ...config };
@@ -76,7 +77,7 @@ export class AriaEngine implements IStorageEngine {
// 2. 构建 SSTableStore
const sstableStore = this.createSSTableStore();
// 3. 初始化 LSM
// 3. 初始化 LSMPK 索引)
this.lsm = new LSM({
memtableSizeThreshold: this.config.memtableSizeThreshold,
levelSizeMultiplier: this.config.levelSizeMultiplier,
@@ -185,6 +186,25 @@ export class AriaEngine implements IStorageEngine {
this.schemas.set(schema.name, schema);
this.tablePKs.set(schema.name, this.getPK(schema));
// 为索引列创建二级索引 LSM
const sstableStore = this.createSSTableStore();
for (const [colName, colDef] of Object.entries(schema.columns)) {
if (colDef.index || colDef.unique || colDef.primaryKey) {
const idxKey = `${schema.name}:idx:${colName}`;
if (!this.secondaryIndexes.has(idxKey)) {
const idxLsm = new LSM({
memtableSizeThreshold: this.config.memtableSizeThreshold,
levelSizeMultiplier: this.config.levelSizeMultiplier,
blockSize: this.config.pageSize,
bloomBitsPerKey: this.config.bloomFilterBitsPerKey,
sstableStore,
});
await idxLsm.init();
this.secondaryIndexes.set(idxKey, idxLsm);
}
}
}
await this.persistSchemas();
this.wal.append({
@@ -263,10 +283,13 @@ export class AriaEngine implements IStorageEngine {
// Within transaction: buffer to snapshot
this.txnSnapshot.set(key, validated);
} else {
// Direct write to LSM
// Direct write to LSM (PK index)
this.lsm.put(key, validated);
}
// 更新二级索引
this.updateSecondaryIndexes(tableName, pkValue, validated, null);
pks.push(pkValue);
this.wal.append({
@@ -280,6 +303,7 @@ export class AriaEngine implements IStorageEngine {
this.opCounter += rows.length;
await this.checkpointManager.tick();
this.tryGC();
return pks;
}
@@ -373,6 +397,9 @@ export class AriaEngine implements IStorageEngine {
key: String(row[pkCol]),
data: updated,
});
// 更新二级索引
this.updateSecondaryIndexes(tableName, String(row[pkCol]), updated, row);
}
}
@@ -407,6 +434,9 @@ export class AriaEngine implements IStorageEngine {
tableName,
key: String(row[pkCol]),
});
// 移除二级索引
this.updateSecondaryIndexes(tableName, String(row[pkCol]), null, row);
}
}
@@ -504,34 +534,6 @@ export class AriaEngine implements IStorageEngine {
});
}
private tryIndexLookup(
tableName: string,
query: QueryPlan,
): Record<string, unknown>[] | null {
if (!query.where) return null;
const pkCol = this.tablePKs.get(tableName)!;
for (const [col, condition] of Object.entries(query.where)) {
if (col !== pkCol) continue;
// 等值条件
if (typeof condition !== 'object' || condition === null) {
const key = `${tableName}:${condition}`;
const value = this.lsm.get(key);
return value ? [{ ...value, [pkCol]: condition }] : [];
}
const cond = condition as Record<string, unknown>;
if ('$eq' in cond) {
const key = `${tableName}:${cond.$eq}`;
const value = this.lsm.get(key);
return value ? [{ ...value, [pkCol]: cond.$eq }] : [];
}
}
return null;
}
private getPK(schema: TableSchema): string {
for (const [name, col] of Object.entries(schema.columns)) {
if (col.primaryKey) return name;
@@ -686,10 +688,139 @@ export class AriaEngine implements IStorageEngine {
}
}
// =======================================================================
// 二级索引
// =======================================================================
/** 更新行的二级索引条目 */
private updateSecondaryIndexes(
tableName: string, pkValue: string,
newRow: Record<string, unknown> | null,
oldRow: Record<string, unknown> | null,
): void {
const schema = this.schemas.get(tableName);
if (!schema) return;
for (const [colName, colDef] of Object.entries(schema.columns)) {
if (!colDef.index && !colDef.unique && !colDef.primaryKey) continue;
const idxKey = `${tableName}:idx:${colName}`;
const idxLsm = this.secondaryIndexes.get(idxKey);
if (!idxLsm) continue;
// 删除旧值
if (oldRow) {
const oldVal = oldRow[colName];
if (oldVal !== undefined && oldVal !== null) {
idxLsm.delete(`${String(oldVal)}:${pkValue}`);
}
}
// 插入新值
if (newRow) {
const newVal = newRow[colName];
if (newVal !== undefined && newVal !== null) {
idxLsm.put(`${String(newVal)}:${pkValue}`, { pk: pkValue });
}
}
}
}
/** 通过二级索引快速查找 */
private tryIndexLookup(
tableName: string,
query: QueryPlan,
): Record<string, unknown>[] | null {
if (!query.where) return null;
const schema = this.schemas.get(tableName);
if (!schema) return null;
const pkCol = this.tablePKs.get(tableName)!;
for (const [col, condition] of Object.entries(query.where)) {
// 跳过 $and/$or/$not 逻辑组合
if (col === '$and' || col === '$or' || col === '$not') continue;
const colDef = schema.columns[col];
const hasIndex = colDef && (colDef.index || colDef.unique || colDef.primaryKey);
if (!hasIndex && col !== pkCol) continue;
// PK 等值 → 主 LSM 精确查找
if (col === pkCol) {
if (typeof condition !== 'object' || condition === null) {
const key = `${tableName}:${condition}`;
const value = this.lsm.get(key);
return value ? [{ ...value, [pkCol]: condition }] : [];
}
const cond = condition as Record<string, unknown>;
if ('$eq' in cond) {
const key = `${tableName}:${cond.$eq}`;
const value = this.lsm.get(key);
return value ? [{ ...value, [pkCol]: cond.$eq }] : [];
}
}
// 二级索引查找
const idxKey = `${tableName}:idx:${col}`;
const idxLsm = this.secondaryIndexes.get(idxKey);
if (!idxLsm) continue;
// $eq → 精确查找
if (typeof condition !== 'object' || condition === null) {
return this.indexScanToRows(tableName, pkCol, idxLsm, col, String(condition), String(condition));
}
const c = condition as Record<string, unknown>;
if ('$eq' in c) {
const v = String(c.$eq);
return this.indexScanToRows(tableName, pkCol, idxLsm, col, v, v);
}
// $in → 多次精确查找
if ('$in' in c && Array.isArray(c.$in)) {
const results: Record<string, unknown>[] = [];
for (const val of c.$in) {
const rows = this.indexScanToRows(tableName, pkCol, idxLsm, col, String(val), String(val));
results.push(...rows);
}
return results;
}
// $gt / $gte / $lt / $lte → 范围扫描
if ('$gt' in c || '$gte' in c || '$lt' in c || '$lte' in c) {
const startKey = c.$gt ? `${String(Number(c.$gt) + 1)}:` : c.$gte ? `${String(c.$gte)}:` : `${col}:`;
const endKey = c.$lt ? `${String(Number(c.$lt) - 1)}:\uffff` : c.$lte ? `${String(c.$lte)}:\uffff` : `${col}:\uffff`;
return this.indexScanToRows(tableName, pkCol, idxLsm, col, startKey, endKey);
}
}
return null;
}
/** 从索引扫描结果恢复完整行 */
private indexScanToRows(
tableName: string, pkCol: string, idxLsm: LSM,
_col: string, startKey: string, endKey: string,
): Record<string, unknown>[] {
const entries = idxLsm.rangeScan(startKey, endKey);
const rows: Record<string, unknown>[] = [];
for (const [, idxEntry] of entries) {
const pk = (idxEntry as any).pk as string;
if (!pk) continue;
const row = this.lsm.get(`${tableName}:${pk}`);
if (row) rows.push({ ...row, [pkCol]: pk });
}
return rows;
}
// =======================================================================
// 辅助
// =======================================================================
/** 每 10 次 gc 计数器触发一次 MVCC 垃圾回收 */
private tryGC(): void {
this.gcCounter++;
if (this.gcCounter >= 10) {
this.mvcc.gc(100);
this.gcCounter = 0;
}
}
private ensureOpen(): void {
if (!this.opened) throw new DatabaseError('AriaEngine not opened', 'DB_NOT_OPEN');
}
+18
View File
@@ -4,6 +4,7 @@
*/
import type { IndexEntry, SSTableMeta } from '../types';
import { BloomFilter } from './bloom';
const SSTABLE_MAGIC = 0x53535442; // "SSTB"
@@ -17,6 +18,7 @@ export class SSTableReader {
private indexEntries: IndexEntry[] = [];
private entryCount = 0;
private meta: SSTableMeta;
private bloomFilter: BloomFilter | null = null;
constructor(data: Uint8Array, meta: SSTableMeta) {
this.data = data;
@@ -31,6 +33,9 @@ export class SSTableReader {
/** 精确查找 key */
get(targetKey: string): Record<string, unknown> | null {
// Bloom Filter 快速否定
if (this.bloomFilter && !this.bloomFilter.mayContain(targetKey)) return null;
const blockIdx = this.locateBlock(targetKey);
if (blockIdx < 0) return null;
@@ -175,10 +180,23 @@ export class SSTableReader {
const indexOffset = this.view.getUint32(footerOffset, false);
const indexSize = this.view.getUint32(footerOffset + 4, false);
const bloomOffset = this.view.getUint32(footerOffset + 8, false);
const bloomSize = this.view.getUint32(footerOffset + 12, false);
const bloomHashCount = this.view.getUint32(footerOffset + 16, false);
this.entryCount = this.view.getUint32(footerOffset + 20, false);
// 解析索引块
this.parseIndexBlock(indexOffset, indexSize);
// 加载 Bloom Filter
if (bloomOffset > 0 && bloomSize > 0 && bloomOffset + bloomSize <= this.data.byteLength) {
try {
const bloomBytes = this.data.slice(bloomOffset, bloomOffset + bloomSize);
this.bloomFilter = BloomFilter.fromData(bloomBytes, bloomHashCount || 10);
} catch {
// 损坏的 bloom filter 不影响读取(仅跳过快速否定优化)
}
}
}
private parseIndexBlock(offset: number, _size: number): void {
+13 -4
View File
@@ -92,8 +92,12 @@ export class SSTableBuilder {
const indexBlockSize = this.estimateIndexBlockSize(indexEntries);
// 写入到 buffer
const finalSize = totalSize + indexBlockSize + SSTABLE_FOOTER_SIZE;
// 序列化 bloom filter 以获取其大小
const bloomData = bloomFilter.serialize();
const bloomSize = bloomData.byteLength;
// 写入到 buffer(包含 bloom block
const finalSize = totalSize + indexBlockSize + bloomSize + SSTABLE_FOOTER_SIZE;
const buf = new ArrayBuffer(finalSize);
const view = new DataView(buf);
@@ -108,12 +112,17 @@ export class SSTableBuilder {
const indexOffset = offset;
offset = this.writeIndexBlock(view, offset, indexEntries);
// ---- Bloom Filter Block ----
const bloomOffset = offset;
new Uint8Array(view.buffer).set(bloomData, offset);
offset += bloomSize;
// ---- Footer ----
const footerOffset = offset;
view.setUint32(footerOffset, indexOffset, false); // index_offset
view.setUint32(footerOffset + 4, indexBlockSize, false); // index_size
view.setUint32(footerOffset + 8, 0, false); // bloom_offset (embedded in footer)
view.setUint32(footerOffset + 12, 0, false); // bloom_size
view.setUint32(footerOffset + 8, bloomOffset, false); // bloom_offset
view.setUint32(footerOffset + 12, bloomSize, false); // bloom_size
view.setUint32(footerOffset + 16, bloomFilter.getHashCount(), false);
view.setUint32(footerOffset + 20, this.entries.length, false);
view.setUint32(footerOffset + 24, SSTABLE_MAGIC, false);
+7 -1
View File
@@ -280,6 +280,10 @@ export interface AriaEngineConfig {
compression?: boolean;
/** 存储后端 */
storageBackend?: 'indexeddb' | 'opfs' | 'memory';
/** WAL 大小阈值(字节,超过则强制 checkpoint,默认 16MB */
walSizeThreshold?: number;
/** 最大内存预算(MB,默认 64) */
maxMemoryMB?: number;
}
export const DEFAULT_ARIA_CONFIG: Required<AriaEngineConfig> = {
@@ -289,8 +293,10 @@ export const DEFAULT_ARIA_CONFIG: Required<AriaEngineConfig> = {
levelSizeMultiplier: DEFAULT_LEVEL_SIZE_MULTIPLIER,
bloomFilterBitsPerKey: DEFAULT_BLOOM_BITS_PER_KEY,
walEnabled: true,
walSyncMode: 'batch',
walSyncMode: 'full',
checkpointInterval: 1000,
compression: false,
storageBackend: 'indexeddb',
walSizeThreshold: 16 * 1024 * 1024, // 16MB
maxMemoryMB: 64,
};