当线上接口请求量比较大时,若是刚好遇到缓存失效,会形成大量的请求直接打到数据库,致使数据库压力过大、甚至崩溃。若是缓存的数据实时性要求不那么高,能够试试 do-once-while-concurrent
https://github.com/abusizhish...mysql
do-once-while-concurrent
中有三个主要方法,git
Req 方法
对具备同一资源标识的请求进行拦截Wait 方法
等待数据Release 方法
广播信号,数据已就位
下面是一个简单的示例
咱们的实际项目中有 两级缓存
,一级 本地缓存
,一级 redis
,若是都查询不到才会 读取mysql
或 调用中台接口
,本次只模拟 本地缓存失效
时, do-once-while-concurrent
对防止 缓存穿透
的处理(实际叫 重复资源过滤
更合理)github
1.缓存失效时, 全部请求该缓存的请求会先调用 Req方法
对具备相同标签的重复请求进行拦截
2.只有第一个请求会 获取锁
,执行读取redis操做
3.全部其余的线程 获取锁
失败,调用 Wait
方法,等待第一个线程 执行结束
4.第一个线程读取到用户信息,写入本地缓存,经过 close(chan)
事件来 广播消息
5.其余线程收到消息,结束 等待
,读取本地缓存,返回用户信息redis
package main import ( "errors" "fmt" "github.com/abusizhishen/do-once-while-concurrent/src" "log" "sync" "time" ) func main() { //并发do something for i := 0; i < 5; i++ { go doSomeThing() } //避免程序直接退出 time.Sleep(time.Second * 5) } var once src.DoOnce //模拟获取用户信息 func doSomeThing() { var userId = 12345 var user, err = getUserInfo(userId) fmt.Println(user, err) } //example for usage // 演示获取用户详情的过程,先从本地缓存读取用户,若是本地缓存不存在,就从redis读取 var keyUser = "user_%d" func getUserInfo(userId int) (user UserInfo, err error) { user, err = userCache.GetUser(userId) if err == nil { return } log.Println(err) var requestTag = fmt.Sprintf(keyUser, userId) if !once.Req(requestTag) { log.Println("没抢到锁,等待抢到锁的线程执行结束。。。") once.Wait(requestTag) log.Println("等待结束:", requestTag) return userCache.GetUser(userId) } //获得资源后释放锁 defer once.Release(requestTag) log.Println(requestTag, "得到锁,let's Go") //为演示效果,sleep time.Sleep(time.Second * 3) //redis读取用户信息 log.Println("redis读取用户信息:", userId) user, err = getUserInfoFromRedis(userId) if err != nil { return } //用户写入缓存 log.Println("用户写入缓存:", userId) userCache.setUser(user) return } //用户信息缓存 type UserCache struct { Users map[int]UserInfo sync.RWMutex } type UserInfo struct { Id int Name string Age int } var userCache UserCache var errUserNotFound = errors.New("user not found in cache") func (c *UserCache) GetUser(id int) (user UserInfo, err error) { c.RLock() defer c.RUnlock() var ok bool user, ok = userCache.Users[id] if ok { return } return user, errUserNotFound } func (c *UserCache) setUser(user UserInfo) { c.Lock() defer c.Unlock() if c.Users == nil { c.Users = make(map[int]UserInfo) } c.Users[user.Id] = user return } func getUserInfoFromRedis(id int) (user UserInfo, err error) { user = UserInfo{ Id: 12345, Name: "abusizhishen", Age: 18, } return }
输出sql
2020/03/09 20:11:39 user not found in cache 2020/03/09 20:11:39 user_12345 得到锁,let's Go 2020/03/09 20:11:39 user not found in cache 2020/03/09 20:11:39 没抢到锁,等待抢到锁的线程执行结束。。。 2020/03/09 20:11:39 user not found in cache 2020/03/09 20:11:39 没抢到锁,等待抢到锁的线程执行结束。。。 2020/03/09 20:11:39 user not found in cache 2020/03/09 20:11:39 user not found in cache 2020/03/09 20:11:39 没抢到锁,等待抢到锁的线程执行结束。。。 2020/03/09 20:11:39 没抢到锁,等待抢到锁的线程执行结束。。。 2020/03/09 20:11:42 redis读取用户信息: 12345 2020/03/09 20:11:42 用户写入缓存: 12345 2020/03/09 20:11:42 等待结束: user_12345 2020/03/09 20:11:42 等待结束: user_12345 {12345 abusizhishen 18} <nil> {12345 abusizhishen 18} <nil> {12345 abusizhishen 18} <nil> 2020/03/09 20:11:42 等待结束: user_12345 {12345 abusizhishen 18} <nil> 2020/03/09 20:11:42 等待结束: user_12345 {12345 abusizhishen 18} <nil>
能够看到,当第一个线程 获取锁
后,其余线程所有处于 等待状态
,直到第一个线程 执行结果
,释放锁
,其余线程 获取到数据
,返回结果数据库
事实上不止于防止 缓存穿透
, do-once-while-concurrent
更准确的定位是 重复资源过滤
,,在某讲座业务中,使用 do-once-while-concurrent
来避免同一时刻同一用户id 重复解析
、列表页 重复检索
、排序
等,减小了资源竞争,提升了总体的qps
和稳定性
缓存