As vezes precisamos reduzir o tempo de uma requisição, seja lá qual for o motivo. Após algumas horas de pesquisas encontrei várias soluções, mas estava querendo algo bem simples, foi ai que o Redis
entrou na jogada, por ser uma estrutura de dados de chave e valor na memória.
Encontrei alguns exemplos, mas, ainda assim, queria melhorar, graças ao fantástico esquema de plugins do hapi.js
resolvi desenvolver um plugin. Inclusive esse foi meu primeiro plugin, espero que seja o primeiro de muitos.
Não tenho muita criatividade para nomes, então resolvi chamar o plugin de hapi-slap
Para o projeto de exemplo utilizaremos o banco de dados Postgres com ORM(sequelize), foi implementado um CRUD para um model People
para demonstração do cache.
const People = sequelize.define('People', { first_name: { type: DataType.STRING(120) }, last_name: { type: DataType.STRING(120) }, email: { type: DataType.STRING(120) }, gender: { type: DataType.STRING(120) } }, { createdAt: 'created_at', updatedAt: 'update_at', tableName: 'people' });
Caso queira acompanhar o código estará disponível no github slap-caching.
Implementação
Vamos registrar o plugin no hapi.js
server.register([{ register: require('hapi-slap'), options: { url: 'redis://127.0.0.1:6379/0', expireIn: 300 } }], (err) => { if (err) { return console.error(err); } server.start(() => { console.info(`Server started at ${server.info.uri}`); }); });
Podemos informar uma propriedade url
de conexão com Redis
e qual o tempo de expiração global para o cache, o padrão é 300 segundos (5 minutos)
Vamos configurar o plugin para uma endpoint
server.route({ method: 'GET', path: '/people', config: { plugins: { slap: { rule: 'people' } }, handler: handlers.all, validate: validators.all() } });
Vamos implementar o handler
async function all (request, reply) { try { const cache = await request.getCache(); // retornando cache if (cache) { return reply(cache); } const db = request.getDb('slap'); const where = request.query.gender ? {where: {gender: request.query.gender}} : {}; const values = await db.models.People.findAll(where); request.addCache(values); // adicionando cache return reply(values); } catch (err) { return reply.badImplementationCustom(err); } }
Por padrão o plugin disponibiliza um método chamado getCache
, no qual é responsável por retornar o cache, neste caso, não precisaríamos ir ao banco de dados e fazer nossa consulta. Caso contrário, consultaremos em nosso banco de dados e adicionaremos no cache através do método addCache
. Assim se houver uma nova requisição já estará em cache.
Outro detalhe interessante é com relação a query
, neste caso temos uma query referente ao gênero, podemos fazer a seguinte requisição /people?gender=Female
ou /people?gender=Male
ambas serão cacheadas de acordo com seu filtro.
Podemos ter os seguintes caches:
– /people
– /people?gender=Male
– /people?gender=Female
Também podemos configurar de acordo com um determinado parâmetro.
Vamos configurar um endpoint GET /people/{id}
, nele informamos a regra rule
neste caso demos o nome de people-id
, não vamos utilizar o tempo padrão para expiração, vamos informar 60 segundos (1 minuto) na propriedade expireIn
.
server.route({ method: 'GET', path: '/people/{id}', config: { plugins: { slap: { rule: 'people-id', expireIn: 60 } }, handler: handlers.show, validate: validators.show() } });
Vamos implementar o handler
async function show (request, reply) { try { const id = request.params.id; const cache = await request.getCache(id); // retornando cache if (cache) { return reply(cache); } const db = request.getDb('slap'); const value = await db.models.People.findOne({where: {id: id}}); if (!value) { return reply.notFound(); } request.addCache(value, id); // adicionando cache return reply(value); } catch (err) { return reply.badImplementationCustom(err); } }
Neste caso teremos duas diferenças com relação ao primeiro exemplo. No método getCache
adicionamos nosso parâmetro, assim saberemos localizar o cache com relação a regra configurada. Para adicionar ao cache utilizamos o método addCache
e passamos nosso parâmetro.
Agora imagine a seguinte situação, os dados estão em cache, só que o mesmo sofreu alguma alteração ou houve uma inclusão de um registro ou até mesmo foi excluído, como ficaria?
Vamos configurar o slap para os seguintes endpoints POST /people
, PUT /people/{id}
e DELETE /people/{id}
server.route({ method: 'POST', path: '/people', config: { plugins: { slap: { clear: ['people', 'people-id'] } }, handler: handlers.create, validate: validators.create() } }); server.route({ method: 'PUT', path: '/people/{id}', config: { plugins: { slap: { clear: ['people', 'people-id'] } }, handler: handlers.update, validate: validators.update() } }); server.route({ method: 'DELETE', path: '/people/{id}', config: { plugins: { slap: { clear: ['people', 'people-id'] } }, handler: handlers.remove, validate: validators.remove() } });
Existe uma propriedade chamada clear
, nela especificamos quais regras queremos deletar, como nos exemplos acima criamos duas regras, people
e people-id
vamos adicionar na propriedade clear
.
Vamos implementar o handler
async function create (request, reply) { try { const db = request.getDb('slap'); request.clearCache(); // limpando o cache const value = await db.models.People.create(request.payload); return reply({id: value.id}); } catch (err) { return reply.badImplementationCustom(err); } } async function update (request, reply) { try { const db = request.getDb('slap'); const id = request.params.id; const value = await db.models.People.findOne({where: {id: id}}); if (!value) { return reply.notFound(); } request.clearCache(); // limpando o cache await value.update(request.payload, {where: {id: id}}); return reply({id: id}); } catch (err) { return reply.badImplementationCustom(err); } } async function remove (request, reply) { try { const id = request.params.id; const db = request.getDb('slap'); const value = await db.models.People.findOne({where: {id: id}}); if (!value) { return reply.notFound(); } request.clearCache(); // limpando o cache return reply({ok: true}); } catch (err) { return reply.badImplementationCustom(err); } }
Para deletar os caches que estão configurados na propriedade clear
utilizaremos o método clearCache
.
Neste caso, se houver alguma inclusão, alteração ou exclusão limparemos o cache, assim não corremos o risco do cache estar desatualizado.
Fiz alguns testes local, e conseguimos perceber a diferença em utilizar cache, segue abaixo alguns logs.
Foram inseridos 1000 registros na tabela People
no banco de dados.
Log sem utilizar o plugin de cache
$ 171018/110640.157, [response] http://localhost:3000: get /people {} 200 (94ms) $ 171018/110640.939, [response] http://localhost:3000: get /people {} 200 (45ms) $ 171018/110641.515, [response] http://localhost:3000: get /people {} 200 (41ms) $ 171018/110642.107, [response] http://localhost:3000: get /people {} 200 (43ms) $ 171018/110642.588, [response] http://localhost:3000: get /people {} 200 (64ms) $ 171018/110643.017, [response] http://localhost:3000: get /people {} 200 (36ms) $ 171018/110643.476, [response] http://localhost:3000: get /people {} 200 (44ms) $ 171018/110643.898, [response] http://localhost:3000: get /people {} 200 (42ms) $ 171018/110644.309, [response] http://localhost:3000: get /people {} 200 (38ms) $ 171018/110644.711, [response] http://localhost:3000: get /people {} 200 (41ms)
$ 171018/111116.247, [response] http://localhost:3000: get /people/1 {} 200 (50ms) $ 171018/111116.810, [response] http://localhost:3000: get /people/1 {} 200 (13ms) $ 171018/111117.554, [response] http://localhost:3000: get /people/1 {} 200 (12ms) $ 171018/111118.098, [response] http://localhost:3000: get /people/1 {} 200 (12ms) $ 171018/111118.754, [response] http://localhost:3000: get /people/1 {} 200 (13ms) $ 171018/111119.274, [response] http://localhost:3000: get /people/1 {} 200 (12ms) $ 171018/111119.954, [response] http://localhost:3000: get /people/1 {} 200 (12ms) $ 171018/111120.634, [response] http://localhost:3000: get /people/1 {} 200 (11ms) $ 171018/111121.189, [response] http://localhost:3000: get /people/1 {} 200 (11ms) $ 171018/111121.809, [response] http://localhost:3000: get /people/1 {} 200 (11ms)
Observação: O próprio banco faz cache de suas query, podemos perceber a diferença do primeiro retorno para os demais.
Log utilizando cache
$ 171018/111328.677, [response] http://localhost:3000: get /people {} 200 (74ms) $ 171018/111330.794, [response] http://localhost:3000: get /people {} 200 (10ms) $ 171018/111331.621, [response] http://localhost:3000: get /people {} 200 (8ms) $ 171018/111332.273, [response] http://localhost:3000: get /people {} 200 (10ms) $ 171018/111333.001, [response] http://localhost:3000: get /people {} 200 (10ms) $ 171018/111333.721, [response] http://localhost:3000: get /people {} 200 (13ms) $ 171018/111334.465, [response] http://localhost:3000: get /people {} 200 (10ms) $ 171018/111335.241, [response] http://localhost:3000: get /people {} 200 (9ms) $ 171018/111336.034, [response] http://localhost:3000: get /people {} 200 (9ms) $ 171018/111336.873, [response] http://localhost:3000: get /people {} 200 (10ms)
$ 171018/114222.465, [response] http://localhost:3000: get /people/1 {} 200 (41ms) $ 171018/114223.539, [response] http://localhost:3000: get /people/1 {} 200 (3ms) $ 171018/114224.307, [response] http://localhost:3000: get /people/1 {} 200 (3ms) $ 171018/114225.131, [response] http://localhost:3000: get /people/1 {} 200 (3ms) $ 171018/114225.883, [response] http://localhost:3000: get /people/1 {} 200 (3ms) $ 171018/114226.523, [response] http://localhost:3000: get /people/1 {} 200 (2ms) $ 171018/114227.227, [response] http://localhost:3000: get /people/1 {} 200 (3ms) $ 171018/114227.955, [response] http://localhost:3000: get /people/1 {} 200 (2ms) $ 171018/114228.579, [response] http://localhost:3000: get /people/1 {} 200 (2ms) $ 171018/114229.371, [response] http://localhost:3000: get /people/1 {} 200 (1ms)
Retornando 1000 registros:
– Sem cache, média de: 40 ~ 30ms
– Com cache, média de: 13 ~ 8ms
Retornando 1 registro em específico:
– Sem cache, média de: 13 ~ 11ms
– Com cache, média de: 3 ~ 2ms
Com esse simples teste, já conseguimos perceber uma diferença significante.
Espero que tenham gostado, façam alguns testes e tirem suas próprias conclusões!
Github do plugin: hapi-slap
Até a próxima!