讓寫入數據庫的數據自動寫入緩存

在項目開發中,爲了減輕數據庫的 I/O 壓力,加快請求的響應速度,緩存是經常使用到的技術。RedisMemcache 是如今經常使用的兩個用來作數據緩存的技術。數據緩存一些常見的作法是,讓數據寫入到數據庫之後經過一些自動化的腳本自動同步到緩存,或者在向數據庫寫數據後再手動向緩存寫一次數據。這些作法難免都有些繁瑣,且代碼也很差維護。我在寫 Node.js 項目的時候,發現利用 Mongoose(一個 MongoDB 的 ODM)和 Sequelize(一個 MySQL 的 ORM)的一些功能特性可以優雅的作到讓寫入到 MongoDB/MySQL 的數據自動寫入到 Redis,而且在作查詢操做的時候可以自動地優先從緩存中查找數據,若緩存中找不到才進入 DB 中查找,並將 DB 中找到的數據寫入緩存。javascript

本文不講解 Mongoose 和 Sequelize 的基本用法,這裏只講解如何作到上面所說的自動緩存。java

本文要用到的一些庫爲 MongooseSequelizeioredislodash。Node.js 版本爲 7.7.1。node

在 MongoDB 中實現自動緩存

// 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 中實現自動緩存

// 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 較差,固然這裏不少細節考慮得不夠完善,可根據業務合理調整。

相關文章
相關標籤/搜索