利用MicroDiff获得更快的缓存验证

102 阅读6分钟

速度和性能是任何应用程序的一个重要方面,而提高应用程序速度的最流行的技术之一是实施缓存。

通过缓存,我们可以存储应用程序的数据,以便快速访问,避免重复运行昂贵的查询或应用逻辑。

缓存的一个重要方面是确保我们从缓存中发送的数据是相关的和最新的,这就导致我们要进行缓存验证。

缓存验证可以通过几种方式来处理,包括设置缓存过期时间或在应用数据发生变化时主动使数据失效。

在这篇文章中,我们将在一个简单的API应用中实现缓存,并在应用中处理缓存验证。为了验证我们的缓存数据,我们将利用一个叫做MicroDiff的简单差异库来帮助说明我们的缓存的相关性。

MicroDiff是一个轻量级的JavaScript库,用于检测对象和数组的差异,因其不到1kb的微小尺寸、速度和无依赖性而受到欢迎。

MicroDiff对于我们的缓存验证来说是一个很好的选择,因为它在提供清晰的差异检测的同时,不会对我们的应用程序造成不必要的膨胀。使用它,我们可以监听那些表明需要更新我们的缓存数据的数据变化,而不会拖慢我们的应用程序。

教程先决条件

要跟上这篇文章,请安装以下内容。

设置项目

在我们的项目中,我们将利用之前构建的简单的Express.js API,并对其进行扩展以实现缓存和缓存验证。我们将从克隆我们的API应用程序开始。

$ git clone git@github.com:ibywaks/cookbook.git 

接下来,让我们安装我们的缓存库,node-cache,和我们的差异库MicroDiff

$ yarn add microdiff node-cache

在我们的项目中设置缓存

接下来,我们将首先在我们的项目中用一个自定义的缓存类来封装我们的缓存库。这样我们就可以轻松地在缓存库之间切换,而不需要在我们的应用程序中进行大量的编辑。

# src/lib/local-cache.ts

import NodeCache from 'node-cache'

type CacheKey = string | number

class LocalCache {
    private static _instance: LocalCache
    private cache: NodeCache
    private constructor(ttlSeconds: number) {
        this.cache = new NodeCache({
            stdTTL: ttlSeconds,
            checkperiod: ttlSeconds * 0.2,
        })
    }
    public static getInstance(): LocalCache {
        if (!LocalCache._instance) {
            LocalCache._instance = new LocalCache(1000)
        }
        return LocalCache._instance
    }
    public get<T>(key: CacheKey): T | undefined {
        return this.cache.get(key)
    }
    public set<T>(key: CacheKey, data: T): void {
        this.cache.set(key, data)
    }
    public unset(key: CacheKey): void {
        this.cache.del(key)
    }
    public hasKey(key: CacheKey): boolean {
        return this.cache.has(key)
    }
}

export default LocalCache.getInstance()

缓存应用数据

当在一个应用程序中实施缓存以提高速度和性能时,考虑哪些数据应该被缓存是很重要的。对于那些应该被缓存以提高性能的数据,一些重要的标准是:。

  • 不经常变化的数据
  • 经常被取用/读取的数据
  • 从复杂的查询、逻辑或外部应用中得到的数据

在你的应用程序中实施缓存时,一个重要的考虑因素是专注于缓存那些读取频率高于变化频率的数据。

我们的食谱API有一个列出食谱的端点,用户可能会更频繁地阅读这些食谱,而不是添加新的食谱。出于这个原因,菜谱列表端点是一个很好的缓存候选者。

另外,烹饪书API的用户会经常阅读可用的食谱列表。因此,让我们为我们的GET recipes端点添加一些缓存。

首先,让我们开始更新控制器的缓存,一旦数据被检索到。

# src/api/controllers/recipes/index.ts

...
import localCache from '../../../lib/local-cache'

...
export const getAll = async (filters: GetAllRecipesFilters): Promise<Recipe[]> => {
    const recipes = await service.getAll(filters)
      .then((recipes) => recipes.map(mapper.toRecipe))

    if (recipes.length) {
        localCache.set(primaryCacheKey, recipes)
    }
    return recipes
}

接下来,我们将创建一个中间件来检查缓存中的数据是否存在。如果缓存的数据存在,我们将数据作为响应返回,否则我们将调用控制器来检索数据并保存到缓存中。

# src/lib/check-cache.ts

import { Request, Response, NextFunction } from 'express'
import LocalCache from './local-cache'

export const checkCache = (req: Request, res: Response, next: NextFunction) => {
    try {
        const {baseUrl, method} = req
        const [,,,cacheKey] = baseUrl.split('/')
        if (method === 'GET' && LocalCache.hasKey(cacheKey)) {
            const data = LocalCache.get(cacheKey)
            return res.status(200).send(data)
        }
        next()
    } catch (err) {
        // do some logging
        throw err
    }
}

在上面的片段中,我们从baseUrl ,也就是recipes ,从api/v1/recipes ,提取我们的资源名称。这个资源名称是我们检查缓存数据的关键。

接下来,我们将缓存检查器中间件添加到相应的路由中。

# src/api/routes/recipes.ts

...
import {checkCache} from '../../lib/check-cache'

const recipesRouter = Router()

recipesRouter.get('/', checkCache, async (req: Request, res: Response) => {
    const filters: GetAllRecipesFilters = req.query

    const results = await controller.getAll(filters)

    return res.status(200).send(results)
})

使用MicroDiff进行缓存验证

现在我们已经成功地缓存了我们最常访问的端点,/api/v1/recipes ,重要的是要处理食谱被改变或增加的情况,以确保我们的用户总是收到最相关的数据。

为了做到这一点,我们将通过监听我们sequelize模型的变化Hooks来监听我们的数据的最低级别的变化点。我们通过创建一个Hook定义并将其添加到我们的Sequelize初始化中来做到这一点。

# src/db/config.ts

...
import { SequelizeHooks } from 'sequelize/types/lib/hooks'
import localCache from '../lib/local-cache'

...

const hooks: Partial<SequelizeHooks<Model<any, any>, any, any>> = {
  afterUpdate: (instance: Model<any, any>) => {
    const cacheKey = `${instance.constructor.name.toLowerCase()}s`
    const currentData = instance.get({ plain: true })

    if (!localCache.hasKey(cacheKey)) {
      return
    }

    const listingData = localCache.get<any>(cacheKey) as any[]
    const itemIndex = listingData.findIndex(
      (it) => it.id === instance.getDataValue('id')
    )
    const oldItemData = ~itemIndex ? listingData[itemIndex] : {}

    const instanceDiff = diff(oldItemData, currentData)

    if (instanceDiff.length > 0) {
      listingData[itemIndex] = currentData
      localCache.set(cacheKey, listingData)
    }
  },
  afterCreate: (instance: Model<any, any>) => {
    const cacheKey = `${instance.constructor.name.toLowerCase()}s`
    const currentData = instance.get({ plain: true })

    if (!localCache.hasKey(cacheKey)) {
      return
    }

    const listingData = localCache.get<any>(cacheKey) as any[]

    listingData.push(currentData)
    localCache.set(cacheKey, listingData)
  },
}

const sequelizeConnection = new Sequelize(dbName, dbUser, dbPassword, {
  host: dbHost,
  dialect: dbDriver,
  logging: false,
  define: {hooks}
})

export default sequelizeConnection

上面我们在Sequelize实例上定义了Hooks,用来监听creates和updates。对于我们缓存的模型上的creates,我们只是用新的数据实例更新缓存对象。

对于带有我们正在缓存的数据的模型的更新,我们使用我们的microdiff 库来比较旧的缓存数据和更新的模型数据。这个库突出了数据的变化,并指出了更新缓存的需要。

在应用程序中应用缓存时,最重要的考虑因素之一是确保为用户提供当前有效的数据,这就是为什么我们应用缓存失效技术来确保我们的缓存数据是正确的,特别是当应用程序允许用户写入和更新数据源时。

在执行缓存失效时,常见的情况是缓存对变化过于敏感,这意味着每一次写或更新操作,无论是否明显改变数据,都转化为对我们缓存的更新。

避免缓存的敏感性是在我们的缓存验证中使用microdiff 的一个重要影响,所以我们可以通过首先检测到有变化要更新,来避免对我们的缓存进行不必要的写入或闪动。

总结

在这篇文章中,我们探讨了缓存,它的好处和对缓存数据的重要考虑。我们也经历了对缓存数据的验证,特别是使用Microdiff 来检查缓存数据的变化。

这篇文章的所有代码都可以在GitHub上找到。我希望你能在评论中分享你是如何处理缓存验证的,以及你在项目中使用的任何很酷的缓存技巧。

The postGet faster cache validation with MicroDiffappeared first onLogRocket Blog.