redis 기반 분포 식 잠 금 실현 및 spring-boot-starter 통합

12745 단어 자바Springboot
글 목록
  • 개술
  • 사용
  • 1.가방 안내
  • 2.잠 금 기능 을 실현 하 는 service
  • 를 작성 합 니 다.
  • 3.redis 의 key 검사
  • 4.호출(잠 금 성공)
  • 5.호출(잠 금 실패)
  • 실현
  • 1.redislock 어떻게 실현
  • 2.어떻게 주해 화
  • 를 실현 합 니까?
  • 3.생 성 에이전트
  • 통합 spring-boot-starter
  • 분포 식 자물쇠 가 무엇 인지 아직 모른다 면 회색 블 로그 의 분포 식 자물쇠 가 무엇 인지 참고 하 세 요.
    이 글 은 redis 분포 식 자 물 쇠 를 어떻게 디자인 하 는 지 에 대해 redis 분포 식 자 물 쇠 를 구체 적 으로 실현 하고 기능 을 spring-boot 에 융합 시 켜 주해 화 를 실현 하 며 설정 이 매우 간소화 되 었 다.github 주 소 는 여기 있 습 니 다.
    개술
    이 글 은 주로 테스트 사례 를 결합 하여 redis 분포 식 잠 금 사용 을 소개 하고 실현 원 리 를 깊이 소개 한다.프로젝트 에 사용 하려 면 README 을 먼저 읽 으 세 요.
    쓰다
    1.가방 안내
    clone 이 프로젝트 는 프로젝트 디 렉 터 리 에 들 어가 mvn install 을 실행 하고 가방 을 maven 창고 에 설치 합 니 다.
        
            com.redislock
            redislock-spring-boot-starter
            1.0.0
        
    

    버 전 요구 사항:spring-boot 2.0.0 이상,jdk 1.8 이상,redis 2.6.12 이상
    2.잠 금 기능 을 실현 하 는 서비스 쓰기
    다음 과 같다.
    @Service
    public class TestService {
        @RedisSynchronized(value = "talk",fallbackMethod = "shutup")
        public String myTurn(String speak){
            //      ,             
            //             ,   sleep10s(     10s)
            try {
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return speak;
        }
        @Fallback(value = "shutup",replaceReturn = true)
        private String notYourTurn(RedisLockJoinPoint redisLockJoinPoint){
            //           
            return "silence";
        }
    }
    

    3.redis 의 key 검사
    redis 클 라 이언 트 를 설치 한 후 redis 서 비 스 를 시작 하여 redis 클 라 이언 트 에 로그 인 합 니 다.
    127.0.0.1:6379> keys *
    (empty list or set)
    127.0.0.1:6379>
    

    현재 redis 에는 키 가 없습니다.
    4.호출(잠 금 성공)
    첫 번 째 단계 에서@RedisSynchronized 레이 블 을 호출 하 는 방법
    @RunWith(SpringRunner.class)
    @SpringBootTest(classes = RedislockApplication.class)
    public class RedislockTest {
        @Autowired
        private TestService testService;
        @Test
        public void testLock() {
            System.out.println("    :" + testService.myTurn("bulaha", 0));
        }
    }
    

    방법 실행 이 끝나 기 전에 redis 를 다시 보면 redis 에 key 가 하나 더 있 습 니 다.@RedisSynchronized 의 value 값 과 같 습 니 다.현재 방법 으로 자 물 쇠 를 얻 었 음 을 의미 합 니 다.
    127.0.0.1:6379> keys *
    1) "talk"
    127.0.0.1:6379>
    

    10s 후 실행 결 과 는 다음 과 같 습 니 다.
        :bulaha
    

    방법 이 실 행 된 후에 redis 를 다시 관찰 하면'talk'이 자동 으로 제거 되 었 습 니 다.즉,자물쇠 가 풀 렸 습 니 다.
    127.0.0.1:6379> keys *
    (empty list or set)
    127.0.0.1:6379> keys *
    1) "talk"
    127.0.0.1:6379> keys *
    (empty list or set)
    127.0.0.1:6379>
    

    5.호출(잠 금 실패)
    다른 스 레 드 나 프로 세 스 가 잠 금 을 선점 하 였 을 때 현재 스 레 드 잠 금 이 실 패 했 을 때의 상황 을 모 의 합 니 다.먼저 redis 에 key"talk"을 설정 합 니 다.
    127.0.0.1:6379> set talk 1
    OK
    127.0.0.1:6379> keys *
    1) "talk"
    127.0.0.1:6379> 
    

    세 번 째 단계 에서 의 테스트 방법 을 다시 실행 합 니 다.실행 결 과 는 다음 과 같 습 니 다.
        :silence
    

    결국 자 물 쇠 를 성공 적 으로 얻 었 을 때 우리 가 원 하 는 결과 인'bulaha'를 출력 하 는 것 이 아니 라 첫 번 째 단계 에서@Fallback 방법의 반환 값 을 출력 하 는 것 을 볼 수 있 습 니 다.이것 이 바로 방법 강등 입 니 다.잠 금 에 실 패 했 을 때 대체 논 리 를 실행 하고 대체 값 을 되 돌려 줍 니 다.(강등 방법 을 지정 하지 않 아 도 됩 니 다.잠 금 실패 시 이상 을 던 집 니 다.)
    현재 세 개의 주석 만 있 으 면@RedisSynchronized,@Fallback,@Fallback Handler 세 개의 설정 항목 redislock.prefix redislock.timeout redislock.heart-beat 는 README 참조
    이루어지다
    1.redislock 어떻게 실현
    분산 잠 금 을 실현 하 는 정책 은:
    1.setnx 를 사용 하여 redis 에 키 를 설정 하고 만 료 시간(redis 는 2.6.12 버 전에 서 강 화 된 set 명령 을 지원 합 니 다.즉,SET key value[expiration EX seconds|PX milliseconds][NX|XX]입 니 다.set 명령 하나 로 setnx+expire 를 실현 할 수 있 습 니 다.우 리 는 상기 글 에서 말 한 setnx 와 expire 가 각각 실 행 될 때 발생 하 는 잠 금 문 제 를 걱정 할 필요 가 없습니다.좋 지 않 습 니까?)구체 적 으로 자바 에서 redis 에 이 명령 을 어떻게 보 냅 니까?여 기 는 spring-data-redis 를 사용 합 니 다.코드 는 다음 과 같 습 니 다.
    public class MyRedisTemplate{
        private RedisTemplate redisTemplate;
    
        public void setRedisTemplate(RedisTemplate redisTemplate) {
            this.redisTemplate = redisTemplate;
        }
    
        public Boolean setifAbsent(String key, String value, long timeoutMilis){
            Boolean execute = redisTemplate.execute(new MyRedisCallback(key,value,timeoutMilis));
            return execute;
        }
    class MyRedisCallback implements RedisCallback{
        private String key;
        private String value;
        private long timeoutMilis;
    
        public MyRedisCallback(String key, String value, long timeoutMilis) {
            this.key = key;
            this.value = value;
            this.timeoutMilis = timeoutMilis;
        }
    
        @Override
        public Boolean doInRedis(RedisConnection connection) throws DataAccessException {
            RedisStringCommands redisStringCommands = connection.stringCommands();
            //      set  ,  true      
            Boolean set = redisStringCommands.set(key.getBytes(), value.getBytes(), Expiration.milliseconds(timeoutMilis), RedisStringCommands.SetOption.SET_IF_ABSENT);
            return set;
        }
    }
        public Boolean expire(String key, long time) {
            return redisTemplate.expire(key,time,TimeUnit.MILLISECONDS);
        }
    
        public Boolean delete(String key) {
            return redisTemplate.delete(key);
        }
    }
    

    2.성공 적 으로 획득 한 자 물 쇠 를 하나의 집합 에 넣 고 Timer 를 열 어 집합 중의 자 물 쇠 를 연장 합 니 다.이 집합 은 스 레 드 간 에 공유 해 야 하기 때문에 스 레 드 안전 한 집합 을 사용 해 야 합 니 다.여 기 는 Concurrent HashMap 을 사용 합 니 다.원래 Set 를 사용 하려 고 했 는데 자바 의 concurrent 패키지 에 제공 되 지 않 았 으 니 Concurrent HashMap 으로 대체 합 니 다.사실 자바 의 Set 은 Map 으로 이 루어 집 니 다)
    3.잠 금 을 풀 때 키 를'속 명 집합'에서 제거 한 다음 redis 에서 키 를 제거 합 니 다.여기 서 순 서 를 바 꿀 수 없습니다.A 스 레 드 가 먼저 redis 에서 key 를 제거 하면 바로 또 하나의 B 스 레 드 가 자 물 쇠 를 얻 고 key 를'속 명 집합'에 추가 한 후에 A 스 레 드 가 이 key 를'속 명 집합'에서 제거 할 수 있 기 때 문 입 니 다.그러면 스 레 드 B 가 얻 은 자 물 쇠 는 속 명 기능 이 없습니다.만약 이 스 레 드 가 하필 잠 금 이 만 료 되 어도 실행 되 지 않 았 다 면 잠 겨 있 는 자원 을 병행 하 는 상황 이 발생 했 을 것 이다.
    public class RedisLock {
        private long timeout = LOCK_TIMEOUT;
        private long heartBeat = LOCK_HEART_BEAT;
        
        private MyRedisTemplate myRedisTemplate;
    
        public void setMyRedisTemplate(MyRedisTemplate myRedisTemplate) {
            this.myRedisTemplate = myRedisTemplate;
        }
    
        private Map aliveLocks = new ConcurrentHashMap();
    
        private final Timer timer = new Timer();
    
    	// bean         timer, timer       
        @PostConstruct
        public void init(){
            timer.schedule(new TimerTask() {
                @Override
                public void run() {
                    keeplockAlive();
                }
            },0,heartBeat);
        }
        //      timer  
        @PreDestroy
        private void destroy(){
            timer.cancel();
        }
        private void keeplockAlive(){
            if(aliveLocks.size() > 0){
                for (String key:
                        aliveLocks.keySet()) {
                    myRedisTemplate.expire(key,timeout);
                }
            }
        }
    
        public void unlock(String key) {
            aliveLocks.remove(key);
            myRedisTemplate.delete(key);
        }
    
        public boolean lock(String key){
            return lock(key,true);
        }
    
        public boolean lock(String key,boolean keepAlive){
            Boolean redisLock = myRedisTemplate.setifAbsent(key, "redisLock", timeout);
            if(redisLock && keepAlive){
                aliveLocks.put(key,"");
            }
            return redisLock;
        }
    }
    

    2.어떻게 주해 화 를 실현 합 니까
    현재 우 리 는 이전에 말 한 RedisLock 류 를 사용 하여 잠 금 해 제 를 실현 할 수 있 습 니 다.사실은 분포 식 잠 금 의 기본 기능 이 이미 실현 되 었 습 니 다.당신 은 다음 과 같이 사용 하여 분포 식 잠 금 을 실현 할 수 있 습 니 다.
    redislock.lock("mykey");
    //        
    redislock.unlock("mykey");
    

    그러나 이렇게 사용 하 는 것 은 편리 하지 도 않 고 안전 하지 도 않 습 니 다.RedisLock 인 스 턴 스 를 주입 하여 스스로 추가 작업 을 해 야 합 니 다.우 리 는 Spring 의 방법 을 본 떠 서 자 물 쇠 를 넣 으 려 는 방법 을 표시 하 는 데 사용 할 주 해 를 설명 할 수 있 습 니 다.
    @Documented
    @Target({ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    public @interface RedisSynchronized {
        String value() default "";
        String fallbackMethod() default "";
    }
    

    지금 우 리 는 자신의 주해 가 생 겼 으 니,너 는 마음대로 어떤 방법 에 그들 을 표시 할 수 있다.하지만 표 시 된 방법 을 몇 번 을 실행 하 더 라 도 원 하 는 대로 자 물 쇠 를 채 우지 않 는 다 는 것 은 당연한 일이 다.우 리 는 비록 자신의 주 해 를 가지 고 있 지만,우 리 는 진정 으로 자 물 쇠 를 넣 는 기능 을 우리 에 게 표시 하 는 방법 을 부여 하지 않 았 기 때문이다.그러면 우 리 는 어떻게 해야만 spring 과 다른 프레임 워 크 처럼 사용 할 때 주석 만 표시 하면 해당 하 는 기능 을 부여 할 수 있 습 니까?바로 동적 대리 입 니 다.이렇게 말 하면 Spring 생태 를 실현 하 는 두 가지 이기 라 고 할 수 있 는데 하 나 는 주해 이 고 다른 하 나 는 동적 대리 이다.빛 나 는 외모 에 대한 주 해 는 우리 에 게 우호 적 인 체험 을 제공한다.동적 대 리 는 배후 에 숨 어 전체 메커니즘 의 운행 을 지탱 한다.동적 에이전트 소개:spirng 의 동적 에이전트 사용 은 주로 두 가지 가 있 는데 하 나 는 jdk 동적 에이전트 이 고 하 나 는 cglib 동적 에이전트 입 니 다.그 중에서 jdk 동적 대 리 는 대리 류 에 의 해 반드시 인 터 페 이 스 를 계승 해 야 한다 고 요구 하지만 cglib 는 이러한 제한 이 없 는 것 도 cglib 가 성행 할 수 있 는 원인 중 하나 이다.그리고 cglib 도 여러 가지 버 전 으로 나 뉘 는데 일반적인 cglib 는 asm 와 같은 추가 의존 이 필요 합 니 다.그리고 cglib-nodp 는 말 그대로 no dependence 로 실제 사용 할 때 의존 하 는 가방 이 호 환 되 지 않 는 문 제 를 피 할 수 있 습 니 다.그리고 우리 가 사용 하 는 것 은 상기 두 가지 중에서 우리 가 사용 하 는 것 은 spring-core 가방 에 있 는 cglib 입 니 다.Spring 은 아주 좋 은 기능 을 추 가 했 습 니 다.다음은 제 가 소개 하 겠 습 니 다.
    3.에이전트 생 성
    우리 의 최종 목적 을 달성 하려 면 다음 과 같은 세 가지 일 을 해 야 합 니 다.1.대 리 를 하고 방법 에@RedisSynchronized 주 해 를 추가 한 임 의 업무 bean 은 잠 금 해제 기능 을 가 진 대 리 를 생 성 합 니 다.2.교체,@Autoware 를 사용 하여 bean 을 주입 할 때 우리 가 대리 한 bean 을 주입 해 야 합 니 다.3.타 이 밍,프 록 시 타 이 밍 을 선택 하 십시오.즉,저희 업무 코드 가 실행 되 기 전에 Spring 초기 화가 완료 되 기 전에 프 록 시 를 완성 하고 교체 해 야 합 니 다.상기 세 가지 요점 을 결합 하여 우 리 는 cglib 를 결합 하여 Spring 의 한 구성 요 소 를 도입 하면 모두 만족 할 수 있 습 니 다.그 가 바로 BeanPost Processor 입 니 다.BeanPostProcessor 소개:BeanPostProcessor 의 설명 을 참고 하여 용기 가 작 동 할 때 BeanPostProcessor 의 실현 류 를 자동 으로 감지 하여 미리 예화 시 키 고 그 다음 인 스 턴 스 의 bean 예화 시 인터페이스 방법 을 바 꿀 수 있 음 을 알 수 있 습 니 다.인터페이스 방법 postprocessBeforeinitialization 은 속성 설정 이 완료 되면 초기 화 방법(init-method)이 실행 되 기 전에 리 셋 됩 니 다.인터페이스 방법 postprocessAfter Initialization 초기 화 방법(init-method)이 실 행 된 후 되 돌 아 옵 니 다.리 셋 방법의 리 턴 값 은 원래 의 bean 일 수도 있 고 대 리 된 bean 일 수도 있 습 니 다(either the original or a wrapped one).
    프로필 에서 우 리 는 BeanPostProcessor 를 계승 하면 시기 가 있 기 때문에 우 리 는 리 셋 방법 에서 대 리 를 할 수 있 고 대리 후에 생 성 된 대 리 를 직접 리 턴 값 으로 되 돌려 주 고 교체 하면 된다 는 것 을 이해 했다.실현
    public class RedisLockAutoProxyCreator implements BeanPostProcessor{
     
        private RedisLock redisLock;
    
        public void setRedisLock(RedisLock redisLock) {
            this.redisLock = redisLock;
        }
        @Override
        public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
            Class> aClass =  AopUtils.getTargetClass(bean);
            for (Method method : aClass.getDeclaredMethods()) {
             //        RedisSynchronized   cglib  ,     bean
                if(method.isAnnotationPresent(RedisSynchronized.class)){
                    Enhancer enhancer = new Enhancer();
                    enhancer.setSuperclass(aClass);
                    enhancer.setCallback(new MyHandler(bean));
                    return enhancer.create();
                }
            }
            //        RedisSynchronized    bean
            return bean;
        }
        class MyHandler implements InvocationHandler {
            private Object o;
            MyHandler(Object o){
                this.o=o;
            }
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            //     RedisSynchronized            
                if(!method.isAnnotationPresent(RedisSynchronized.class)){
                    return method.invoke(o, args);
                }
                //   RedisSynchronized    ,     
                String key = getKey(o,method,args);
                boolean locked = redisLock.lock(key);
                if(locked){
                    try{
                        return method.invoke(o, args);
                    }finally {
                        redisLock.unlock(key);
                    }
                }
                //      
                throw new LockFailedException("lock failed");
            }
    
            private String getKey(Object o, Method method, Object[] args) {
                RedisSynchronized annotation = method.getAnnotation(RedisSynchronized.class);
                String key = annotation.value();
                if("".equals(key)){
                    key = method.toGenericString();
                }
                return key;
            }
        }
    }
    

    프 록 시 과정 을 더욱 명확 하 게 표현 하기 위해 이 코드 는 약간 복잡 한 잠 금 강등 부분 을 버 렸 습 니 다.이상 이 모두 이해 했다 면 clone 코드 는 잠 금 강등 의 실현 을 볼 수 있 습 니 다.
    여기 서 우 리 는 주해 화 된 redis 자 물 쇠 를 완성 했다.
    통합 spring-boot-starter
    편폭 문제 로 여기까지 말씀 드 리 겠 습 니 다.통합 spring-boot-starter 에 관심 이 있 으 면 메 시 지 를 남 겨 주세요.저 는 따로 한 편 을 써 서 설명 하 겠 습 니 다.이 글 은 이미 다 써 서 전송 문 이다.

    좋은 웹페이지 즐겨찾기