Aplicações que exigem atualizações frqeuentes no banco de dados, como sistemas de progresso em tempo real ou coleta de respostas em quizzes, podem enfrentar gargalos de performence devido à latência de I/O. Uma estratégia eficiente para mitigar esse problema consiste em implementar uma camada de cache temporário em memória e sincronizar os dados de forma assíncrona e periódica.
Estrutura do Buffer de Cache
Para gerenciar o estado temporário, utilizamos um ConcurrentHashMap. Esta estrutura permite o acesso concorrente seguro, armazenando o identificador do usuário e um objeto contendo os dados e o carimbo de data/hora da última atividade.
@Service
@Slf4j
public class UserStateService {
private final Map<Long, UserProgressBuffer> progressBuffer = new ConcurrentHashMap<>();
private final UserMapper userMapper;
public Map<String, Object> fetchProgress(Long userId) {
// Verifica se os dados já estão no buffer de memória
if (progressBuffer.containsKey(userId)) {
log.debug("Recuperando progresso do cache para o usuário: {}", userId);
UserProgressBuffer cacheEntry = progressBuffer.get(userId);
cacheEntry.setLastUpdate(System.currentTimeMillis());
return cacheEntry.getData();
}
// Caso não esteja no cache, busca na base de dados
log.info("Carregando progresso do banco de dados para o usuário: {}", userId);
UserEntity entity = userMapper.selectById(userId);
if (entity == null) return null;
UserProgressBuffer newEntry = new UserProgressBuffer(
entity.getJsonProgress(),
System.currentTimeMillis()
);
progressBuffer.put(userId, newEntry);
return newEntry.getData();
}
}
Habilitando o Agendamento no Spring Boot
Para que o motor do Spring processe as tarefas em segundo plano, é necessário adicionar a anotação @EnableScheduling na classe de configuração principal da aplicação.
@SpringBootApplication
@EnableScheduling
public class QuizApplication {
public static void main(String[] args) {
SpringApplication.run(QuizApplication.class, args);
}
}
Sincronização Periódica e Limpeza de Cache
A tarefa agendada percorre o mapa de cache, persiste as informações pendentes no banco de dados e remove entradas inativas para evitar vazamento de memória (memory leak). O uso do método fixedDelay garante que um novo ciclo de sincronização só comece após a conclusão do anterior.
@Component
@RequiredArgsConstructor
public class DatabaseSyncTask {
private final UserStateService stateService;
private final UserMapper userMapper;
// Intervalo de 60 segundos entre as execuções
@Scheduled(fixedDelay = 60000)
public void synchronizeProgress() {
long currentTime = System.currentTimeMillis();
long expirationThreshold = 1000 * 60 * 15; // 15 minutos de inatividade
stateService.getBuffer().forEach((userId, buffer) -> {
// Persistência dos dados atuais no banco
userMapper.updateProgress(userId, JSON.toJSONString(buffer.getData()));
// Se o cache estiver inativo por muito tempo, remove da memória
if (currentTime - buffer.getLastUpdate() > expirationThreshold) {
stateService.getBuffer().remove(userId);
}
});
}
}
Vnatagens desta Abordagem
Ao adotar este padrão de "Write-Behind", a aplicação ganha escalabilidade por reduzir drasticamente o número de instruções UPDATE enviadas ao banco de dados. Em vez de uma gravação a cada clique do usuário, os dados são consolidados em memória e enviados em lotes ou intervalos controlados, otimizando o pool de conexões e o processamento do servidor de banco de dados.