14 Star 74 Fork 9

AvenirTech 未来科技 / AvenirSQL

Create your Gitee Account
Explore and code with more than 6 million developers,Free private repositories !:)
Sign up
Clone or Download
Cancel
Notice: Creating folder will generate an empty file .keep, because not support in Git
Loading...
README.md

AvenirSQL

介绍

用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

技术特点

  • 1.支持增删改查
  • 2.精确查找支持哈希索引,范围查找支持B+树索引
  • 3.智能缓存,提升QPS性能
  • 4.提供用户管理,cli程序(curl.js)
  • 5.实现串行锁功能
  • 6.灵活的策略配置

代码结构

./database/ : AvenirSQL核心实现

./AvenirSQL.js : AvenirSQL启动程序

./curl.js : cli程序

./dtest.js : 测试程序

./jmeter/ : Jmeter测试用例文件

./db/ : 数据库和表数据

./run.ini : 数据库配置文件

./AvenirSQL/ : Nodejs版的AvenirSQL操作库

以下是项目中使用的Avenir开源组织开发的Node模块:

  1. Nodejs通用方法库 libcu
  2. Avenir日志库 avenir-log
  3. Nodejs对文件某一行的操作库 nth-file
  4. Nodejs多叉树和字典树实现 multiple-tree

支持的数据类型

  1. number

数字,默认长度10

  1. int

其实也是数字 并不是整数,默认长度10

  1. bignumber

不限制长度的数字

  1. varchar

字符型 默认长度20

  1. string

字符型 默认长度20

  1. bigstring

不限制长度的字符

具体技术实现

  • 1.统一的底层错误处理函数,避免函数重复传递参数。
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 就行 
    }
}
  1. select distinct

在返回了结果集之后,根据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;
    }
  1. 导入导出

导出即执行select操作,并将SQL语句转换为insert语句。

导入则按分隔符执行SQL语句

导出文件名规范:dump[table || database]_dbname_tableName_YYYYMMDDHHmmss.sql

接口规范

数据库连接使用tcp通信,传输文本为JSON数据格式。

  1. 登录
{
    user:"root",
    password:"123456"
    type:"login"
}

返回值:

{
    code:0,
    message:"success",
    data:"ef3d843f26c4e900e9ab4979f324d5571a4cb5f5c011278b36985b2802c828185ad0bdd7e19390cfffe479afe1b09d1c"
}
  1. 数据库SQL操作
{
    sign:"ef3d843f26c4e900e9ab4979f324d5571a4cb5f5c011278b36985b2802c828185ad0bdd7e19390cfffe479afe1b09d1c",
    type:"sql",
    data:"select * from test"

}

返回值:

{
    code : 0,
    message:"success",
    data:[{
        name:"test",
        id:"1",
    }]
}
  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"
    },

}

SQL规范

  1. 暂不支持join、 like、 group by

  2. show命令

show database 展示目前所有的数据库列表

show table [dbname] 展示该数据库下的所有表名,不输入dbname会去获取默认值

show dump 展示默认的导出目录下所有的文件名

{"code":0,"message":"success","data":["home_def","hot","t"]}
  1. update

不支持不带where条件的全表更新 如update t set a = '1'

  1. distinct

distinc a,b,c 即代表数据库里面a,b,c三个列都重复才会判断重复。

通过多叉树模块multiple-tree来实现

  1. dump

dump table dbname.tableName as select * from tableName where a > 10000

如果不带SQL语句,则导出全表,在配置文件配置是否是覆盖导出和导入

配置文件示例

文件名必须是run.ini,在安装目录下

若找不到此文件,AvenirSQL会自动创建默认的配置文件,内容如下:

  1. exec 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

总结

技术没有高低贵贱之分,脑海中如果有想法的话,我们要做的就是去把它实现。

编程之路漫漫修远兮,吾将上下而求索。

谢谢。

Repository Comments ( 17 )

Sign in to post a comment

About

用Node.js设计一个数据库 expand collapse
NodeJS and 4 more languages
GPL-2.0
Cancel

Releases (12)

All

AvenirSQL

Contributors

All

Activities

Load More
can not load any more
NodeJS
1
https://git.oschina.net/onlyyyy/AvenirSQL.git
git@git.oschina.net:onlyyyy/AvenirSQL.git
onlyyyy
AvenirSQL
AvenirSQL
master

Search

161121 f78d6d6f 1850385 154831 86f8c370 1850385