组队大项目--采用redis分布式锁应对缓存击穿 | 青训营笔记

74 阅读2分钟

这是我参与「第五届青训营」笔记创作活动的第15天

缓存击穿概念

我们的业务通常会有几个数据会被频繁地访问,比如对某个大v的关注,这类被频地访问的数据被称为热点数据。

如果缓存中的某个热点数据过期了,此时大量的请求访问了该热点数据,就无法从缓存中读取,直接访问数据库,数据库很容易就被高并发的请求冲垮,这就是缓存击穿的问题。

应对缓存击穿的方案如下:

  • 互斥锁方案,保证同一时间只有一个业务线程更新缓存,未能获取互斥锁的请求,要么等待锁释放后重新读取缓存,要么就返回空值或者默认值。

项目实践

分布式锁是是用来控制分布式系统中互斥访问共享资源的一种手段,经常应用于高并发的场景中,也是防止缓存击穿的措施。在我们的项目中,例如在用户关注操作中,如果被关注的用户粉丝表不在缓存中,就需要去访问数据库刷新到缓存中,假如某一时刻某个大V的粉丝表缓存过期,而他又此刻突然涨粉,大量用户同时关注,就会用很多请求同时去访问数据库,这样的场景就叫缓存击穿,为此需要使用分布式锁,使得第一个拿到锁的用户得到大V的粉丝表后,将数据存到缓存中,其他用户就可以等待锁释放后,去缓存中直接取出,无需请求数据库,减少读取压力。

使用redis设计分布式锁是一种很常见的实现方式,使用redis的set nx ex命令进行加锁并设置过期时间,如果获得锁失败,则设置一个时间间隔再重新获取锁

set key randomvalue ex ttl nx

该命令中key是要加锁的对象,randomvalue可以用雪花算法生成随机值,用于解锁的校验

解锁时,对获取的锁的值进行校验,如果不相等说明原来的锁已被释放,现在的锁是其他协程加的,使用lua脚本保证原子性操作:

if redis.call("get", KEYS[1]) == ARGV[1] then
    return redis.call("del", KEYS[1])
else
    return 0
end

增加看门狗机制,加锁成功时启动一个协程,不断地延长锁的过期时间,释放锁的时候关闭看门狗

func (l *DistributedLock) startWatchDog() {
   delteTime := time.Duration(l.TTL / 3)
   ticker := time.NewTicker(delteTime)
   defer ticker.Stop()
   redisConn := redisPool.Get()
   defer redisConn.Close()
   for {
      select {
      case <-ticker.C:
         // 延长锁的过期时间
         ok, err := redis.Int(redisConn.Do("expire", l.Key, l.TTL))
         // 异常或锁已经不存在则不再续期
         if (err != nil) || ok == 0 {
            return
         }
      case <-l.watchDog:
         // 已经解锁
         return
      }
   }
}