使用Redis如何保证数据的一致性

业务系统通常使用数据库(如MySQL)来存储持久化数据,并使用缓存(如Redis)来提升系统的性能。同时使用数据库和缓存,就会出现一个老生常谈的问题,就是缓存与数据库一致性的问题。关于这个问题,下面提供了一系列的解决办法:

双写策略

先写数据库,再写缓存

在遇到并发的时候会出现问题:A 请求先将数据库的数据更新为 1,然后在更新缓存前,请求 B 将数据库的数据更新为 2,紧接着也把缓存更新为 2,然后 A 请求更新缓存为 1。

先写缓存,再写数据库

也会因为并发问题,A 请求先将缓存的数据更新为 1,然后在更新数据库前,请求 B 将缓存的数据更新为 2,紧接着也把数据库更新为 2,然后 A 请求更新数据库为 1。

解决双写策略的问题,我们可以:

  • 在缓存执行更新之前去加一个分布式锁,保证在同一时间只去运行同一个请求的更新(单飞)。但这也会带来性能问题。

  • 在更新完缓存的时候,给缓存加上一个较短的过期时间,这样哪怕出现数据不一致的问题,也仅仅只是一会,不会影响业务。

读时更新,写时删除

先删缓存,再写数据库

如果有两个请求中一个请求执行数据的更新,那么他就会先去删除缓存;在这期间另一个请求去读取数据,然后将数据库中未修改的数据更新到缓存中;然后第一个请求去修改数据库中的数据;这就会导致最终数据库中的数据和缓存中的数据不一致。

解决这个问题,我们可以采用延时双删的策略,下面是延时双删的伪代码:

 //删除缓存
 redis.DEL(context.Background(),"x")
 //更新数据库
 db.update(x)
 //睡眠
 time.sleep(N)
 //在删除缓存
 redis.DEL(context.Background(),"x")

中间睡几秒是为了,请求B能够完成从数据库读取数据,并存到缓存中。

先更新数据库,再删除缓存

假如某个用户数据在缓存中不存在,请求 A 读取数据时从数据库中查询到年龄为 20,在未写入缓存中时另一个请求 B 更新数据。它更新数据库中的年龄为 21,并且清空缓存。这时请求A 把从数据库中读到的年龄为 20 的数据写入到缓存中。

但这只是理论上的,在实际中缓存写入是远比数据库写入快的。所以很难出现请求A的缓存还没写入请求B就已经更新数据的情况,并且如果A的数据缓存更新在B删除缓存之前,也是后面回去数据库直接查找数据。所以他是可以保证数据一致性的

由于这是两步操作,所以可能会出现第二个操作失败的情况,这时候我们就可以采用消息队列重试机制。当操作失败就重式直到完成,或达到上限,返回错误。

永不过期,由后端自主更新

设置一个定时器,定时去数据库中拉去新的数据。

采用监听binlog,当数据库有更新操作的时候,再去更新数据

单飞

在高并发,大量请求的环境下,限制某个操作或请求仅有一个客户端能够执行,其它客户端则需要等待或直接返回。这样的机制可以通过分布式锁来实现。

 package main
 ​
 import (
     "context"
     "fmt"
     "github.com/go-redis/redis/v8"
     "time"
 )
 ​
 var (
     Rdb     *redis.Client
     ctx     = context.Background()
     lockKey = "my-lock-key"
 )
 ​
 func getRdb() {
     Rdb = redis.NewClient(&redis.Options{
         Addr:     "8.130.17.124:6379",
         Password: "021001",
         DB:       2,
     })
     fmt.Println("Redis连接成功")
 }
 ​
 func Close() {
     err := Rdb.Close()
     if err != nil {
         return
     }
     fmt.Println("Redis关闭成功")
 }
 ​
 // 尝试去获取锁
 func acquireLock() (bool, error) {
     result, err := Rdb.SetNX(ctx, lockKey, "1", 5*time.Second).Result()
     if err != nil {
         return false, err
     }
     return result, nil
 }
 ​
 // 释放锁
 func releaseLock() error {
     _, err := Rdb.Del(ctx, lockKey).Result()
     if err != nil {
         return err
     }
     return nil
 }
 ​
 func main() {
     getRdb()
     defer Close()
 ​
     locked, err := acquireLock()
     if err != nil {
         fmt.Println(err)
         return
     }
     if locked {
         fmt.Println("获取锁成功")
         // 业务逻辑
         time.Sleep(6 * time.Second)
 ​
         //释放锁
         err = releaseLock()
         if err != nil {
             fmt.Println(err)
             return
         }
         fmt.Println("释放锁成功")
     } else {
         fmt.Println("获取锁失败,另一个进程持有锁")
     }
 }
 ​
 //func main() {
 //  getRdb()
 //  defer Close()
 //
 //  lockKey = "my-lock-key"
 //
 //  // 模拟另一个进程持有锁
 //  fmt.Println("模拟另一个进程持有锁...")
 //  locked, err := acquireLock()
 //  if err != nil {
 //      fmt.Println("锁初始化失败:", err)
 //      return
 //  }
 //
 //  if locked {
 //      fmt.Println("锁已设置 (模拟另一个进程持有锁)")
 //
 //      // 等待一段时间后尝试获取锁
 //      time.Sleep(2 * time.Second)
 //
 //      // 当前进程尝试获取锁
 //      fmt.Println("当前进程尝试获取锁...")
 //      lockedAgain, err := acquireLock()
 //      if err != nil {
 //          fmt.Println("获取锁失败:", err)
 //          return
 //      }
 //
 //      if lockedAgain {
 //          fmt.Println("成功获取锁,处理任务中...")
 //      } else {
 //          fmt.Println("获取锁失败,另一进程仍持有锁。")
 //      }
 //
 //      // 释放锁
 //      fmt.Println("释放锁...")
 //      err = releaseLock()
 //      if err != nil {
 //          fmt.Println("释放锁失败:", err)
 //      } else {
 //          fmt.Println("锁已释放")
 //      }
 //
 //  } else {
 //      fmt.Println("无法设置模拟锁,可能锁已被占用。")
 //  }
 //}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值