Go中实现redis与mysql双写一致(项目demo示例:Gin+Redis+Mysql+Gorm+Canal+Kafka)

在这里插入图片描述

前言

本文详细讲解了一个完整的Go示例项目,演示如何通过Gin + Redis + Mysql + Gorm + Canal + Kafka 构建一个支持用户信息修改的双写一致性系统。
该示例将包括:

  • 使用延迟双删策略保证 Redis 和 MySQL 一致性

  • 使用 Kafka/Canal 实现 Binlog 驱动的最终一致性同步机制

  • Docker 支持部署 Redis、MySQL、Kafka、Zookeeper、Canal 及应用服务

  • 清晰的项目结构和详细的代码注释

一、项目概述

本示例项目基于Gin框架构建了一个用户服务,演示如何在用户信息更新时保持 MySQL 与 Redis 缓存的一致性。

1.1 核心思想:

  • 写操作先采用“延迟双删”策略(先删缓存 -> 写数据库 -> 延迟再删缓存),同时通过Canal+Kafka监听 MySQL Binlog,将变更消息发送到 Kafka 并由消费者更新或清除 Redis 缓存,以最终保证数据一致性。
  • 参考:cloud.tencent.comdeveloper.aliyun.com

1.2 整体流程:

  • 客户端发起更新请求后,服务先删除对应的 Redis 缓存,再更新 MySQL 数据库;与此同时,Canal 作为 MySQL 的伪从库捕获Binlog 变更,将事件推送到 Kafka 指定主题,后端的 Kafka 消费者接收事件后对 Redis 进行相应更新或删除操作,从而实现 最终一致性

  • 由于延迟双删只能保证最终一致性,并且存在短暂不一致,因此加入 Canal+Kafka 机制可进一步增强数据一致性保证。

  • 参考:cloud.tencent.comdeveloper.aliyun.com

1.3 图示:

在这里插入图片描述

二、技术栈与架构

  • Web 框架:Gin(处理 HTTP 请求)
  • ORM:GORM(操作 MySQL 数据库)
  • Redis 客户端:go-redis v9(操作缓存)
  • 消息队列:Kafka(接收 Canal 发送的 Binlog 变更消息)
  • Canal:监听 MySQL Binlog,将数据变更推送到 Kafka
  • Docker Compose:部署 MySQL、Redis、Kafka、ZooKeeper、Canal-server 和 Go 应用

基于以上组件,系统流程如下:

1、用户更新请求:客户端调用 /user/update 接口发送用户 ID 和新用户名。
---------
2、延迟双删策略:在写入数据库前,先调用Redis 客户端删除 user:{id} 缓存,再写入MySQL;写库完成后延迟一定时间(例如500ms),再次删除缓存,降低并发情况下读到旧数据的概率。
参考:developer.aliyun.com
---------
3、Canal 同步:Canal 作为 MySQL 的订阅节点,自动监听 Binlog 变更。每当用户表发生更新时,Canal将变更数据发送到 Kafka 指定 Topic。
---------
4、Kafka 消费者:Go 服务中启动一个后台消费者监听该 Topic,接收到UPDATE 消息后,解析用户 ID,并对 Redis 做补充操作(可删除缓存或重写最新数据),确保数据最终一致。

三、项目结构

user-service/                 
├── docker-compose.yml       # Docker Compose 配置:MySQL, Redis, ZooKeeper, Kafka, Canal, 应用  
├── go.mod                   # Go 模块文件  
├── main.go                  # 程序入口:初始化 DB、Redis、Kafka 消费者,配置 Gin 路由  
├── models/                  
│   └── user.go              # GORM User 模型  
├── service/                 
│   └── user_service.go      # 用户服务逻辑:更新用户、延迟双删策略  
├── cache/                   
│   └── redis_client.go      # Redis 客户端封装  
└── consumer/                
    └── canal_consumer.go    # Kafka 消费者:监听 Canal 发送的变更消息并更新缓存  
  • docker-compose.yml:定义了 MySQL(开启 Binlog)、Redis、ZooKeeper、Kafka、Canal-server 和 Go 应用容器,以及它们的网络关系。
  • main.go:程序入口,使用 GORM 连接 MySQL,go-redis 连接 Redis,启动 Canal/Kafka 消费者,并使用 Gin 注册 /user/update 接口。
  • models/user.go:定义 User 结构体和 GORM 映射表,用于描述用户数据。
  • service/user_service.go:实现用户更新逻辑,包含延迟双删策略:删除缓存、写库、睡眠、再删缓存。
  • cache/redis_client.go:封装 Redis 连接和常用操作函数,如 DeleteSetGet 等。
  • consumer/canal_consumer.go:启动 Kafka 消费者,监听 Canal 发送的 user 表更新消息并解析,根据操作类型(增删改)来更新或删除 Redis 缓存,实现最终一致性。

四、关键代码片段

4.1 数据模型(models/user.go)

package models

import "gorm.io/gorm"

// User 定义用户模型(演示用,简化字段)
type User struct {
    gorm.Model
    Username string `gorm:"type:varchar(255)"`
}
  • 该结构体映射到 MySQL 中的 users 表。GORM 的 AutoMigrate 可根据该模型自动创建表结构。

4.2 初始化数据库和缓存(main.go)

// 初始化 MySQL 连接
db, err := gorm.Open(mysql.Open("root:password@tcp(mysql:3306)/userdb?parseTime=true"), &gorm.Config{})
if err != nil { log.Fatal(err) }
db.AutoMigrate(&models.User{})

// 初始化 Redis 连接
redisClient = redis.NewClient(&redis.Options{
    Addr: "redis:6379", Password: "", DB: 0,
})
if err := redisClient.Ping(ctx).Err(); err != nil { log.Fatal(err) }
  • main 函数中,先通过 GORM 连接 MySQL(数据库名 userdb),并执行 AutoMigrate 保证有 users 表;
  • 然后用 go-redis 连接到 Redis 服务器,并验证连通性。

4.3 用户更新接口(main.go)

router.PUT("/user/update", func(c *gin.Context) {
    // 解析请求参数
    var req struct { UserID uint; NewName string }
    if err := c.ShouldBindJSON(&req); err != nil {
        c.JSON(400, gin.H{"error": err.Error()}); return
    }
    // 延迟双删:先删除缓存
    cacheKey := fmt.Sprintf("user:%d", req.UserID)
    redisClient.Del(ctx, cacheKey)
    // 更新数据库
    if err := db.Model(&models.User{}).Where("id = ?", req.UserID).
         Update("username", req.NewName).Error; err != nil {
        c.JSON(500, gin.H{"error": err.Error()}); return
    }
    // 延时再删缓存
    go func() {
        time.Sleep(500 * time.Millisecond)
        redisClient.Del(ctx, cacheKey)
    }()
    c.JSON(200, gin.H{"status": "updated"})
})
  • 此接口接受 JSON 请求体,例如 {"user_id":1,"new_name":"alice"}。逻辑为:
    • 先删除缓存redisClient.Del),再执行 MySQL 更新,之后启动一个协程延迟 500ms后再次删除缓存。
  • 上述流程即实现了延迟双删策略。参考:developer.aliyun.com
  • 在高并发场景下可有效减少读到过期缓存的可能性。

4.4 Canal+Kafka 消费者(consumer/canal_consumer.go)

package consumer

import (
    "context"; "encoding/json"; "fmt"
    "github.com/segmentio/kafka-go"
)

func StartCanalConsumer(ctx context.Context, redisClient *redis.Client) {
    reader := kafka.NewReader(kafka.ReaderConfig{
        Brokers: []string{"kafka:9092"}, Topic: "canal_topic", GroupID: "user-service-group",
    })
    for {
        msg, err := reader.ReadMessage(ctx)
        if err != nil {
            log.Println("Kafka read error:", err); continue
        }
        // 假设 Canal 消息为 JSON 格式:{ "table": "users", "type": "UPDATE", "data": {"id":1,"username":"alice"}, ... }
        var event struct {
            Table string                 `json:"table"`
            Type  string                 `json:"type"`
            Data  map[string]interface{} `json:"data"`
        }
        if err := json.Unmarshal(msg.Value, &event); err != nil {
            log.Println("JSON parse error:", err); continue
        }
        // 仅处理用户表的变更事件
        if event.Table == "users" && event.Type == "UPDATE" {
            userID := fmt.Sprintf("%v", event.Data["id"])
            // 再次删除或更新缓存
            redisClient.Del(ctx, "user:"+userID)
        }
    }
}
  • 在后台启动的 Kafka 消费者不断读取 Canal 推送到 canal_topic 主题的消息。解析后针对 users表的更新事件执行缓存操作。
  • 这里示例仅对 Redis user:{id} 做删除,也可根据需要改为重写最新数据。通过此方式,即使延迟双删未能完全覆盖的短暂不一致,也能被消费者最终处理,保证最终一致性
  • 参考:cloud.tencent.com

4.5 Docker Compose 配置(docker-compose.yml)

version: '3'
services:
  mysql:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: userdb
    ports: ["3306:3306"]
    command: ["--default-authentication-plugin=mysql_native_password","--log-bin=mysql-bin","--binlog-format=ROW"]
  redis:
    image: redis:6
    ports: ["6379:6379"]
  zookeeper:
    image: bitnami/zookeeper:latest
    environment: { ALLOW_ANONYMOUS_LOGIN: 'yes' }
  kafka:
    image: bitnami/kafka:latest
    environment:
      KAFKA_CFG_ZOOKEEPER_CONNECT: zookeeper:2181
      KAFKA_CFG_LISTENERS: PLAINTEXT://:9092
    ports: ["9092:9092"]
    depends_on: [zookeeper]
  canal:
    image: canal/canal-server:latest
    environment:
      canal.destinations: example
      canal.mq.servers: kafka:9092
      canal.instance.master.address: mysql:3306
      canal.instance.dbUsername: canal
      canal.instance.dbPassword: canalPwd
      canal.mq.topic: canal_topic
    ports: ["11111:11111"]  # Canal admin 端口
    depends_on: [mysql,kafka]
  user-service:
    build: .
    image: user-service:latest
    environment: { DB_HOST: mysql, REDIS_HOST: redis, KAFKA_HOST: kafka }
    depends_on: [mysql, redis, kafka, canal]
    ports: ["8080:8080"]
  • mysql:启用 binlog (--log-bin) 并设置 ROW 模式,让 Canal 能读取行级变更。
  • canal:使用官方 canal-server 镜像(需自行配置 Canal 用户和 topic),将订阅的 Binlog 事件发送到 Kafka 的 canal_topic
  • user-service:Go 应用容器,部署编译后的服务镜像,链接到其他服务。

五、构建与运行

5.1 准备:安装 Docker 和 Docker Compose。

5.2 代码编译:在 user-service 目录下执行 go mod tidy && go build -o user-service main.go

5.3 启动服务:运行 docker-compose up -d,它会启动 MySQL、Redis、ZooKeeper、Kafka、Canal和 Go 应用。确保 MySQL Binlog 和 Canal 用户配置正确。

5.4 测试接口:使用 curlPostman 调用:

curl -X PUT http://localhost:8080/user/update \
  -H "Content-Type: application/json" \
  -d '{"user_id":1,"new_name":"alice"}'
  • 该请求会触发延迟双删和数据库更新。
  • 可检查 MySQL 中 users 表和 Redis 缓存:查询 MySQL 得到新用户名,查询 Redis 的 user:1 应该为空(缓存已被删除)。
  • 稍后 Canal 消费器会再次清理或更新缓存(根据实现)

六、总结

  • 通过上述示例,展示了使用 延迟双删策略和 Canal+Kafka 机制实现 MySQL 与 Redis 缓存最终一致性的完整流程。
  • 项目代码清晰、注释详细,可以作为缓存一致性方案的学习参考。
  • 参考资料:
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

卜锦元

白嫖是人类的终极快乐,我也是

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值