Desenvolvimento

9 mar, 2018

Criando estatísticas de visitas de página usando Redis

Publicidade

Há muitos casos de uso em que o Redis pode ser uma boa escolha (cache, filas, mensagens, contagem, armazenamento simples, etc). Em geral, para obter dados quentes, o Redis é conhecido por ser rápido, simples e útil.

Isso é porque ele é um armazenamento de estrutura de dados em memória, usado como banco de dados, que suporta armazenamento de strings, listas, hashes, e assim por diante. Você pode ver mais sobre o Redis na página oficial e, neste artigo, veremos alguns motivos pelos quais o Redis se tornou tão popular.

Criar um contador de acesso em tempo real para gerar estatísticas para um site, pode parecer uma tarefa complexa envolvendo sistemas diferentes ou algo assim. Ao usar o Redis, podemos fazê-lo rapidamente, sem qualquer impacto negativo de desempenho e usando um pequeno espaço de memória.

Para este exemplo, vamos armazenar estatísticas sobre páginas visitadas diariamente. Dessa forma, poderemos saber quantas vezes uma página foi visitada para cada dia. Considere um site “Redis expert” contendo as páginas:

/home

/contact

/about-me

/posts

/storing-data-on-redis

A primeira coisa que precisamos fazer é definir uma estrutura chave para nossos registros. Nossos dados serão separados por dia, então precisamos usar uma data em nossa chave e também a página que recebeu a visita.

Então, podemos ter uma estrutura para a chave, como:

page:[page url]:[date]

Em um cenário real:

page:home:04-02-2018

Ao ver a estrutura-chave, podemos ter uma ideia do que precisamos fazer para criar nossas estatísticas: incrementaremos o valor da chave para cada acesso nessa página, e isso é algo fácil de executar.

Vamos verificar algumas abordagens diferentes para fazê-lo.

Incrementando o valor da chave usando INCR

O comando Redis INCR nos permite incrementar o valor de uma determinada chave sem saber o valor existente. O Redis não suporta o tipo de dados Integer, então ele interpretará nossos dados, neste caso, como uma String.

Agora que sabemos qual comando usar e ter uma estrutura de chave já definida, vejamos uma implementação simples do processo de incrementação usando o cliente Jedis:

Você pode usar qualquer cliente Redis da sua escolha. Existem vários clientes para diferentes linguagens. Neste artigo, estou usando o Jedis (cliente para Java).

import redis.clients.jedis.Jedis;

public class GenerateStatisticsForVisitedPages {
	
	public void generateStatistics(String page, String date) {
		String key = String.format("page:%s:%s", page, date);
		Jedis jedis = new Jedis("localhost");
		long result = jedis.incr(key);
		
		System.out.println( 
				String.format("page %s had %d accesses in %s", 
						page, result, date)
		);
	}
		
	public static void main(String[] args) {
		String date = "02/09/2018";
		
		String[] pagesVisited = {
				"/home",
				"/contact",
				"/about-me",
				"/all-posts",
				"/storing-data-on-redis"
		};
		
		GenerateStatisticsForVisitedPages generator = new GenerateStatisticsForVisitedPages();
		generator.generateStatistics(pagesVisited[0], date);
		generator.generateStatistics(pagesVisited[1], date);
		generator.generateStatistics(pagesVisited[2], date);
		generator.generateStatistics(pagesVisited[1], date);
		generator.generateStatistics(pagesVisited[1], date);
				
	}
}

Isso produzirá a seguinte saída:

page /home had 1 accesses in 02/09/2018

page /contact had 1 accesses in 02/09/2018

page /about-me had 1 accesses in 02/09/2018

page /contact had 2 accesses in 02/09/2018

page /contact had 3 accesses in 02/09/2018

Na linha 8, onde fazemos long resultado = jedis.incr(key); o valor da chave está sendo incrementado. Podemos obter o mesmo resultado executando em um redis-cli (linha de comando) a instrução INCR, como:

127.0.0.1:6379> INCR page:/contact:02/09/2018

Talvez você esteja se perguntando: não posso fazê-lo executando dois comandos (GET and SET)? Sim, você pode. Porém, fazendo assim você não executaria uma operação atômica. Seu código pode ter problemas de condição de corrida.

Essa é a maneira mais simples para armazenar e recuperar dados de visitas de páginas.

E se quisermos armazenar nossos dados organizando-os por usuário e pela data da visita de uma página? A principal diferença é que agora a quantidade de dados armazenados no Redis será muito maior do que a quantidade armazenada no exemplo anterior, no qual estávamos contando visitas para cada página existente.

Usando bitmaps

Devido à enorme quantidade de dados que vamos armazenar agora, usaremos bitmaps. Bitmaps são arrays de bits (arrays contendo apenas 0 e 1), comumente usados porque, quando armazenamos dados em um formato binário, a quantidade de memória necessária é muito pequena e, também, as operações de cálculo envolvendo bitmaps são extremamente rápidas.

O Redis fornece alguns comandos para manipular bitmaps. Vejamos alguns deles antes de implementar nossa solução definitiva usando o Jedis:

127.0.0.1:6379> SETBIT access:/home:04/02/2018 123 1 (integer) 0

O comando SETBIT é bastante semelhante ao comando SET. Ambos estabelecem um valor para uma determinada chave. Aqui a nossa chave é access:/home:04/02/2018, e estamos definindo um valor 123 para essa chave, onde 123 representa o código exclusivo do usuário que visitou a página inicial em 04/02/2018.

O parâmetro final (número 1) é o que chamamos de bit. Ele valida que o usuário de fato clicou em nossa página inicial, ou seja: quando definimos o valor do bit para 1, isso significa que o usuário clicou e, se por algum motivo nós definirmos o valor do bit para 0, estamos invalidando o clique do usuário.

Como podemos criar bitmaps, é claro, também podemos obter seu valor de bit usando o comando GETBIT:

127.0.0.1:6379> GETBIT access:/home:04/02/2018 (integer) 1

Gerando dados de teste

Agora que vimos as principais operações envolvendo bitmaps, vamos criar nossa implementação para obter estatísticas. Criaremos uma classe Java para gerar os dados (simulando 1.000 acessos de 500 usuários diferentes em um período de 30 dias):

import java.util.Random;

import redis.clients.jedis.Jedis;

public class StoringAccessDataUsingBitmap {
	
	public void store(long userCode, String date) {
		Jedis jedis = new Jedis("localhost");
		String key = String.format("access:%s", date);
		
		jedis.setbit(key, userCode, true);
	}
	
	public static void main(String[] args) {
		int amountOfUsers = 500;
		int amountOfAccess = 1000;
		int amountOfDays = 30;
		
		Random random = new Random();
		StoringAccessDataUsingBitmap acesso = new StoringAccessDataUsingBitmap();
		
		for(Integer numero = 1; numero <= amountOfAccess; numero++) {
			long user = (random.nextInt(amountOfUsers) + 1);
			String date = String.format("%02d/11/2018", (random.nextInt(amountOfDays) + 1));
			acesso.store(user, date);
		}
	}

}

Agora devemos ter uma boa quantidade de dados armazenados no Redis para recuperar algumas informações interessantes.

Obtendo estatísticas de acesso à página

Vamos criar os métodos para extrair as informações de acesso à página por dia e por semana:

import java.util.Arrays;

import redis.clients.jedis.Jedis;

public class ExtractingAccessDataUsingBitmap {
	
	public long getAccessesByPeriod(String...dates) {
		Jedis jedis = new Jedis("localhost");
		long total = 0;
		
		for (String date : dates) {
			String key = String.format("access:%s", date);
			total += jedis.bitcount(key);
		}
		
		return total;
	}
		
	public static void main(String[] args) {
		ExtractingAccessDataUsingBitmap accessData = new ExtractingAccessDataUsingBitmap();
		
		String[] daily = { "05/11/2018" };
		String[] weekly = {
			"16/11/2018", "17/11/2018", "18/11/2018", "19/11/2018",
			"20/11/2018", "21/11/2018", "22/11/2018"
		};
		
		long totalDaily = accessData.getAccessesByPeriod(daily);
		long totalWeekly = accessData.getAccessesByPeriod(weekly);
		
		System.out.println(String.format(
				"Total unique users on %s was: %d", 
				Arrays.asList(daily), totalDaily
		));
		
		System.out.println(String.format(
				"Total unique users on %s was: %d",
		Arrays.asList(weekly),totalWeekly
		));
	}
}

O único novo comando que usamos aqui é BITCOUNT. Ele retorna o número de bits definidos em um valor de chave, ou seja, o BITCOUNT faz uma soma dos valores com o valor de bit igual a 1 e o retorna.

A saída deve ser algo como:

Total unique users on [05/11/2018] was: 27

Total unique users on [16/11/2018, 17/11/2018, 18/11/2018, 19/11/2018, 20/11/2018, 21/11/2018, 22/11/2018] was: 246

Lembre-se: geramos aleatoriamente os dados, de modo que o conteúdo da saída possa variar.

Como vimos, ao conhecer as diferentes estruturas de dados suportadas pelo Redis, podemos implementar facilmente soluções para armazenar e recuperar rapidamente muitos tipos de dados. Para contar, temos alternativas que se encaixam melhor quando estamos lidando com uma grande quantidade de dados (bitmaps) e estruturas de dados com operações mais simples, como Strings com comandos INCR e INCRBY.

Para mais exemplos como este, veja meu repositório no GitHub discovering-redis.

Referências e links úteis

***

Artigo traduzido com autorização do autor. Publicado originalmente em: http://andreybleme.com/2018-02-04/creating-page-visit-statistics-using-redis/