资讯专栏INFORMATION COLUMN

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

dackel / 443人阅读

摘要:但添加的和方法很难达到较高的数据一致性,若要追求很强的数据一致性就用它们所对应的的和。能保证很好的数据一致性,但也仅限于修改数据时是查询出来再,若是直接也做不到数据一致性。

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

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

本文要用到的一些库为 Mongoose、Sequelize、ioredis 和 lodash。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 较差,当然这里很多细节考虑得不够完善,可根据业务合理调整。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/36321.html

相关文章

  • Redis 缓存之六

    摘要:文件的缺点如果机器发生故障,那么会导致丢失的数据更多每次在子线程来执行快照文件的时候,如果数据文件特别的大,会导致客户端提供的服务暂停数毫秒以至于数秒。 Redis 数据持久化RDB和AOF介绍 (1)RDB:对Redis的数据进行周期性的持久化 (2)AOF:对每一条写入指令作为日志,以append-only的模式写入到一个日志文件之中,在Redis重新启动的时候,可以通过AOF日志...

    svtter 评论0 收藏0
  • 【Bugly干货分享】TRIM:提升磁盘性能,缓解Android卡顿

    摘要:长期使用手机必将产生大量的磁盘碎片,而磁盘碎片将会降低磁盘的读写性能,从而影响系统流畅度。相对于方案一,该方案总体耗时较长,但不会影响正常操作时的磁盘性能。主动调用后,可以发现卡的效率指标均恢复至接近原始值水平但仍未完全达到初始状态的水平。 Bugly 技术干货系列内容主要涉及移动开发方向,是由 Bugly 邀请腾讯内部各位技术大咖,通过日常工作经验的总结以及感悟撰写而成,内容均属原创...

    bovenson 评论0 收藏0
  • 如何建设高吞吐量日志平台

    摘要:对于七牛云的实践来说,你也可以把容器内的日志投递出去。七牛云日志平台面临的挑战七牛云是如何解决日志平台吞吐量的挑战的呢首先,的集群通过扩展,是能够承接百级别的数据的那么问题的瓶颈就变成了如何将百量级的数据从中写入到上。 showImg(https://segmentfault.com/img/remote/1460000015879375?w=640&h=235); 上图列举了一些日...

    AlexTuan 评论0 收藏0
  • 「回顾」网易数据基础平台建设

    摘要:特点有聚合运算相关算法,时序数据库相对于关系型数据库没有特别复杂的查询,最常见的使用类型是宽表使用,在此基础上做一些聚合算法插值查询。 首先简单介绍一下网易杭州研究院情况简介,如下图所示: showImg(https://segmentfault.com/img/bVbni6K?w=720&h=285); 我们公司主要从事平台技术开发和建设方面,工作的重点方向主要在解决用户在数据治理中...

    sevi_stuo 评论0 收藏0
  • [译] 设计一个现代缓存

    摘要:鉴于大多数场景里不同数据项使用的都是固定的过期时长,采用了统一过期时间的方式。缓存能复用驱逐策略下的队列以及下面将要介绍的并发机制,让过期的数据项在缓存的维护阶段被抛弃掉。的设计实现来自于大量的洞见和许多贡献者的共同努力。 本文来自阿里集团客户体验事业群 简直同学的投稿,简直基于工作场景对于缓存做了一些研究,并翻译了一篇文章供同道中人学习。 原文:http://highscalabi...

    hankkin 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<