让写入数据库的数据自动写入缓存

溢出露台
• 阅读 5624

在项目开发中,为了减轻数据库的 I/O 压力,加快请求的响应速度,缓存是常用到的技术。RedisMemcache 是现在常用的两个用来做数据缓存的技术。数据缓存一些常见的做法是,让数据写入到数据库以后通过一些自动化的脚本自动同步到缓存,或者在向数据库写数据后再手动向缓存写一次数据。这些做法不免都有些繁琐,且代码也不好维护。我在写 Node.js 项目的时候,发现利用 Mongoose(一个 MongoDB 的 ODM)和 Sequelize(一个 MySQL 的 ORM)的一些功能特性能够优雅的做到让写入到 MongoDB/MySQL 的数据自动写入到 Redis,并且在做查询操作的时候能够自动地优先从缓存中查找数据,若缓存中找不到才进入 DB 中查找,并将 DB 中找到的数据写入缓存。

本文不讲解 Mongoose 和 Sequelize 的基本用法,这里只讲解如何做到上面所说的自动缓存。

本文要用到的一些库为 MongooseSequelizeioredislodash。Node.js 版本为 7.7.1。

在 MongoDB 中实现自动缓存

// redis.js
const Redis = require('ioredis');
const Config = require('../config');

const redis = new Redis(Config.redis);

module.exports = redis;

上面文件的代码主要用于连接 redis。

// mongodb.js
const mongoose = require('mongoose');

mongoose.Promise = global.Promise;
const demoDB = mongoose.createConnection('mongodb://127.0.0.1/demo', {});

module.exports = demoDB;

上面是连接 mongodb 的代码。

// 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,供以后的查询使用。添加的这三个静态方法的调用方法和他们所对应的的原生方法一致,只是可以多传入一个时间,用来设置数据在缓存中的过期时间。

// 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 方法继承了上面说到的中间件和静态方法。

// 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 也做不到数据一致性。

在 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 较差,当然这里很多细节考虑得不够完善,可根据业务合理调整。

点赞
收藏
评论区
推荐文章
blmius blmius
4年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
美凌格栋栋酱 美凌格栋栋酱
7个月前
Oracle 分组与拼接字符串同时使用
SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Easter79 Easter79
3年前
sql注入
反引号是个比较特别的字符,下面记录下怎么利用0x00SQL注入反引号可利用在分隔符及注释作用,不过使用范围只于表名、数据库名、字段名、起别名这些场景,下面具体说下1)表名payload:select\from\users\whereuser\_id1limit0,1;!(https://o
Stella981 Stella981
3年前
Linux查看GPU信息和使用情况
1、Linux查看显卡信息:lspci|grepivga2、使用nvidiaGPU可以:lspci|grepinvidia!(https://oscimg.oschina.net/oscnet/36e7c7382fa9fe49068e7e5f8825bc67a17.png)前边的序号"00:0f.0"是显卡的代
Wesley13 Wesley13
3年前
FLV文件格式
1.        FLV文件对齐方式FLV文件以大端对齐方式存放多字节整型。如存放数字无符号16位的数字300(0x012C),那么在FLV文件中存放的顺序是:|0x01|0x2C|。如果是无符号32位数字300(0x0000012C),那么在FLV文件中的存放顺序是:|0x00|0x00|0x00|0x01|0x2C。2.  
Stella981 Stella981
3年前
KVM调整cpu和内存
一.修改kvm虚拟机的配置1、virsheditcentos7找到“memory”和“vcpu”标签,将<namecentos7</name<uuid2220a6d1a36a4fbb8523e078b3dfe795</uuid
Easter79 Easter79
3年前
Twitter的分布式自增ID算法snowflake (Java版)
概述分布式系统中,有一些需要使用全局唯一ID的场景,这种时候为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点,首先他相对比较长,另外UUID一般是无序的。有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。而twitter的snowflake解决了这种需求,最初Twitter把存储系统从MySQL迁移
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Python进阶者 Python进阶者
1年前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这