前言:目前在完成简易抖音项目时,遇到了Redis和Mysql数据一致性的问题,原先考虑的解决方案是:先更新redis,再使用消息队列异步更新到Mysql中。在查阅资料的过程中,发现大家往往不会选择更新缓存,因为更新缓存这个操作开销大,不如直接淘汰缓存再重新从数据库读取好。因此,下面总结了常用的集中一致性解决方案:cache aside、read\write through、write behind caching
Cache aside
在Cache aside缓存策略中,读操作首先会检查缓存,看看是否已经有所需数据的副本。如果缓存中存在数据,应用程序直接从缓存中读取。如果缓存中不存在数据,应用程序会从数据存储中读取数据,然后手动将数据存储到缓存中,以备将来使用。
写操作中,会先更新数据库,再删除缓存。再重新加载缓存。 以下是几种使用cache aside写操作实现高并发且数据一致的实现方案:
缓存延迟双删
常见的做法是先更新或删除数据库中的数据,然后再更新或删除缓存中的数据,以保持数据的一致性。
下面是go代码的实例:
package main
import (
"fmt"
"sync"
"time"
)
type DelayDoubleDelete struct {
dataStorage DataStorage
cache Cache
mutex sync.Mutex
}
func NewDelayDoubleDelete(dataStorage DataStorage, cache Cache) *DelayDoubleDelete {
return &DelayDoubleDelete{
dataStorage: dataStorage,
cache: cache,
}
}
func (ddd *DelayDoubleDelete) UpdateData(key, value string) error {
ddd.mutex.Lock()
defer ddd.mutex.Unlock()
err := ddd.dataStorage.Set(key, value)
if err != nil {
return err
}
go func() {
time.Sleep(time.Second) // Simulate delay
ddd.cache.Delete(key)
}()
return nil
}
func (ddd *DelayDoubleDelete) DeleteData(key string) error {
ddd.mutex.Lock()
defer ddd.mutex.Unlock()
err := ddd.dataStorage.Delete(key)
if err != nil {
return err
}
go func() {
time.Sleep(time.Second) // Simulate delay
ddd.cache.Delete(key)
}()
return nil
}
但是,缓存延迟双删机制仍然可能遇到问题,例如:
- 更新数据时的问题:当要更新一个数据时,首先更新了数据库中的数据,但由于网络延迟或其他问题,更新缓存的操作失败了,导致缓存中的旧数据仍然有效。
- 删除数据时的问题:同样地,当要从数据库中删除一个数据时,首先从数据库中删除了数据,但删除缓存的操作失败了,导致缓存中仍然存在数据的副本。
Read/write through
-
Read-Through: 在Read-Through模式中,当应用程序需要从数据存储(如数据库)中读取数据时,它首先会检查缓存。如果缓存中不存在所需数据,应用程序会请求数据存储,然后将获取的数据存储到缓存中,以便将来使用。这种方式可以显著减少对数据存储的频繁访问,提高读取操作的性能。应用程序在读取数据时不需要关心缓存的存在,所有的缓存操作都由缓存层透明地处理。
-
Write-Through: 在Write-Through模式中,当应用程序需要写入数据时,它首先会将数据更新写入到缓存中,然后再将数据写入到数据存储中。这样做的好处是,保证了数据存储和缓存的一致性,因为数据存储总是先于缓存进行更新。写入数据时,应用程序不需要等待数据存储的确认,因为数据存储的更新是异步进行的。然而,这可能会引入一定的写入延迟,因为每次写入都需要更新缓存和数据存储
Write behind
Write-Behind适用于写入频率较高但对于实时数据一致性要求相对较低的场景。它可以在一定程度上平衡数据写入性能和数据一致性之间的关系。然而,由于数据存储的更新是异步的,因此在使用Write-Behind时需要仔细考虑数据一致性、错误处理和失败恢复机制