处理双写策略中的数据同步
1 简介
如果使用缓存提升性能,可能有缓存和数据库的数据同步协调问题。这里简单聊聊。
其中一个直接的策略就是双写策略,写完数据库写缓存。
2 缓存
Redis是一个开源的数据库缓存系统,是一个高性能的Key-Value存储系统。
Redis 提供五种数据类型: String、Hash、List、Set 及ZSset(Sorted Set).
(1)String是最简单的类型,--个key对应--个 value。
(2)List是一个链表结构,主要功能是push、 pop、获取一个范围的所有值等等。使用List结构,可以轻松地实现最新消息排队功能。(3)Hash是一个String类型的field(字段)和value (属性)的映射表,Hash 特别适合用于存储对象。一个Hash可以存多个key-value,类似一个对象的多 个字段和属性。
(4)Set是String类型的无序集合。集合成员是不可重复的。
(5)ZSet是有序集合,每个元素都会关联一个double 类型的权重参数(score),使得集合中的元素能够按score进行有序排列。
比如电商实现当前热销商品排名的功能,则应该选择使用ZSet结构。
若系统采用Redis 作为数据库缓存,将数据持久化存储在MYSQL数据库中,则必然需要解决二者的数据实时同步问题。解决Redis和MySQL数据实时同步问题的常见方案是:
1 应用程序读数据时先读取Redis中的key,如读到而且未失效则返回kev对应的数据。
2 如读不到或key失效,则读取数据库,并同步Redis;
3 写数据时先写数据库,并设置内存对应的key失效。
3 缓存使用注意事项
- 避免bigkey
String类型控制在10KB以内,hash、list、set、zset元素个数不要超过5000, 这是为了拒绝bigkey(防止网卡流量、慢查询)。
- 使用时要选择适合的数据类型。
不少人只用Redis的String类型,上来就是set和get。
实际上,Redis提供了「丰富的数据结构类型」,有些业务场景,更适合hash、zset等其他数据结果。
- 慎用O(n)复杂度命令,
如hgetall、smember,lrange等,因为Redis是单线程执行命令的。
hgetall、smember等命令时间复杂度为O(n),当n持续增加时,会导致 Redis CPU 持续飙高,阻塞其他命令的执行。
- 慎用Redis的monitor命令
Redis Monitor 命令用于实时打印出Redis服务器接收到的命令,如果我们想知道客户端对redis服务端做了哪些命令操作,就可以用Monitor 命令查看,但是它一般「调试」用而已,尽量不要在生产上用!
因为monitor命令可能导致redis的内存持续飙升。
- 生产环境不能使用 keys指令
Redis Keys 命令用于查找所有符合给定模式pattern的key。
如果想查看Redis 某类型的key有多少个,不少小伙伴想到用keys命令。
禁止使用flushall、flushdb,
Flushall 命令用于清空整个 Redis 服务器的数据(删除所有数据库的所有 key )。
Flushdb 命令用于清空当前数据库中的所有 key。
这两命令是原子性的,不会终止执行。一旦开始执行,不会执行失败。
-
避免使用SORT、SINTER等复杂度过高的命令。
-
小心使用del命令
删除key你一般使用什么命令?是直接del?如果删除一个key,直接使用del命令当然没问题。
但是,你想过del的时间复杂度是多少嘛?我们分情况探讨一下:如果删除一个String类型的key,时间复杂度就是O(1),「可以直接del」。
如果删除一个List/Hash/Set/ZSet类型时,它的复杂度是O(n), n表示元素个数。
因此,如果你删除一个List/Hash/Set/ZSet类型的key时,元素越多,就越慢。
「当n很大时,要尤其注意」,会阻塞主线程的。
那么,如果不用del,应该怎么删除呢?❝
如果是List类型,你可以执行lpop或者rpop,直到所有元素删除完成。
如果是Hash/Set/ZSet类型,你可以先执行hscan/sscan/scan查询,再执行hdel/srem/zrem依次删除每个元素。
❞
4 缓存同步实例
在Go中实现Redis和MySQL之间的数据实时同步,常见的方案包括:
- 双写策略
这种方式要求在更新数据库(MySQL)的同时,也要更新Redis缓存。
这种策略较为简单,但可能面临一致性问题,特别是在MySQL写成功而Redis写失败的情况下。
实现步骤:
数据库操作:更新MySQL中的数据。
缓存操作:更新Redis中的缓存数据。
事务处理:确保MySQL和Redis操作的事务一致性。
示例代码:
package main
import (
"fmt"
"log"
"github.com/go-redis/redis/v8"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"context"
)
var ctx = context.Background()
func updateDataInMySQLAndRedis(db *gorm.DB, rdb *redis.Client, key string, value string) error {
// MySQL事务
tx := db.Begin()
if tx.Error != nil {
return tx.Error
}
// 更新MySQL
err := tx.Exec("UPDATE your_table SET your_column = ? WHERE your_key = ?", value, key).Error
if err != nil {
tx.Rollback()
return err
}
// 更新Redis
err = rdb.Set(ctx, key, value, 0).Err()
if err != nil {
tx.Rollback()
return err
}
// 提交MySQL事务
if err := tx.Commit().Error; err != nil {
return err
}
return nil
}
func main() {
// 连接MySQL
dsn := "user:password@tcp(127.0.0.1:3306)/dbname"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
log.Fatal("Failed to connect to MySQL:", err)
}
// 连接Redis
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
})
// 更新数据
key := "user:123"
value := "John Doe"
err = updateDataInMySQLAndRedis(db, rdb, key, value)
if err != nil {
log.Fatal("Failed to update data:", err)
}
fmt.Println("Data updated successfully in both MySQL and Redis")
}
- 延时双删策略
当使用双写策略时,可能会有短时间内的缓存不一致问题。延时双删策略解决了这个问题:
删除缓存:更新MySQL数据前,先删除Redis缓存中的数据。
更新MySQL:更新MySQL数据库中的数据。
延时删除缓存:在一定时间后(如500ms),再次删除Redis缓存中的数据,确保缓存过期。
- 点赞
- 收藏
- 关注作者
评论(0)