众所周知,为了提高了性能,我们会在数据库层前面加一个读缓存(redis or Memcached)。但是如果在高并发时候,有很多个请求,正好请求同一个值,而该值的缓存正好失效或者不在缓存中,就会导致这些很多请求都变成数据库查询。这就是缓存雪崩。
为了解决这种问题,常见的思路就是给请求查缓存时加锁,最多只允许有一个请求查询数据库。
比如有getProduct
查商品 ID 为 1000 的商品的伪代码
func getProduct(id:string) (product,error) { // 用 id 锁住 // 从缓存中查 // 查到就释放锁返回值 // 没有查到就查数据库 // 放到缓存中,释放锁,返回值 }
但是在 Golang 语言中,我找了一下没有找到现有的库,所以我自己做一个简单的 keylock 的开源库。
https://github.com/CorrectRoadH/keylock
现在有提供两种锁,一种是单体应用中的锁,另一种适合分布式服务中的分布式锁,需要 redis 。
这里提供一个单体应用中的 key 锁的简单的 demo
type UserService struct { store *store.UserStore cache *user.UserCacheAdapter apiv1pb.UnimplementedUserServiceServer keylock keylock.KeyLock } func NewUserService(store *store.UserStore, cache *user.UserCacheAdapter) *UserService { keylock,err := keylock.New() if err != nil { fmt.Println(err) } return &UserService{ store: store, cache: cache, keylock: keylock, } } func (s *UserService) UpsertUser(ctx context.Context, req *apiv1pb.UpsertUserRequest) (*apiv1pb.User, error) { s.cache.DeleteCache(ctx, req.User.Id) // 修改数据库之前删一次缓存 user, err := s.store.UpsertUser(ctx, convertUser(req.User)) s.cache.DeleteCache(ctx, req.User.Id) // 修改数据库之后再删一次缓存 if err != nil { return nil, err } return convertUserPb(user), nil } func (s *UserService) GetUser(ctx context.Context, req *apiv1pb.GetUserRequest) (*apiv1pb.User, error) { // 在查询缓存前先通过 UserId 锁住,最多允许一个线程进入数据库,防止缓存雪崩 s.keylock.Lock(req.Id) defer s.keylock.Unlock(req.Id) user, err := s.cache.User(ctx, req.Id) if err == nil { return convertUserPb(user), nil } user, err = s.store.User(ctx, req.Id) if err != nil { return nil, status.Error(codes.NotFound, err.Error()) } s.cache.UpsertUser(ctx, user) return convertUserPb(user), nil }
如果觉得这个库有用的话,不妨给我点个 Star
![]() | 1 HXHL OP 为什么会一个回复都没有呀。 |
2 Gipserr 2023-12-25 11:39:34 +08:00 因为查数据库都有锁,保证不会同时查。 |
4 kuituosi 2023-12-25 12:54:04 +08:00 1.对雪崩的理解是错的 2. singleflight 了解一下 |
![]() | 5 gitrebase 2023-12-25 13:0814 +08:00 |
![]() | 7 crysislinux 2023-12-25 17:14:05 +08:00 via Android singleflight 只能单进程用吧。所以不能说没意义。只是如果不是 call 第三方 api ,singleflight 基本也够用了。 |