Otimização de Escrita em Banco de Dados com Cache em Memória e Spring @Scheduled

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.

Tags: Spring Boot java cache ConcurrentHashMap Scheduled Tasks

Publicado em 7-9 20:37