V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
爱意满满的作品展示区。
HXHL
V2EX  ›  分享创造

KeyLock 我做一个简单的开源库,可以用在防止缓存雪崩的读缓存的最佳实践之一中

  •  
  •   HXHL · 2023-12-25 11:05:32 +08:00 · 1282 次点击
    这是一个创建于 369 天前的主题,其中的信息可能已经有所发展或是发生改变。

    技术背景

    众所周知,为了提高了性能,我们会在数据库层前面加一个读缓存(redis or Memcached)。但是如果在高并发时候,有很多个请求,正好请求同一个值,而该值的缓存正好失效或者不在缓存中,就会导致这些很多请求都变成数据库查询。这就是缓存雪崩。

    为了解决这种问题,常见的思路就是给请求查缓存时加锁,最多只允许有一个请求查询数据库。

    比如有getProduct查商品 ID 为 1000 的商品的伪代码

      func getProduct(id:string) (product,error) {
      	  // 用 id 锁住
          // 从缓存中查
          // 查到就释放锁返回值
        
          // 没有查到就查数据库
          // 放到缓存中,释放锁,返回值
      }
    

    KeyLock

    但是在 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🌟

    点个 Star 呗

    HXHL
        1
    HXHL  
    OP
       2023-12-25 11:35:54 +08:00
    😪为什么会一个回复都没有呀。
    Gipserr
        2
    Gipserr  
       2023-12-25 11:39:34 +08:00
    因为查数据库都有锁,保证不会同时查。
    HXHL
        3
    HXHL  
    OP
       2023-12-25 12:03:15 +08:00 via iPhone
    @Gipserr 阿?真的吗,为什么书上说要加锁🤯
    kuituosi
        4
    kuituosi  
       2023-12-25 12:54:04 +08:00
    1.对雪崩的理解是错的
    2. singleflight 了解一下
    gitrebase
        5
    gitrebase  
       2023-12-25 13:08:14 +08:00
    1. #4 说的 singleflight 了解一下
    2. https://github.com/dtm-labs/rockscache 了解一下
    3. 这和分布式锁有什么区别吗
    HXHL
        6
    HXHL  
    OP
       2023-12-25 13:12:59 +08:00
    @gitrebase 🥲好的,我把帖子下沉了。主要是没有把这里搞明白。
    crysislinux
        7
    crysislinux  
       2023-12-25 17:14:05 +08:00 via Android
    singleflight 只能单进程用吧。所以不能说没意义。只是如果不是 call 第三方 api ,singleflight 基本也够用了。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   909 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 23ms · UTC 22:17 · PVG 06:17 · LAX 14:17 · JFK 17:17
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.