在项目开发中,为了减轻数据库的 I/O 压力,加快请求的响应速度,缓存是经常使用到的技术。Redis 和 Memcache 是如今经常使用的两个用来作数据缓存的技术。数据缓存一些常见的作法是,让数据写入到数据库之后经过一些自动化的脚本自动同步到缓存,或者在向数据库写数据后再手动向缓存写一次数据。这些作法难免都有些繁琐,且代码也很差维护。我在写 Node.js 项目的时候,发现利用 Mongoose(一个 MongoDB 的 ODM)和 Sequelize(一个 MySQL 的 ORM)的一些功能特性可以优雅的作到让写入到 MongoDB/MySQL 的数据自动写入到 Redis,而且在作查询操做的时候可以自动地优先从缓存中查找数据,若缓存中找不到才进入 DB 中查找,并将 DB 中找到的数据写入缓存。javascript
本文不讲解 Mongoose 和 Sequelize 的基本用法,这里只讲解如何作到上面所说的自动缓存。java
本文要用到的一些库为 Mongoose、Sequelize、ioredis 和 lodash。Node.js 版本为 7.7.1。node
// redis.js const Redis = require('ioredis'); const Config = require('../config'); const redis = new Redis(Config.redis); module.exports = redis;
上面文件的代码主要用于链接 redis。mysql
// mongodb.js const mongoose = require('mongoose'); mongoose.Promise = global.Promise; const demoDB = mongoose.createConnection('mongodb://127.0.0.1/demo', {}); module.exports = demoDB;
上面是链接 mongodb 的代码。redis
// mongoBase.js const mongoose = require('mongoose'); const Schema = mongoose.Schema; const redis = require('./redis'); function baseFind(method, params, time) { const self = this; const collectionName = this.collection.name; const dbName = this.db.name; const redisKey = [dbName, collectionName, JSON.stringify(params)].join(':'); const expireTime = time || 3600; return new Promise(function (resolve, reject) { redis.get(redisKey, function (err, data) { if (err) return reject(err); if (data) return resolve(JSON.parse(data)); self[method](params).lean().exec(function (err, data) { if (err) return reject(err); if (Object.keys(data).length === 0) return resolve(data); redis.setex(redisKey, expireTime, JSON.stringify(data)); resolve(data); }); }); }); } const Methods = { findCache(params, time) { return baseFind.call(this, 'find', params, time); }, findOneCache(params, time) { return baseFind.call(this, 'findOne', params, time); }, findByIdCache(params, time) { return baseFind.call(this, 'findById', params, time); }, }; const BaseSchema = function () { this.defaultOpts = { }; }; BaseSchema.prototype.extend = function (schemaOpts) { const schema = this.wrapMethods(new Schema(schemaOpts, { toObject: { virtuals: true }, toJSON: { virtuals: true }, })); return schema; }; BaseSchema.prototype.wrapMethods = function (schema) { schema.post('save', function (data) { const dbName = data.db.name; const collectionName = data.collection.name; const redisKey = [dbName, collectionName, JSON.stringify(data._id)].join(':'); redis.setex(redisKey, 3600, JSON.stringify(this)); }); Object.keys(Methods).forEach(function (method) { schema.statics[method] = Methods[method]; }); return schema; }; module.exports = new BaseSchema();
上面的代码是在用 mongoose 建模的时候,全部的 schema 都会继承这个 BaseSchema。这个 BaseSchema 里面就为全部继承它的 schema 添加了一个模型在执行 save 方法后触发的文档中间件,这个中间件的做用就是在数据被写入 MongoDB 后再自动写入 redis。而后还为每一个继承它的 schema 添加了三个静态方法,分别是 findByIdCache、findOneCache 和 findCache,它们分别是 findById、findOne 和 find 方法的扩展,只是不一样之处在于用添加的三个方法进行查询时会根据传入的条件先从 redis 中查找数据,若查到就返回数据,若查不到就继续调用所对应的的原生方法进入 MongoDB 中查找,若在 MongoDB 中查到了,就把查到的数据写入 redis,供之后的查询使用。添加的这三个静态方法的调用方法和他们所对应的的原生方法一致,只是能够多传入一个时间,用来设置数据在缓存中的过时时间。sql
// userModel.js const BaseSchema = require('./mongoBase'); const mongoDB = require('./mongodb.js'); const userSchema = BaseSchema.extend({ name: String, age: Number, addr: String, }); module.exports = mongoDB.model('User', userSchema, 'user');
这是为 user 集合建的一个模型,它就经过 BaseSchema.extend 方法继承了上面说到的中间件和静态方法。mongodb
// index.js const UserModel = require('./userModl'); const action = async function () { const user = await UserModel.create({ name: 'node', age: 7, addr: 'nodejs.org' }); const data1 = await UserModel.findByIdCache(user._id.toString()); const data2 = await UserModel.findOneCache({ age: 7 }); const data3 = await UserModel.findCache({ name: 'node', age: 7 }, 7200); return [ data1, data2, data3]; }; action().then(console.log).catch(console.error);
上面的的代码就是向 User 集合中写了一条数据后,而后依次调用了咱们本身添加的三个用于查询的静态方法。把 redis 的 monitor 打开,发现代码已经按咱们预想的那样执行了。数据库
总结,上面的方案主要经过 Mongoose 的中间件和静态方法达到了咱们想要的功能。但添加的 findOneCache 和 findCache 方法很难达到较高的数据一致性,若要追求很强的数据一致性就用它们所对应的的 findOne 和 find。findByIdCache 能保证很好的数据一致性,但也仅限于修改数据时是查询出来再 save,如果直接 update 也作不到数据一致性。npm
// mysql.js const Sequelize = require('sequelize'); const _ = require('lodash'); const redis = require('./redis'); const setCache = function (data) { if (_.isEmpty(data) || !data.id) return; const dbName = data.$modelOptions.sequelize.config.database; const tableName = data.$modelOptions.tableName; const redisKey = [dbName, tableName, JSON.stringify(data.id)].join(':') redis.setex(redisKey, 3600, JSON.stringify(data.toJSON())); }; const sequelize = new Sequelize('demo', 'root', '', { host: 'localhost', port: 3306, hooks: { afterUpdate(data) { setCache(data); }, afterCreate(data) { setCache(data); }, }, }); sequelize .authenticate() .then(function () { console.log('Connection has been established successfully.'); }) .catch(function (err) { console.error('Unable to connect to the database:', err); }); module.exports = sequelize;
上面的代码主要做用是链接 MySQL 并生成 sequelize 实例,在构建 sequelize 实例的时候添加了两个钩子方法 afterUpdate 和 afterCreate。afterUpdate 用于在模型实例更新后执行的函数,注意必须是模型实例更新才会触发此方法,若是是直接相似 Model.update 这种方式更新是不会触发这个钩子函数的,只能是一个已经存在的实例调用 save 方法的时候会触发这个钩子。afterCreate 是在模型实例建立后调用的钩子函数。这两个钩子的主要目的就是用来当一条数据被写入到 MySQL 后,再自动的写入到 redis,即实现自动缓存。缓存
// mysqlBase.js const _ = require('lodash'); const Sequelize = require('sequelize'); const redis = require('./redis'); function baseFind(method, params, time) { const self = this; const dbName = this.sequelize.config.database; const tableName = this.name; const redisKey = [dbName, tableName, JSON.stringify(params)].join(':'); return (async function () { const cacheData = await redis.get(redisKey); if (!_.isEmpty(cacheData)) return JSON.parse(cacheData); const dbData = await self[method](params); if (_.isEmpty(dbData)) return {}; redis.setex(redisKey, time || 3600, JSON.stringify(dbData)); return dbData; })(); } const Base = function (sequelize) { this.sequelize = sequelize; }; Base.prototype.define = function (model, attributes, options) { const self = this; return this.sequelize.define(model, _.assign({ id: { type: Sequelize.UUID, primaryKey: true, defaultValue: Sequelize.UUIDV1, }, }, attributes), _.defaultsDeep({ classMethods: { findByIdCache(params, time) { this.sequelize = self.sequelize; return baseFind.call(this, 'findById', params, time); }, findOneCache(params, time) { this.sequelize = self.sequelize; return baseFind.call(this, 'findOne', params, time); }, findAllCache(params, time) { this.sequelize = self.sequelize; return baseFind.call(this, 'findAll', params, time); }, }, }, options)); }; module.exports = Base;
上面的代码同以前的 mongoBase 的做用大体同样。在 sequelize 建模的时候,全部的 schema 都会继承这个 Base。这个 Base 里面就为全部继承它的 schema 添加了三个静态方法,分别是 findByIdCache、findOneCahe 和 findAllCache,它们的做用和以前 mongoBase 中的那三个方法做用同样,只是为了和 sequelize 中原生的 findAll 保持一致,findCache 在这里变成了 findAllCache。在 sequelize 中为 schema 添加类方法(classMethods),即至关于在 mongoose 中为 schema 添加静态方法(statics)。
// mysqlUser.js const Sequelize = require('sequelize'); const base = require('./mysqlBase.js'); const sequelize = require('./mysql.js'); const Base = new base(sequelize); module.exports = Base.define('user', { name: Sequelize.STRING, age: Sequelize.INTEGER, addr: Sequelize.STRING, }, { tableName: 'user', timestamps: true, });
上面定义了一个 User schema,它就从 Base 中继承了findByIdCache、findOneCahe 和 findAllCache。
const UserModel = require('./mysqlUser'); const action = async function () { await UserModel.sync({ force: true }); const user = await UserModel.create({ name: 'node', age: 7, addr: 'nodejs.org' }); await UserModel.findByIdCache(user.id); await UserModel.findOneCache({ where: { age: 7 }}); await UserModel.findAllCache({ where: { name: 'node', age: 7 }}, 7200); return 'finish'; }; action().then(console.log).catch(console.error);
总结,经过 sequelize 实现的自动缓存方案和以前 mongoose 实现的同样,也会存在数据一致性问题,findByIdCache 较好,findOneCache 和 findAllCache 较差,固然这里不少细节考虑得不够完善,可根据业务合理调整。