当前位置: 首页 > 后端技术 > Node.js

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

时间:2023-04-03 22:40:59 Node.js

在项目开发中,为了降低数据库的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.jsconstRedis=require('ioredis');constConfig=require('../config');constredis=newRedis(Config.redis);module.exports=redis;上面文件中的代码主要是用来连接redis的。//mongodb.jsconstmongoose=require('mongoose');mongoose.Promise=global.Promise;constdemoDB=mongoose.createConnection('mongodb://127.0.0.1/demo',{});module.exports=演示数据库;以上是连接mongodb的代码。//mongoBase.jsconstmongoose=require('mongoose');constSchema=mongoose.Schema;constredis=require('./redis');functionbaseFind(method,params,time){constself=this;constcollectionName=this.collection.name;constdbName=this.db.name;constredisKey=[dbName,collectionName,JSON.stringify(params)].join(':');constexpireTime=时间||3600;returnnewPromise(function(resolve,reject){redis.get(redisKey,function(err,data){if(err)returnreject(err);if(data)returnresolve(JSON.parse(data));self[方法](params).lean().exec(function(err,data){if(err)returnreject(err);if(Object.keys(data).length===0)returnresolve(data);redis.setex(redisKey,expireTime,JSON.stringify(data));resolve(data);});});});}constMethods={findCache(params,time){returnbaseFind.call(this,'查找',参数,时间);},findOneCache(params,time){返回baseFind.call(this,'findOne',参数,时间);},findByIdCache(params,time){returnbaseFind.call(this,'findById',params,time);},};constBaseSchema=function(){this.defaultOpts={};};BaseSchema.prototype.extend=function(schemaOpts){constschema=this.wrapMethods(newSchema(schemaOpts,{toObject:{virtuals:true},toJSON:{virtuals:true},}));返回模式;};BaseSchema.prototype.wrapMethods=function(schema){schema.post('save',function(data){constdbName=data.db.name;constcollectionName=data.collection.name;constredisKey=[dbName,collectionName,JSON.stringify(data._id)].join(':');redis.setex(redisKey,3600,JSON.stringify(this));});目的。keys(Methods).forEach(function(method){schema.statics[method]=Methods[method];});返回架构;};module.exports=newBaseSchema();上面的代码是用mongoose建模的时候,所有的schema都会继承这个BaseSchema此BaseSchema添加了一个文档中间件,该中间件在为所有继承它的模式执行保存方法后触发。这个中间件的作用是数据写入MongoDB后自动写入redis。然后在每个继承它的schema中添加三个静态方法,分别是findByIdCache、findOneCache和findCache,它们是findById、findOne和find方法的扩展,但不同的是,用添加的三个方法进行查询时,会先从中查找数据redis根据传入的条件,找到就返回数据,没有找到就继续调用相应的native方法去MongoDB中查找,找到就去MongoDB中查找。数据写入redis,供以后查询。新增的三个静态方法的调用方式与对应的native方法相同,只是可以额外传入一个时间来设置缓存中数据的过期时间。//userModel.jsconstBaseSchema=require('./mongoBase');constmongoDB=require('./mongodb.js');constuserSchema=BaseSchema.extend({name:String,age:Number,addr:String,});module.exports=mongoDB.model('User',userSchema,'user');这是一个为用户集合构建的模型,通过BaseSchema.extend方法继承了上面提到的中间件和静态方法。//index.jsconstUserModel=require('./userModl');constaction=asyncfunction(){constuser=awaitUserModel.create({name:'node',age:7,addr:'nodejs.org'});constdata1=awaitUserModel.findByIdCache(user._id.toString());constdata2=awaitUserModel.findOneCache({age:7});constdata3=awaitUserModel.findCache({name:'node',age:7},7200);返回[data1,data2,data3];};action().then(console.log).catch(console.error);上面的代码是往User集合中写入一条数据,然后依次调用我们添加的三个静态方法进行查询。打开redis的monitor,发现代码已经按照我们的预期执行了。综上所述,上述方案主要是通过Mongoose的中间件和静态方法来实现我们想要的功能。但是新增的findOneCache和findCache方法很难做到数据的高一致性。如果要追求数据强一致性,就用他们对应的findOne和find。findByIdCache可以保证良好的数据一致性,但仅限于修改数据时的查询和保存。如果直接更新,则无法实现数据一致性。在MySQL中实现自动缓存//mysql.jsconstSequelize=require('sequelize');const_=require('lodash');constredis=require('./redis');constsetCache=function(data){if(_.isEmpty(data)||!data.id)返回;constdbName=data.$modelOptions.sequelize.config.database;consttableName=data.$modelOptions.tableName;constredisKey=[dbName,tableName,JSON.stringify(data.id)].join(':')redis.setex(redisKey,3600,JSON.stringify(data.toJSON()));};constsequelize=newSequelize('demo','root','',{host:'localhost',port:3306,hooks:{afterUpdate(data){setCache(data);},afterCreate(data){setCache(data);},},});sequelize.authenticate().then(function(){console.log('已成功建立连接。');}).catch(function(err){console.error('无法连接数据库:',呃);});module.exports=续集;以上代码的主要作用是连接MySQL并生成sequelize实例。构建sequelize实例时,增加了两个钩子方法afterUpdate和afterCreateafterUpdate用于在更新模型实例后执行函数。请注意,必须在更新模型实例时触发此方法。如果update直接类似于Model.update,则不会触发这个钩子函数,只能在一个已经存在的实例调用save方法时触发这个钩子。afterCreate是在创建模型实例后调用的钩子函数。这两个hook的主要目的是在一条数据写入mysql后自动写入redis,也就是实现自动缓存。//mysqlBase.jsconst_=require('lodash');constSequelize=require('sequelize');constredis=require('./redis');functionbaseFind(method,params,time){constself=this;constdbName=this.sequelize.config.database;consttableName=this.name;constredisKey=[dbName,tableName,JSON.stringify(params)].join(':');return(asyncfunction(){constcacheData=awaitredis.get(redisKey);if(!_.isEmpty(cacheData))returnJSON.parse(cacheData);constdbData=awaitself[method](params);if(_.isEmpty(dbData))return{};redis.setex(redisKey,time||3600,JSON.stringify(dbData));returndbData;})();}constBase=function(sequelize){this.sequelize=sequelize;};Base.prototype.define=function(model,attributes,options){constself=this;返回this.sequelize.define(model,_.assign({id:{type:Sequelize.UUID,primaryKey:true,defaultValue:Sequelize.UUIDV1,},},attributes),_.defaultsDeep({classMethods:{findByIdCache(params,time){this.sequelize=self.sequelize;returnbaseFind.call(this,'findById',params,time);},findOneCache(params,time){this.sequelize=self.sequelize;returnbaseFind.call(this,'findOne',params,time);},findAllCache(params,time){this.sequelize=self.sequelize;returnbaseFind.call(this,'findAll',参数,时间);},},},选项));};module.exports=Base;上面的代码和前面的mongoBase的功能大致相同。在sequelize中建模时,所有的schema都会继承这个Base。这个Base为所有继承它的schema添加了三个静态方法,分别是findByIdCache、findOneCache和findAllCache。它们的作用和mongoBase中前面三个方法的作用是一样的,只是为了和sequelize中原来的findAll保持一致,这里的findCache变成了findAllCache。在sequelize中为schema添加类方法(classMethods)等同于在mongoose中为schema添加静态方法(statics)。//mysqlUser.jsconstSequelize=require('sequelize');constbase=require('./mysqlBase.js');constsequelize=require('./mysql.js');constBase=newbase(sequelize);module.exports=Base.define('user',{name:Sequelize.STRING,age:Sequelize.INTEGER,addr:Sequelize.STRING,},{tableName:'user',timestamps:true,});上面定义的User模式一旦创建,它就会从Base继承findByIdCache、findOneCache和findAllCache。constUserModel=require('./mysqlUser');constaction=asyncfunction(){awaitUserModel.sync({force:true});constuser=awaitUserModel.create({name:'node',age:7,addr:'nodejs.org'});等待UserModel.findByIdCache(user.id);awaitUserModel.findOneCache({where:{age:7}});awaitUserModel.findAllCache({where:{name:'node',age:7}},7200);return'finish';};action().then(console.log).catch(console.error);总结一下,sequelize实现的自动缓存方案和之前mongoose实现的自动缓存方案一样,也会存在数据一致性问题。findByIdCache更好,findOneCache和findAllCache更差。当然这里很多细节没有考虑好,可以根据业务合理调整。