// 解决缓存穿透
if exist := bloomFilter.find(ID); !exist {
return err("not found")
}
// 查询redis
if v, ok := redis.get(ID); ok {
return v
}
lock(ID) // 使用redis实现的分布式锁,解决缓存击穿
defer unLock(ID)
// 再查询一次redis
if v, ok := redis.get(ID); ok {
return v
}
// 查询mysql
if v, ok := mysql.get(ID); ok {
redis.set(ID)
return v
}
return err("not found")
缓存穿透
请求没有的数据,所以每次都会redis查不到mysql也查不到。
预防:
1. redis放空值,设置过期
2. bloom filter:类似于哈希表的一种算法,用所有可能的查询条件生成一个bitmap,在进行数据库查询之前会使用这个bitmap进行过滤,如果不在其中则直接过滤,从而减轻数据库层面的压力。google的guava中有实现BloomFilter算法。
缓存击穿
大量请求去mysql访问同一条数据,之前redis并没有去缓存这条记录。
避免方案:
1、个别数据提前预热到Redis,例如在活动整点开始的场景
2、分库分表 增加mysql读性能
缓存雪崩
预防:
1. redis挂掉的情况
redis高可用集群
2. 缓存集体过期的情况
错开数据过期时间
应对:
降级熔断
mysql和redis数据一致性
摘要:
【1】双删为什么要延时?因为update连的是主库,get连的是从库(回源过程),所以:延时时长=db主从同步时长
【2】延时双删的方式:
(1)key过期-修改db 。【延时双删在实践中会使用置key过期来实现,即:先过期key然后再改db】
(2)key过期-修改db-修改key。【相较(2)主动修改key以提高性能】
(3)另起协程睡眠。【弊端:无持久化机制,大量协程gc耗资源】
首先看下,一般情况下是如何操作Redis与数据库的一致的:
方案1:
- 更新的时候,先更新数据库,然后再删除缓存。
- 读的时候,先读缓存;如果没有的话,就读数据库,同时将数据放入缓存,并返回响应。
乍一看,一致性问题貌似很好的得到了解决。但仔细一想,你会发现还是有问题:如果先更新了数据库,删除缓存的时候失败了怎么办?那么数据库中是新数据,缓存中是老数据,数据出现不一致了。
方案1的改进,方案2:
先删除缓存,后更新数据库。因为即使后面更新数据库失败了,缓存是空的,读的时候会从数据库中重新拉,虽然都是旧数据,但数据是一致的。
所以方案就变成了:
- 更新的时候,先删除缓存,然后再更新数据库。
- 读的时候,先读缓存;如果没有的话,就读数据库,同时将数据放入缓存,并返回响应。
但是也不行,因为在高并发的场景下,会出现这样的情况:数据发生了变更,先删除了缓存,然后去修改数据库。此时还没来得及修改,一个请求过来了,去读缓存,发现缓存空了,去读数据库,读到了准备修改前的旧数据,并且把旧数据放到了缓存。随后,数据变更程序完成了数据库的修改。那么完了,这个时候发生数据不一致了......
所以,可行的方案有二:
可行方案1. 延时双删
下面的第五步,线程A再去删一次缓存,可以添加到延时消息队列,消费时重复尝试删除n次,一定要确保删除成功
可行方案2. 串行化(感兴趣的可以看下)
实例:
// 新增:添加到布隆过滤器,添加到数据库,通过kafka添加到es
// 修改:删掉redis当前记录,修改数据库当前记录,通过kafka删掉es当前记录,延时双删redis
// 删除:删掉redis当前记录,删掉数据库当前记录,通过kafka删掉es当前记录,延时双删redis
// 获取:布隆过滤,从redis获取,如果redis没有则从数据库获取,期间加锁,如果锁被抢占,则延时后再从redis获取,获取到则返回,否则去数据库查询
// es里的获取:不走redis
func (service *Service) CreateTopic(ctx context.Context, topicDetail *model.TopicDetail) error {
if err := dao.TiDBInstance.CreateTopic(ctx, topicDetail); err != nil {
currErr := fmt.Errorf("[service] CreateTopic dao.TiDBInstance.CreateTopic err: %v", err)
service.Log.Error(currErr)
sentry.CaptureException(currErr)
return err
}
f := func() error {
// 添加到bloom filter
if err := dao.RedisInstance.SetBitTopics(ctx, []int64{topicDetail.ID}); err != nil {
currErr := fmt.Errorf("[service] CreateTopic BloomFilter dao.RedisInstance.SetBitTopics err: %v", err)
service.Log.Error(currErr)
sentry.CaptureException(currErr)
// TiDB 补偿
rowsAffected, delErr := dao.TiDBInstance.DelTopicByIdsWithoutUserBehavior(ctx, []int64{topicDetail.ID})
if delErr != nil {
currDelErr := fmt.Errorf("[service] CreateTopic 补偿 dao.TiDBInstance.DelTopicByIds err: %v ", delErr)
service.Log.Error(currDelErr)
sentry.CaptureException(currDelErr)
return currDelErr
}
if rowsAffected != 1 {
rowsAffectedErr := fmt.Errorf(
"[service] CreateTopic 补偿 dao.TiDBInstance.DelTopicByIds rowsAffected: %v != 1", rowsAffected)
service.Log.Error(rowsAffectedErr)
sentry.CaptureException(rowsAffectedErr)
return rowsAffectedErr
}
return err
}
// Index to es
basicUtil.PubEvent(service.Pub, topic.EV_DM_TOPIC, event.DataEventUint64_NEW, topicDetail.ID)
return nil
}
// lock
return dao.RedisInstance.LockWrap(ctx, model.GetKeyForLockTopic(topicDetail.ID), f)
}
func (service *Service) UpdateTopic(ctx context.Context, topicDetail *model.TopicDetail) (int64, error) {
var rowsAffected int64
f := func() error {
// Redis
if err := dao.RedisInstance.Del(ctx, []string{model.GetKeyForTopic(topicDetail.ID)}); err != nil {
currErr := fmt.Errorf("[service] UpdateTopic dao.RedisInstance.Del err: %v, key: %v",
err, model.GetKeyForTopic(topicDetail.ID))
service.Log.Error(currErr)
sentry.CaptureException(currErr)
return err
}
// TiDB
var updateErr error
rowsAffected, updateErr = dao.TiDBInstance.UpdateTopicWithoutUserBehavior(ctx, topicDetail)
if updateErr != nil {
currErr := fmt.Errorf("[service] UpdateTopic dao.TiDBInstance.UpdateTopicWithoutUserBehavior err: %v id: %v",
updateErr, topicDetail.ID)
service.Log.Error(currErr)
sentry.CaptureException(currErr)
return updateErr
}
if rowsAffected != 1 {
service.Log.Infof("[service] UpdateTopic dao.TiDBInstance.UpdateTopic rowsAffected: %v != 1", rowsAffected)
return nil
} else {
// Index to es
basicUtil.PubEvent(service.Pub, topic.EV_DM_TOPIC, event.DataEventUint64_NEW, topicDetail.ID)
}
// 延时双删
go func() {
time.Sleep(200 * time.Millisecond)
ddl := time.Now().Add(time.Duration(60) * time.Second)
for tries := 0; time.Now().Before(ddl); tries++ {
if err := dao.RedisInstance.Del(context.Background(), []string{model.GetKeyForTopic(topicDetail.ID)}); err != nil {
currErr := fmt.Errorf("[service] UpdateTopic 延时双删 dao.RedisInstance.Del err: %v, key: %v",
err, model.GetKeyForTopic(topicDetail.ID))
service.Log.Error(currErr)
sentry.CaptureException(currErr)
} else {
return
}
time.Sleep(time.Second << uint(tries))
}
}()
return nil
}
// lock
return rowsAffected, dao.RedisInstance.LockWrap(ctx, model.GetKeyForLockTopic(topicDetail.ID), f)
}
func (service *Service) DelTopicById(ctx context.Context, id int64) (int64, error) {
var rowsAffected int64
f := func() error {
// Redis
if err := dao.RedisInstance.Del(ctx, []string{model.GetKeyForTopic(id)}); err != nil {
currErr := fmt.Errorf("[service] DelTopicById dao.RedisInstance.Del err: %v key: %v", err, model.GetKeyForTopic(id))
service.Log.Error(currErr)
sentry.CaptureException(currErr)
return err
}
// TiDB
var delErr error
rowsAffected, delErr = dao.TiDBInstance.DelTopicByIdsWithoutUserBehavior(ctx, []int64{id})
if delErr != nil {
currErr := fmt.Errorf("[service] DelTopicById dao.TiDBInstance.Del err: %v id: %v", delErr, id)
service.Log.Error(currErr)
sentry.CaptureException(currErr)
return delErr
}
if rowsAffected != 1 {
rowsAffectedErr := fmt.Errorf(
"[service] DelTopicByIds dao.TiDBInstance.DelTopicByIds rowsAffected: %v != 1", rowsAffected)
service.Log.Info(rowsAffectedErr)
return rowsAffectedErr
} else {
basicUtil.PubEvent(service.Pub, topic.EV_DM_TOPIC, event.DataEventUint64_DELETE, id)
}
// 延时双删
go func() {
time.Sleep(200 * time.Millisecond)
ddl := time.Now().Add(time.Duration(60) * time.Second)
for tries := 0; time.Now().Before(ddl); tries++ {
if err := dao.RedisInstance.Del(context.Background(), []string{model.GetKeyForTopic(id)}); err != nil {
currErr := fmt.Errorf("[service] DelTopicById 延时双删 dao.RedisInstance.Del err: %v, key: %v",
err, model.GetKeyForTopic(id))
service.Log.Error(currErr)
sentry.CaptureException(currErr)
} else {
return
}
time.Sleep(time.Second << uint(tries))
}
}()
return nil
}
// lock
return rowsAffected, dao.RedisInstance.LockWrap(ctx, model.GetKeyForLockTopic(id), f)
}
func (service *Service) DelTopicByIds(ctx context.Context, ids []int64) (int64, error) {
var rowsAffected int64
for _, id := range ids {
r, err := service.DelTopicById(ctx, id)
if err != nil {
continue
}
rowsAffected += r
}
return rowsAffected, nil
}
func (service *Service) GetTopicByIds(ctx context.Context, preIds []int64, withStatistics, withUserBehavior bool,
userID string) (map[int64]*model.TopicInfo, map[int64]*model.TopicStatistic, error) {
topicInfos := make(map[int64]*model.TopicInfo, 0)
topicStatistics := make(map[int64]*model.TopicStatistic, 0)
// TODO 因redis暂不支持withUserBehavior
if withUserBehavior {
currErr := fmt.Errorf("[service] GetTopicByIds withUserBehavior 暂不支持")
service.Log.Error(currErr)
sentry.CaptureException(currErr)
return topicInfos, topicStatistics, currErr
}
// 缓存穿透
ids, err := dao.RedisInstance.GetBitTopics(ctx, preIds)
if err != nil {
currErr := fmt.Errorf("[service] GetTopicByIds BitMap dao.RedisInstance.GetBitTopics err: %v", err)
service.Log.Error(currErr)
sentry.CaptureException(currErr)
return topicInfos, topicStatistics, currErr
}
if len(ids) == 0 {
return topicInfos, topicStatistics, &common.InternalError{
ErrCode: int32(pb.GetTopicByIdsResp_NOT_FOUND),
ErrMsg: pb.GetTopicByIdsResp_ErrCode_name[int32(pb.GetTopicByIdsResp_NOT_FOUND)],
}
}
sw := sync.WaitGroup{}
// 从bi-svc获取统计数据
var biErr error
if withStatistics {
sw.Add(1)
go func() {
biCtx, _ := context.WithTimeout(ctx, 3*time.Second)
service.Log.Infof("[service] GetTopicByIds bi-svc.TopicStatisticsFromBI ids: %v", ids)
topicStatistics, biErr = service.TopicStatisticsFromBI(biCtx, ids)
sw.Done()
}()
}
// 首次从redis获取
topicInfosRedis, redisGetErr := dao.RedisInstance.GetTopics(ctx, ids)
if redisGetErr != nil {
currErr := fmt.Errorf("[service] GetTopicByIds dao.RedisInstance.GetTopics err: %v", redisGetErr)
service.Log.Error(currErr)
sentry.CaptureException(currErr)
return topicInfos, topicStatistics, redisGetErr
}
lackArr := make([]int64, 0) // redis缺失 首次
lackMap := make(map[int64]bool, 0) // redis缺失 首次 map
lastLackArr := make([]int64, 0) // redis缺失 最终
for _, id := range ids {
if v, ok := topicInfosRedis[id]; ok {
topicInfos[id] = v
} else {
lackArr = append(lackArr, id)
lackMap[id] = true
}
}
// 从TiDB获取
if len(lackArr) != 0 {
for _, lackID := range lackArr {
// 缓存击穿
lockedIDs := make([]int64, 0) // 锁被抢占
if err := dao.RedisInstance.Lock(ctx, model.GetKeyForLockTopicForGetsByTiDB(lackID)); err != nil {
lockedIDs = append(lockedIDs, lackID)
}
// 锁被抢占 延时再次从redis获取
if len(lockedIDs) != 0 {
time.Sleep(500 * time.Millisecond)
// again: get topicInfo from redis
internalTopicInfosRedis, internalRedisGetErr := dao.RedisInstance.GetTopics(ctx, lockedIDs)
if internalRedisGetErr != nil {
currErr := fmt.Errorf("[service] GetTopicByIds dao.RedisInstance.GetTopics err: %v", internalRedisGetErr)
service.Log.Error(currErr)
sentry.CaptureException(currErr)
return topicInfos, topicStatistics, internalRedisGetErr
}
// 添加到结果,并与lackArr差集生成lastLackArr。包含len(internalTopicInfosRedis)==0的情况
for _, v := range internalTopicInfosRedis {
if v.TopicDetail != nil {
topicInfos[v.TopicDetail.ID] = v
if _, ok := lackMap[v.TopicDetail.ID]; ok {
lackMap[v.TopicDetail.ID] = false
}
}
}
for i, v := range lackMap {
if v {
lastLackArr = append(lastLackArr, i)
}
}
} else { // 全部加锁成功
// 生成lastLackArr
lastLackArr = lackArr
}
}
// 到TiDB
if len(lastLackArr) != 0 {
// 仅对lastLackArr解锁
defer func() {
for _, lackID := range lastLackArr {
if unLockErr := dao.RedisInstance.UnLock(ctx, model.GetKeyForLockTopicForGetsByTiDB(lackID)); unLockErr != nil {
currErr := fmt.Errorf("[redis] GetTopicByIds Redis UnLock return unLockErr: %v", unLockErr)
service.Log.Error(currErr)
sentry.CaptureException(currErr)
}
}
}()
topicInfosTiDB, err := dao.TiDBInstance.GetTopicByIds(ctx, lastLackArr, withUserBehavior, userID)
if err != nil {
currErr := fmt.Errorf("[service] GetTopicByIds [从tidb补充] dao.TiDBInstance.GetTopicByIds err: %v", err)
service.Log.Error(currErr)
sentry.CaptureException(currErr)
return topicInfos, topicStatistics, err
}
// 添加到结果
for _, v := range topicInfosTiDB {
if v.TopicDetail != nil {
topicInfos[v.TopicDetail.ID] = v
}
}
// 写入redis
topicInfosTiDBArr := make([]*model.TopicInfo, 0)
for _, v := range topicInfosTiDB {
topicInfosTiDBArr = append(topicInfosTiDBArr, v)
}
redisSetErr := dao.RedisInstance.SetTopics(ctx, topicInfosTiDBArr)
if redisSetErr != nil {
currErr := fmt.Errorf("[service] GetTopicByIds [补充到redis] dao.RedisInstance.SetTopics err: %v", redisSetErr)
service.Log.Error(currErr)
sentry.CaptureException(currErr)
return topicInfos, topicStatistics, redisSetErr
}
}
}
// 从bi-svc获取统计数据
sw.Wait()
if biErr != nil {
currErr := fmt.Errorf("[service] GetTopicByIds bi-svc.TopicStatisticsFromBI err: %v", biErr)
service.Log.Error(currErr)
sentry.CaptureException(currErr)
return topicInfos, topicStatistics, biErr
}
return topicInfos, topicStatistics, nil
}
以上是redis的key有过期策略的情况。
还有一种是redis和数据库强一致,redis的key没有过期策略,强一致的更新数据库和redis,实例:
func (service *Service) CreateItem(ctx context.Context, item *model.Item) error {
// random key/secret
item.Key = common.RandStringBytes(32)
item.Secret = common.RandStringBytes(32)
// TiDB
if err := dao.TiDBInstance.CreateItem(ctx, item); err != nil {
service.Log.Error(err)
sentry.CaptureException(err)
return err
}
f := func() error {
// Redis
if err := dao.RedisInstance.HMSetItem(ctx, model.GetKeyForItemHM(item.Key), item.ConvToRedis()); err != nil {
service.Log.Error(err)
sentry.CaptureException(err)
// TiDB 补偿
rowsAffected, delErr := dao.TiDBInstance.DelItemByIds(ctx, []int64{item.ID})
if delErr != nil {
service.Log.Error(delErr)
sentry.CaptureException(delErr)
return delErr
}
if rowsAffected != 1 {
rowsAffectedErr := fmt.Errorf(
"[service] CreateItem TiDBInstance.DelItemByIds 补偿 rowsAffected: %v != 1", rowsAffected)
service.Log.Error(rowsAffectedErr)
sentry.CaptureException(rowsAffectedErr)
return rowsAffectedErr
}
return err
}
return nil
}
// lock
return dao.RedisInstance.LockWrap(ctx, model.GetKeyForLockItemString(item.ID), f)
}
func (service *Service) UpdateItem(ctx context.Context, item *model.Item) error {
f := func() error {
// TiDB get ori
oriItems, getErr := dao.TiDBInstance.GetItemByIds(ctx, []int64{item.ID})
if getErr != nil {
service.Log.Error(getErr)
sentry.CaptureException(getErr)
return getErr
}
if len(oriItems) != 1 {
getItemByIdsErr := fmt.Errorf("[service] UpdateItem dao.TiDBInstance.GetItem notFound, len(oriItems): %v", len(oriItems))
service.Log.Error(getItemByIdsErr)
sentry.CaptureException(getItemByIdsErr)
return getItemByIdsErr
}
// TiDB update curr
rowsAffected, updateErr := dao.TiDBInstance.UpdateItem(ctx, item)
if updateErr != nil {
service.Log.Error(updateErr)
sentry.CaptureException(updateErr)
return updateErr
}
if rowsAffected != 1 {
service.Log.Infof("[service] UpdateItem dao.TiDBInstance.UpdateItem rowsAffected: %v != 1", rowsAffected)
return nil
}
// Redis update curr
if err := dao.RedisInstance.HMSetItem(ctx, model.GetKeyForItemHM(oriItems[0].Key), item.ConvToRedis()); err != nil {
service.Log.Error(err)
sentry.CaptureException(err)
// TiDB 补偿
oriRowsAffected, oriUpdateErr := dao.TiDBInstance.UpdateItem(ctx, oriItems[0])
if oriUpdateErr != nil {
service.Log.Error(oriUpdateErr)
sentry.CaptureException(oriUpdateErr)
return oriUpdateErr
}
if oriRowsAffected != 1 {
updateOriItemErr := fmt.Errorf("[service] UpdateItem dao.TiDBInstance.UpdateOriItem 补偿 oriRowsAffected: %v != 1", oriRowsAffected)
service.Log.Error(updateOriItemErr)
sentry.CaptureException(updateOriItemErr)
return updateOriItemErr
}
return err
}
return nil
}
// lock
return dao.RedisInstance.LockWrap(ctx, model.GetKeyForLockItemString(item.ID), f)
}