dbcached v1.0.8
介绍
这是一个基于mongoose和redis的数据库缓存方案dbcached, 主要目的为减少查询直接访问数据库的次数. 只针对单表系统, 不处理表关联, 不处理字段选取. 比较适合记录数据内容不是大文章类型, restful方案的项目. 链接地址https://github.com/windsome/dbcached
应用程序调用api时,首先从redis缓存中获取数据,未获取到向数据库获取,并更新到redis缓存,通过api返回. 此程序库有两套接口, 1. 直接调用mongoose的数据库方法. 2. 使用redis缓存.
实现方案
createDbOps
返回的是直接对数据库操作的接口. createCachedOps
返回的是带缓存的接口,主要优化的是查询缓存方面.
redis缓存中,存在3类键用来加速.
- d键, 单条记录的数据, 将数据转成字符串, (有性能损耗)
- key_d = generateRedisKey(model, 'd', item._id), 如: "device##d##612edab50a3d97e9130af795"
- 设置 $r.setexAsync(key_d, EX_SECONDS, JSON.stringify(item))
- s键, 记录查询结果id列表的键
- key_s = generateRedisKey(model, 's', where, sort), 如: "device##s##{\"name\":\"$regex-/Jack/i\"}##"
- 设置 $r.zaddAsync(key_s, ...argsArray);
- 可以根据返回的index作为score值,记录分页数据.
- c键, 记录查询结果的个数. db的count查询比较费时间, 所以用了matcher算法去自行增减.
- key_c = generateRedisKey(model, 'c', where, sort)
- 设置 $r.setexAsync(key_c, EX_SECONDS, data_c)
主要优化逻辑在cached/index的findCreator函数中, 主要过程就是判断该查询是否存在,数据是否满足,满足直接返回cache, 否则进行数据查询, 并更新到cache中. 创建/更新/删除, 通过matcher判断对哪些查询产生影响, 并更新相应的查询.
具体用例请参考test目录.
test/mockdata.js
下的cfg是配置,修改成自己的.
schemas
是测试表结构.
run-cache-find.js
是测试文件, 执行node -r esm run-cache-find.js
看结果.
注意: 需要先运行yarn compile编译到lib目录. 该测试引用的是lib目录文件.
安装使用
## 安装
yarn add dbcache
## db方法
import { createDbOps } from 'dbcache'
import schemas from './test/schemas';
const dbops = createDbOps('mongodb://admin:admin@localhost:27017/testcache?authSource=admin', schemas);
let items = await dbops.find('device', {where:{name:'王老板'}});
console.log('items', items);
await dbops.destroy(); //释放.
## redis方法
import { createDbOps, createCachedOps } from 'dbcache'
import schemas from './test/schemas';
const dbops = createDbOps('mongodb://admin:admin@localhost:27017/testcache?authSource=admin', schemas);
let ops = await createCachedOps({
url: 'redis://localhost:6379/1',
dbops,
});
let items = await ops.find('device', {where:{name:'王老板'}});
console.log('items', items);
await ops.destroy() // 释放
await dbops.destroy() // 释放
接口
export interface DbOps {
createOne: (model: string, data: JsonData) => Promise<JsonData | null>;
createMany: (model: string, items: JsonData[]) => Promise<JsonData[] | null>;
find: (model: string, options: QueryOptions) => Promise<JsonData[]>;
updateOne: (
model: string,
where: JsonData,
data: JsonData,
options?: JsonData,
) => Promise<JsonData | null>;
updateMany: (
model: string,
where: JsonData,
data: JsonData,
options?: JsonData,
) => Promise<JsonData[]>;
deleteOne: (
model: string,
where: JsonData,
options?: JsonData,
) => Promise<JsonData | null>;
deleteMany: (
model: string,
where: JsonData,
options?: JsonData,
) => Promise<number>;
count: (model: string, options: QueryOptions) => Promise<number>;
deleteOneById: (model: string, id: string) => Promise<JsonData | null>; //deleteOne
updateOneById: (
model: string,
id: string,
data: JsonData,
options?: JsonData,
) => Promise<JsonData | null>; // updateOne
findOne: (
model: string,
where: JsonData,
options?: JsonData,
) => Promise<JsonData>; // retrieve
findOneById: (
model: string,
id: string,
options?: JsonData,
) => Promise<JsonData>; // findOne
destroy: () => Promise<boolean>;
}
存在的问题
- db返回数据做了过多费性能处理.
- 从db接口返回的数据都做了 JSON.parse(JSON.stringify(item/items))处理,使得返回的ObjectId都转成了字符串, 这会损失性能, 未来需要优化.
- 当前这么做的原因是,无法确切知道ObjectId的类型,用typeof()获得的是object object,无法与其他的区分, 导致在做matcher时, 无法匹配此类型.
- 未来找到界定ObjectId类型的方法后,可以参考regexp正则表达式的处理方法,进行正反转换.
- 批量更新,删除数据时,检测哪些查询过期了,目前是将每个查询与数据运算,判断数据是否满足查询. 未来,可以直接将查询条件求交集,如果有交集则表示该查询应该更新,count也需要重新查询更新.
- 目前未防止缓存数据直接穿透到数据库. 当某个查询过期时, 目前直接将该查询的s键删除无效, 未来可以将该键插入待更新队列, 由更新任务异步去更新.
- 分页查询, 部分页面过期了, 导致数据穿透, 是否有优化空间?
- 数据库操作应该转成队列执行, 相连接的相同查询且中间没有更新/创建/删除命令的可以合并成一个,减少查询次数. 接口则等待数据返回.
注意的问题
如果使用此库的微服务有多片同时运行,或者多个微服务操作相同的数据库,则涉及到redis-key重入问题. 建议使用相同数据库的服务进行整理,将数据库操作部分抽象出来,统一整理到数据存取服务. 该服务,支持事务操作(数据库事务转为程序逻辑事务),原子操作等.
目的是优化查询的速度,相同的查询,不必再走数据库. 如果业务逻辑比较复杂, 是否还有必要去做? 是不是再往下细究, 发现就他妈是数据库的功能了? 变成自己实现数据库中缓存功能了? 还有必要去优化吗?
还是有必要的, 这里要把查询和增删改区分开. 考虑数据调用场景: 1. 涉及订单,交易等的调用,一般希望用事务解决,保证原子性,正确性. 比如, 客户扣钱给商家. 这个过程中查询和更新交叉调用. 2. 看新闻, 一般只有查询. 3. 点赞,评论,对文章点赞,评论数有影响,一般慢一点也不要紧.
注意事项
用node运行测试程序时,可能遇到不能识别import的问题. 需要
yarn add -D esm
, 然后运行node -r esm <js文件>
.当需要
npm publish
时, 且机器上默认使用阿里源, 需要指定源推送到npmjs上.npm publish --registry https://registry.npmjs.org
发布或更新也可以遵循如下操作
nrm use npm
切换到npm源- 如果更新:
npm version major
升大版本,或者npm version minor/patch
为升中小版本. - 发布
npm publish
- npm好像
1.0.0
以上的版本才可以用
- 可以用nrm源管理工具,方便切换源. 见https://github.com/Pana/nrm