Ensuring Cache‑Database Consistency: Delayed Double Delete vs Binlog‑Driven Async Updates

This article compares the delayed double‑delete method with an asynchronous binlog‑subscription approach using Canal and RabbitMQ, explaining their workflows, pitfalls, and code implementations for maintaining cache and database consistency under high concurrency.

MaGe Linux Operations
MaGe Linux Operations
MaGe Linux Operations
Ensuring Cache‑Database Consistency: Delayed Double Delete vs Binlog‑Driven Async Updates

Introduction

Cache and database consistency is a classic challenge. The article compares two solutions: the delayed double‑delete mechanism and an asynchronous update using Alibaba Canal to pull binlog events and push them through a message queue.

Delayed Double Delete Strategy

Process: client A deletes the cache, writes to the DB; client B reads, finds no cache, queries the DB (may get stale replica data), caches it, then after master‑slave sync client A deletes the cache again. The pause can cause inconsistency.

Key point: if the second cache deletion fails, stale data remains. Retry methods include recording failures in a table for scheduled retries (high DB load) or using an asynchronous MQ to retry.

Demo code:

@RestController
@RequestMapping
public class RedisController {

    @Autowired
    private RedisTemplate redisTemplate;
    @Autowired
    private SysUserMapper sysUserMapper;

    @GetMapping
    public void duobleCancle() throws InterruptedException {
        redisTemplate.delete("1");
        SysUser sysUser = sysUserMapper.selectUserById(Long.valueOf(1));
        SysUser updateSysUser = new SysUser();
        updateSysUser.setUserName("Lxlxxx");
        updateSysUser.setEmail("@163.com");
        UpdateWrapper<SysUser> updateWrapper = new UpdateWrapper();
        updateWrapper.eq("userId",1);
        sysUserMapper.update(updateSysUser,updateWrapper);
        Thread.sleep(3000);
        redisTemplate.opsForValue().append(sysUser.getUserId(), JSON.toJSONString(sysUser));
        redisTemplate.delete("1");
    }
}

The demo shows many pitfalls that can still lead to dirty reads.

Asynchronous Cache Update via Binlog Subscription

Overall flow: install Canal, RabbitMQ, and MySQL; configure Canal to use RabbitMQ; enable binlog on MySQL; consume messages to update or delete Redis entries.

Canal Configuration

canal.ip = 1
canal.serverMode = rabbitmq
canal.mq.servers = 127.0.0.1
canal.mq.vhost=canal
canal.mq.exchange=exchange.trade
canal.mq.username=guest
canal.mq.password=guest
---------------------------------------------------------------------------------
canal.instance.dbUsername=root
canal.instance.dbPassword=123456
canal.instance.mysql.slaveId=1234
canal.instance.master.address=127.0.0.1:3306
canal.instance.defaultDatabaseName=test
canal.mq.topic=example

MySQL my.cnf settings:

log-bin=mysql-bin
binlog-format=ROW
server_id=1

Dependencies (Spring Boot, Redis, AMQP, FastJSON, MyBatis‑Plus):

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.3</version>
</dependency>
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>${mybatis.plus.version}</version>
</dependency>

application.yml snippet:

spring:
  rabbitmq:
    virtual-host: canal
    host: 127.0.0.1
    publisher-confirms: true
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=UTC
    username: root
    password: root
    driver-class-name: com.mysql.jdbc.Driver
  redis:
    host: 127.0.0.1

RabbitMQ configuration class:

@Configuration
public class RabbitMqConfig {

    @Bean
    public Queue TestDirectQueue() {
        return new Queue("exchange.canal.queue", true);
    }

    @Bean
    public DirectExchange TestDirectExchange() {
        return new DirectExchange("exchange.canal");
    }

    @Bean
    public Binding bindingDirect() {
        return BindingBuilder.bind(TestDirectQueue()).to(TestDirectExchange()).with("example");
    }
}

Listener that processes Canal messages and updates Redis:

@Component
@Slf4j
public class RabbitMqListener {

    @Autowired
    private StringRedisTemplate redisTemplate;

    @RabbitListener(queues = "exchange.canal.queue")
    public void process(Message message) {
        log.info("canal queue消费的消息" + message.getBody());
        Map map = JSON.parseObject(message.getBody(), Map.class);
        JSONArray array = null;
        String sqlType = (String) map.get("type");
        if (StringUtils.endsWithIgnoreCase("SELECT", sqlType)) {
            array = JSONArray.parseArray((String) map.get("data"));
        }
        if (array == null) {
            return;
        }
        JSONObject jsonObject = array.getJSONObject(0);
        if (StringUtils.endsWithIgnoreCase("UPDATE", sqlType) ||
            StringUtils.endsWithIgnoreCase("INSERT", sqlType)) {
            redisTemplate.boundValueOps(jsonObject.get("code").toString()).set(jsonObject.toString());
        } else if (StringUtils.endsWithIgnoreCase("DELETE", sqlType)) {
            redisTemplate.delete(jsonObject.get("code").toString());
        }
        if (StringUtils.endsWithIgnoreCase("SELECT", sqlType)) {
            redisTemplate.boundValueOps(jsonObject.get("code").toString()).set(jsonObject.toString());
        } else {
            redisTemplate.delete(jsonObject.get("code").toString());
        }
    }
}

Conclusion

In high‑concurrency scenarios, cache‑database consistency cannot be guaranteed instantly; using asynchronous messaging to achieve eventual consistency provides a reliable way to control cache updates and deletions.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

CacheCanalConsistencyspring-boot
MaGe Linux Operations
Written by

MaGe Linux Operations

Founded in 2009, MaGe Education is a top Chinese high‑end IT training brand. Its graduates earn 12K+ RMB salaries, and the school has trained tens of thousands of students. It offers high‑pay courses in Linux cloud operations, Python full‑stack, automation, data analysis, AI, and Go high‑concurrency architecture. Thanks to quality courses and a solid reputation, it has talent partnerships with numerous internet firms.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.