用Node.js设计一个数据库,支持常见的SQL语句
git clone https://gitee.com/onlyyyy/AvenirSQL
cd AvenirSQL
npm i pm2 -g
安装pm2管理工具
pm2 start AvenirSQL
注意查看run.ini 设置数据库的配置文件
更新配置需要重启数据库
pm2 restart AvenirSQL
建议使用高版本Node.js(v14+),通过版本管理工具n进行更新:
目前n只支持Mac和Linux
npm i n -g
n lts//下载最新版Nodejs
./database/ : AvenirSQL核心实现
./AvenirSQL.js : AvenirSQL启动程序
./curl.js : cli程序
./dtest.js : 测试程序
./jmeter/ : Jmeter测试用例文件
./db/ : 数据库和表数据
./run.ini : 数据库配置文件
./AvenirSQL/ : Nodejs版的AvenirSQL操作库
以下是项目中使用的Avenir开源组织开发的Node模块:
数字,默认长度10
其实也是数字 并不是整数,默认长度10
不限制长度的数字
字符型 默认长度20
字符型 默认长度20
不限制长度的字符
async response(type, client) {
let res = null;
if (typeof type == 'string') {
res = getError(type);
if (!res) {
res = unknown;
}
} else {
let code = type.code;
let data = type.data;
res = getError(code);
if (!res) {
res = unknown;
}
res.data = data;
}
client.write(JSON.stringify(res));
//如果没有配置默认短连接
if( ini.db.keepAlive != true) {
toLog("主动踢掉客户端的连接");
client.end();
}
}
2.数据库结构
数据库:文件夹名
表:数据文件、哈希索引文件、B+树索引文件(聚合索引)
数据文件:
第一行存放表结构定义,第二行开始第一位为压缩的16进制数,表示该行元素是否为空,后续存储按分隔符排列。
哈希索引:
对象,key为主键,value为所在文件的行号
B+树索引:
存放B+树的结构
3.连接管理
为了区分不同的用户对数据库进行的不同操作,如同一秒内多个进程进行多次请求,AvenirSQL会生成一个签名,用户登录后需使用此签名进行操作。
4.串行锁
进行操作前加锁,操作完成后解锁,并刷新缓存(select语句不会刷新缓存)
//自动释放锁防止数据库死锁
async releaseLock() {
let now = moment().valueOf();
let releaseLockTime = ini.db.releaseLockTime;
releaseLockTime = releaseLockTime > ini.db.checkLockTime ? releaseLockTime : ini.db.checkLockTime;
for(let key in this.table) {
let tables = this.table[key];
for(let subKey in tables) {
let times = tables[subKey];
if(moment(now).diff(moment(times),'seconds') > releaseLockTime) {
delete tables[subKey];
toLog("自动释放了锁 ",tables[subKey]);
}
}
}
}
5.缓存
目前共五类缓存,数据库配置文件缓存和表结构缓存不会刷新,哈希索引、表数据、B+树索引缓存会定时刷新。
6.解析SQL
在此感谢阿里巴巴的sql解析器 node-sqlparser
AvenirSQL独有的sql会先解析,除此之外的SQL会转交给node-sqlparser。
//包含原生SQL和能够被AvenirSQL识别的语句
async parse(sql, sign) {
//先解析AvenirSQL特有的语句 再解析原生SQL
toLog("要解析的 sql为 ", sql);
let raw = this.getArray(sql);
if (raw.length === 0 || !sql) {
throw ('SQL_PARSE_ERROR');
} else {
//AvenirSQL解析出错不报错,转给解析器解析,解析器报错直接throw
try {
await this.parseAvenirSql(raw, sql, sign);
} catch (error) {
//不是内部定义的错误就代表程序处理出错了
toLog('error = ', error);
if (error == SUCCESS || error != 'error') {
throw (error);
}
//不需要try catch了,底层会抓住错误
let par = this.parseSql(sql);
await this.doSql(par, sign);
}
}
}
7.事务
事务操作在缓存中临时处理,使用串行锁避免并发,不会出现重复读,读未提交等。
rollback操作直接清除缓存,commit操作提交 写文件,释放锁,同样会释放缓存。
超时自动释放锁 避免出现数据库死锁的情况。
8.日志
暂时只支持记录类日志,未实现undolog,binlog等日志,通过Trace类,avenir-log模块实现。
9.cli程序
提供一个与AvenirSQL交互的工具,实现自动重连功能,当发现报错为code 2 签名失效则直接重连。
//检查返回值,超时就自动重发
async function checkError(response) {
if (response && response.code == 2) {
//返回值是2代表签名失效 重新登录即可
let data = await safeConnect();
if (data.code === 0) {
data = data.data;
} else {
//说明重新登录也报错了 数据库故障
return {
code: -1,
data: null,
}
}
return {
code: 1,
data
};
} else if (response.code == 0) {
return {
code: 0,
data: null
};
} else {
return {
code: -1,
data: null,
}; //其他错误 其实返回这个没啥用 只需要判断1 就行
}
}
在返回了结果集之后,根据distinct参数来对数据去重,通过依次插入到多叉树中,一旦发现插入失败则表示数据重复了。
//处理distinct的函数 20210222 考虑用多叉树来处理
async doDistinct(data, columns, tableDetail) {
let mulTree = new MultipleTree();
if (columns.length == 1 && columns[0].expr.column == tableDetail.key) {
//如果是主键的话本身就是distinct了
toLog("优化器跳过distinct");
return data;
}
//遍历结果集 剔除重复的列
for (let i = 0; i < data.length; i++) {
let line = data[i];
if(mulTree.insert(line) === false) {
data.splice(i,1);
i--;
} else {
continue;
}
}
return data;
}
导出即执行select操作,并将SQL语句转换为insert语句。
导入则按分隔符执行SQL语句
导出文件名规范:dump[table || database]_dbname_tableName_YYYYMMDDHHmmss.sql
数据库连接使用tcp通信,传输文本为JSON数据格式。
{
user:"root",
password:"123456"
type:"login"
}
返回值:
{
code:0,
message:"success",
data:"ef3d843f26c4e900e9ab4979f324d5571a4cb5f5c011278b36985b2802c828185ad0bdd7e19390cfffe479afe1b09d1c"
}
{
sign:"ef3d843f26c4e900e9ab4979f324d5571a4cb5f5c011278b36985b2802c828185ad0bdd7e19390cfffe479afe1b09d1c",
type:"sql",
data:"select * from test"
}
返回值:
{
code : 0,
message:"success",
data:[{
name:"test",
id:"1",
}]
}
{
sign:"ef3d843f26c4e900e9ab4979f324d5571a4cb5f5c011278b36985b2802c828185ad0bdd7e19390cfffe479afe1b09d1c",
id:"transID" //事务执行流程: begin->sql->commit,begin的时候会给id 后续用id来执行sql
type:"trans",
data:"delete from test",
}
返回值:
{
code : 0,
message:"success",
}
分隔符为∫,故所有的列数据不可以含有∫符号,否则会报错
{
//不该发生的错误
UNKNOWN_CMD: {
code: -1,
message: 'unknown command',
},
SYSTEM_BUSY: {
code: -2,
message: 'system busy',
},
BAD_REQUEST: {
code: -3,
message: 'bad request', //请求格式无法JSON序列化
},
UNKNOWN_ERROR: {
code: -100,
message: 'unknown error'
},
//成功
AVENIR_SUCCESS: {
code: 0,
message: 'success'
},
//程序级别错误
SQL_PARSE_ERROR: {
code: 1,
message: 'sql parse error'
},
NOT_CONNECTED: {
code: 2,
message: "no connect info, please login first"
},
//大意了没有3 4不吉利 年轻人不讲5的
FILE_NOT_EXIST: {
code: 6,
message: 'file not exist'
},
GEN_SIGN_ERROR: {
code: 7,
message: 'generate sign error',
},
SET_SIGN_EXIT: {
code: 8,
message: 'sign exit',
},
LACK_OF_SIGN: {
code: 9,
message: 'lack of sign, please login first',
},
INVALID_NAME: {
code: 10,
message: "invalid name",
},
BAD_USER: {
code: 11,
message: 'user or password error',
},
PERMISSION_DENIED: {
code: 12,
message: 'permission denied',
},
//以下是数据库类错误
DATABASE_NOT_FOUND: {
code: 1001,
message: 'database not found'
},
DATABASE_EXIST: {
code: 1002,
message: 'database already exist',
},
TABLE_NOT_FOUND: {
code: 1003,
message: 'table not found'
},
TABLE_EXIST: {
code: 1004,
message: 'table already exist'
},
INVALID_SQL_ERROR: {
code: 1005,
message: "invalid sql or sql parse Error"
},
TOO_MANY_COLUMNS: {
code: 1006,
message: 'too many columns',
},
COLUMN_NOT_FOUND: {
code: 1007,
message: 'column not found',
},
COLUMN_NOT_MATCH: {
code: 1008,
message: 'columns and values not match',
},
COLUMN_REPEAT: {
code: 1009,
message: 'columns repeat',
},
COLUMN_NOT_NULL: {
code: 1010,
message: 'some columns cant be null',
},
COLUMN_NOT_CHECK: {
code: 1011,
message: 'column not check error',
},
ONLY_ONE_KEY: {
code: 1012,
message: 'AvenirSQL only support one key',
},
LACK_OF_PRIMARY_KEY: {
code: 1013,
message: 'lack of primary key',
},
KEY_EXIST: {
code: 1014,
message: 'duplicate primary key value',
},
OPER_NO_ROW: {
code: 1015,
message: 'the operated row not found, may be not a error'
},
VALUE_NOT_NUMBER: {
code: 1016,
message: 'compared value is not a number',
},
SQL_TOO_LONG: {
code: 1017,
message: 'sql is too long',
},
SQL_NOT_SUPPORT: {
code: 1018,
message: 'AvenirSQL dont support this sql yet',
},
GET_LOCK_FAILED: {
code: 1019,
message: 'AvenirSQL get lock timeout',
},
RELEASE_LOCK_FAILED: {
code: 1020,
message: 'AvenirSQL release lock failed',
},
USER_EXISTS: {
code: 1021,
message: 'user already exists',
},
USER_NOT_FOUND: {
code: 1022,
message: 'bad user or password',
},
//事务类错误
TRANS_NOT_FOUND: {
code: 1023,
message: 'invalid trans id',
},
TRANS_TIME_OUT: {
code: 1024,
message: 'trans timeout',
},
NO_GROUP_DIS: {
code: 1025,
message: 'AvenirSQL dont support group by yet'
},
COUNT_NO_TABLE: {
code: 1026,
message: `count(colums) cant be [table.column]`
},
WHITE_SPACE_ERROR: {
code: 1027,
message: `sql cant contain AvenirSQL's separator:${this.WHITE_SPACE}`,
},
NOT_SUPPORT_DATA: {
code: 1028,
message: 'AvenirSQL dont support such data type',
},
COLUMN_TYPE_ERROR: {
code: 1029,
message: 'table columns type error(number or string)',
},
COLUMN_OUT_OF_LENGTH: {
code: 1030,
message: 'columns over the length',
},
DUMP_TABLENAME_ERROR: {
code: 1031,
message: 'dumped table name error'
},
DUMP_SQL_ERROR: {
code: 1032,
message: "dump sql error"
},
}
暂不支持join、 like、 group by
show命令
show database
展示目前所有的数据库列表
show table [dbname]
展示该数据库下的所有表名,不输入dbname会去获取默认值
show dump
展示默认的导出目录下所有的文件名
{"code":0,"message":"success","data":["home_def","hot","t"]}
不支持不带where条件的全表更新 如update t set a = '1'
distinc a,b,c 即代表数据库里面a,b,c三个列都重复才会判断重复。
通过多叉树模块multiple-tree来实现
dump table dbname.tableName as select * from tableName where a > 10000
如果不带SQL语句,则导出全表,在配置文件配置是否是覆盖导出和导入
文件名必须是run.ini,在安装目录下
若找不到此文件,AvenirSQL会自动创建默认的配置文件,内容如下:
exec [fileName]
执行SQL文件 若只有文件名,则会拼上ini文件的dump.path,否则就直接读取全路径。[main]
ip=127.0.0.1
port=44944
#数据库的工作目录
path=./db
#是否输出日志至显示屏 频繁I/O操作将影响性能
ifConsoleLog=true
[db]
#maxConnect=100 目前存在问题
#签名的有效期(秒)
signValidTime=100
#事务自动回滚时间(秒)
rollbackTime=10
#是否维持长连接
keepAlive=false
#是否记录debug类的日志
debug=false
#每个连接的超时时间 毫秒
timeOut=10000
#数据库表列的上限
maxColoums=100
#数据库SQL的最大长度
maxSqlLength=200
#默认的用户数据库
user=User
#缓存失效的时间(秒)
cacheInvalid=200
#检查缓存失效的频率(秒)
clearCache=500
#updateCache 缓存更新策略 0-增删改之后会清除缓存 但直到下次操作之后才会重新读取缓存 1-立即读取缓存
updateCache=0
#数据库得到锁的超时时间(秒)
lockTimeOut=3
#数据库得不到锁的重复尝试次数(次) 默认不得小于10 不得大于100
lockTryTime=10
#自动释放锁的时间 防止数据库出现死锁 单位(秒) 范围为3-100
releaseLockTime=10
#轮询判断是否释放锁的周期 (秒) 默认不会大于releaseLockTime
checkLockTime=10
#文件名配置
[name]
#数据库名为新建的目录 不需要另外写文件了
#数据库表文件后缀
table=.table
#数据库B+索引文件后缀
index=.bpx
#数据库哈希索引文件后缀
hash=.hash
#整体的配置文件
rootSet=AvenirSQL.json
#每个数据库的配置文件
dbSet=.json
#AvenirSQL管理程序的配置
[curl]
#数据库导出功能的配置
[dump]
path=./dump
#是否先删表再导入表 默认是先删除再导入
notForce=false
#日志系统配置
[log]
#日志输出目录
logPath=./log
#写入日志的周期(秒)
loopTime=10
#日志文件名
logName=AvenirSQL
技术没有高低贵贱之分,脑海中如果有想法的话,我们要做的就是去把它实现。
编程之路漫漫修远兮,吾将上下而求索。
谢谢。
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。
1. 开源生态
2. 协作、人、软件
3. 评估模型