Redis 분산 잠 금 기반 구현 (주파수 제한)

수요
호텔 자원 시스템 은 주문 과 견적 을 조회 할 때 제3자 공급 업 체 시스템 을 호출 합 니 다.사용자 가 많 고 주문량 이 많 으 며 QPS 가 높 은 배경 이 있 기 때문에 자원 시스템 은 클 러 스 터 배치 방식 으로 12 대의 기 계 를 배 치 했 고 nginx 를 사용 하여 부하 균형 을 이 루 며 각 노드 에 골 고루 쳤 다.그러나 시스템 성능 요 소 를 위해 공급 업 체 인터페이스 에 주파수 제한 을 추가 해 분당 2000 회 를 넘 으 면 안 된다.
2. 수요 분석
자원 시스템 은 1 분 동안 모든 서비스 노드 를 합 친 호출 공급 업 체 인터페이스 요구 수 는 2000 을 초과 할 수 없다.
  • 단일 노드 라면 쉽게 이 루어 집 니 다. 계수 기 를 하나 추가 하면 범 위 를 초과 하면 방문 을 중단 합 니 다.병발 이 비교적 높 을 때 도 카운터 에 동기 화 자 물 쇠 를 추가 하여 같은 시간 에 하나의 방법 이 하나의 스 레 드 에 만 실 행 될 수 있 도록 하면 된다.
  • 그러나 다 중 노드 의 경우 서로 다른 노드 의 요청 은 하나의 프로 세 스 에 속 하지 않 고 jvm 에 속 하지 않 습 니 다.분포 식 환경 에서 잠 금 메커니즘 은 서로 다른 기계 의 라인 을 공유 할 수 없고 보이 지 않 는 다.데이터 의 최종 일치 성 을 보장 할 수 없다.

  • 따라서 같은 시간 에 한 기계 의 다음 라인 에 만 실 행 될 수 있 도록 분포 식 자 물 쇠 를 추가 해 야 한다.
    3. 분포 식 자 물 쇠 는 어떤 조건 을 갖 추어 야 합 니까?
    (1) 같은 시간 에 한 방법 은 한 기계 의 다음 라인 에 만 실 행 될 수 있 도록 보증한다.(2) 높 은 사용 가능 한 잠 금 해제 와 잠 금 획득 (3) 고성능 의 잠 금 해제 와 잠 금 획득 (4) 은 재 접속 성 (재 접속 으로 이해 할 수 있 으 며, 한 스 레 드 는 여러 번 같은 방법 을 호출 하 며, 데이터 오 류 를 걱정 하지 않 아 도 된다) (5) 잠 금 체제 의 실 효 를 가지 고 있 으 며, 잠 금 (6) 이 차단 되 지 않 은 잠 금 기능 을 가지 고 있다. 즉, 잠 금 을 얻 지 못 하면 바로 잠 금 획득 실패 로 돌아 갈 것 이다.
    4. 흔히 볼 수 있 는 분포 식 자물쇠 의 실현
  • Memcached: Memcached 의 add 명령 을 이용 합 니 다.이 명령 은 키 가 존재 하지 않 는 상황 에서 만 add 가 성공 할 수 있 으 며, 스 레 드 가 잠 겨 있 음 을 의미 합 니 다.
  • Redis: Memcached 방식 과 유사 하 며 Redis 의 setnx 명령 을 이용 합 니 다.이 명령 은 키 가 존재 하지 않 는 상황 에서 만 set 에 성공 할 수 있 는 원자 동작 입 니 다.
  • Zookeeper: Zookeeper 의 순서 임시 노드 를 이용 하여 분포 식 잠 금 과 대기 열 을 실현 합 니 다.Zookeeper 디자인 의 취 지 는 분포 식 잠 금 서 비 스 를 실현 하기 위 한 것 이다.

  • 5. Redis 분포 식 자물쇠 의 실현 원 리 를 바탕 으로
    1. 자물쇠 추가
    가장 쉬 운 방법 은 setnx 명령 입 니 다. key 가 존재 하지 않 는 다 면 이 스 레 드 가 자 물 쇠 를 성공 적 으로 가 져 왔 고 자 물 쇠 를 추가 하여 1 을 되 돌려 줍 니 다.키 가 존재 하면 잠 금 쟁탈 에 실 패 했 음 을 설명 하고 0 을 되 돌려 줍 니 다.호텔 방 상태 인 터 페 이 스 를 가 져 오 면 key = hoteld + checkIn + checkOut + 입주 인원 num, 위조 코드 는 다음 과 같 습 니 다.
    setnx(key,value)
    

    2. 잠 금 해제
    자물쇠 가 있 으 면 자물쇠 가 있어 야 한다.잠 긴 스 레 드 가 작업 을 마 쳤 을 때 다른 스 레 드 가 들 어 갈 수 있 도록 잠 금 을 풀 어야 합 니 다.자 물 쇠 를 풀 수 있 는 가장 쉬 운 방법 은 del 명령 을 실행 하 는 것 입 니 다. 위조 코드 는 다음 과 같 습 니 다.
    del(key)
    

    3. 잠 금 시간 초과
    만약 에 자 물 쇠 를 얻 은 스 레 드 가 작업 을 수행 하 는 과정 에서 끊 어 지면 자 물 쇠 를 명시 적 으로 풀 지 못 하면 이 자원 은 영원히 잠 겨 있 고 다른 스 레 드 는 더 이상 들 어 올 생각 을 하지 마 세 요.따라서 setnx 의 key 는 시간 초과 시간 을 설정 하여 명시 적 으로 풀 리 지 않 더 라 도 이 자 물 쇠 는 일정 시간 후에 자동 으로 풀 려 나 야 합 니 다.setnx 는 시간 초과 인 자 를 지원 하지 않 기 때문에 추가 명령 이 필요 합 니 다. 의사 코드 는 다음 과 같 습 니 다.
    if(setnx(key,value) == 1{
        expire(key,30try {
            do something ......
        } finally {
            del(key)
        }
    }
    

    상기 코드 에 존재 하 는 치 명 적 인 문제:
    현상 1: setnx 와 expire 의 비 원자 성
    · 노드 1 이 setnx 를 실행 하고 자 물 쇠 를 성공 적 으로 가 져 온 후에 expire 를 실행 하지 못 했 습 니 다. 노드 1 이 끊 겼 습 니 다. 그러면 만 료 시간 을 설정 하지 않 았 습 니 다.다른 노드 도 이 자 물 쇠 를 얻 을 수 없어 서 자물쇠 가 사라 집 니 다.
    해결 방법: setnx 대신 set 명령 을 사용 하거나 lua 스 크 립 트 를 사용 하여 작업 의 원자 성 을 확보 합 니 다.
    set(lock_sale_  ID,130,NX)
    lua  :
    eval:“if redis.call('set',KEYS[1],ARGV[1]) then return redis.call('expire',KEYS[1],ARGV[2]) else return 0 end”
    

    현상 2: del 오류 발생
    만약 에 특정한 스 레 드 가 자 물 쇠 를 가 져 온 후에 시간 초과 시간 을 30 초 로 설정 하지만 어떤 이유 로 업무 수행 이 느 려 서 30 초 동안 실행 되 지 않 았 습 니 다.이 때 노드 1 은 자 물 쇠 를 풀 고 노드 2 는 자 물 쇠 를 가 져 옵 니 다.노드 1 은 업무 코드 를 실행 한 후에 도 del 작업 을 수행 합 니 다. 실제로 이때 삭 제 된 것 은 노드 2 의 자물쇠 입 니 다.
    해결 방법: del 삭 제 를 실행 하기 전에 값 판단 을 추가 하여 현재 자물쇠 가 자신 이 추가 한 자물쇠 인지 확인 합 니 다.
    lua  :
    eval: "if redis.call('get',KEYS[1]) == ARGV[1] then return 
    redis.call('del',KEYS[1]) else return 0 else

    현상 3: 병발 가능성, 노드 2 와 노드 1 이 동시에 자원 을 방문 할 가능성 을 버린다.
    현상 2 유형 과 비슷 한 노드 1: 자 물 쇠 를 가 져 오고 업무 코드 를 실행 하 며 시간 초과 노드 1: 자 물 쇠 를 방출 하 는 노드 2: 노드 1 에서 방출 되 는 자물쇠 노드 1: 업무 코드 를 실행 하고 자 물 쇠 를 방출 하 는 노드 2: 자 물 쇠 를 방출 할 때 노드 1 에서 노드 2 의 자 물 쇠 를 방출 하지 않 는 것 을 피한다.그러나 노드 2 에서 노드 1 이 너무 오래 실행 되 어 풀 린 자 물 쇠 를 얻 는 것 을 피 할 수 없다.이것 도 우리 가 원 하 는 결과 가 아니다.
    해결 방법: (1) 실패 노드 1 업무 스크롤 기록 로 그 를 풀 고 (2) 노드 2 는 데 몬 이 시간 초과 에 대한 연장 을 사용 하여 업무 가 완료 되 지 않 았 을 때 오류 가 잠 겨 있 는 것 을 방지 합 니 다.또한 입장 연장 횟수 를 일정 하 게 제한 하여 자원 을 장시간 점용 하지 않도록 해 야 합 니 다.
    lua  :
    eval:" if redis.call('set',KEYS[1],ARGV[1]) then if redis.call('expire',KEYS[1],ARGV[2])
    end local ttlTime = redis.call('ttl',KEYS[1]) if ttlTime == -1 then 
    redis.call('expire',KEYS[1],ARGV[2]) end”
    
    

    이번 수요 로 돌아 가 redis 의 자체 증가 카운터 로 주파수 제한 에 도달 하려 면 공급 업 체 방법 을 호출 하기 전에 redis + incr 명령 을 사용 하여 지정 한 key 에 저 장 된 수치 에 원자 추가 1 작업 을 수행 합 니 다.지정 한 key 가 존재 하지 않 으 면 incr 작업 을 수행 하기 전에 값 을 0 으로 설정 합 니 다.주파수 제한 을 위 한 코드: KEYS [1]: key ARGV [1]: 만 료 시간 ARGV [2]: 주파수 제한
    lua  :
    “local count = redis.call('incr',KEYS[1]) if count == 1 then redis.call('expire',KEYS[1]
    ,ARGV[1]) end  local ttlTime = redis.call('ttl',KEYS[1]) if ttlTime == -1 
    then  redis.call('expire',"KEYS[1] , ARGV[1]) end  return count”
    

    6. 전체 코드:
    AOP 서 라운드 절단면 을 사용 하여 key, value, 주파수 제한 을 동적 으로 설정 하고 redis 를 사용 하여 분포 식 잠 금 을 실현 합 니 다.
        @RedisAPILimit(apiKey = “Key”,limit = count,sec = 60)
        public String getCtripRatePlan(Req req) throws Exception {}
    

    사용자 정의 설명:
    @Target(value = {ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    public @interface RedisAPILimit {
        //    
        int limit() default 5;
        //       5 
        int sec() default 5;
        String apiKey() default "";
    }
    

    AOP+Redis:
    @Around("@annotation(redisAPILimit)")
        public Object  around(ProceedingJoinPoint proceedingJoinPoint,RedisAPILimit redisAPILimit) throws Throwable {
            if (redisAPILimit == null) {
                return proceedingJoinPoint.proceed();
            }
            int limit = redisAPILimit.limit();
            int sec = redisAPILimit.sec();
            String apiKey = redisAPILimit.apiKey();
            Integer currentCount = (Integer)redisTemplate.opsForValue().get(apiKey);
            if (currentCount != null && currentCount>limit) {
                Long expire = redisTemplate.getExpire(apiKey, TimeUnit.SECONDS);
                log.info("{}     ,    {} ,     :{} ,    {}  ",apiKey,currentCount,limit,expire);
                //          ,      
                //throw new CtripException
                return null;
            }
            String script = "local count = redis.call('incr',KEYS[1]) if count == 1 then  redis.call('expire',KEYS[1] , " +
                    "ARGV[1]) end  local ttlTime = redis.call('ttl',KEYS[1]) if ttlTime == -1 then  redis.call('expire'," +
                    "KEYS[1] , ARGV[1]) end  return count " ;
            RedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class);
            Long incNum = (Long) redisTemplate.execute(redisScript, Collections.singletonList(apiKey),sec);
            return proceedingJoinPoint.proceed();
        }
    

    좋은 웹페이지 즐겨찾기