Redis分布式锁机制

Redis分布式锁机制

【基本机制】:

基于redis实现的Java分布式锁主要依赖redis的SETNX()命令和DEL()命令,SETNX相当于上锁(lock),DEL相当于释放锁(unlock)。我们只要实现Lock接口重写lock()和unlock()即可。但是这还不够,安全可靠的分布式锁应该满足满足下面三个条件:

 

  • l 互斥,不管任何时候,只有一个客户端能持有同一个锁。
  • l 不会死锁,最终一定会得到锁,即使持有锁的客户端对应的master节点宕掉。
  • l 容错,只要大多数Redis节点正常工作,客户端应该都能获取和释放锁。

【问题】:

什么情况下回不满足上面三个条件呢。多个线程(客户端)同时竞争锁可能会导致多个客户端同时拥有锁。比如,

(1)线程1在master节点拿到了锁(存入key)

(2)master节点在把线程1创建的key写入slave之前宕机了,此时集群中的节点已经没有锁(key)了,包括master节点的slaver节点

(3)slaver节点升级为master节点

(4)线程2向新的master节点发起锁(存入key)请求,很明显,能请求成功。

可见,线程1和线程2同时获得了锁。如果在更高并发的情况,可能会有更多线程(客户端)获取锁

  • 【死锁】: 

拥有锁的线程(客户端)长时间的执行或者因为某种原因造成阻塞,就会导致锁无法释放(unlock没有调用),其它线程就不能获取锁而而产生无限期死锁的情况。其它线程在执行lock失败后即使粗暴的执行unlock删除key之后也不能正常释放锁,因为锁就只能由获得锁的线程释放,锁不能正常释放其它线程仍然获取不到锁。

设置锁的有效时间(redis的expire命令),不管是什么原因导致的死锁,有效时间过后,锁将会被自动释放 

  • 【解决方法】: 
  •  【容错】:(防止阻塞)

 只要有Redis节点正常工作,客户端应该都能获取和释放锁,我们必须用相同的key不断循环向Master节点请求锁,当请求时间超过设定的超时时间则放弃请求锁,这个可以防止一个客户端在某个宕掉的master节点上阻塞过长时间,如果一个master节点不可用了,应该尽快尝试下一个master节点。释放锁比较简单,因为只需要在所有节点都释放锁就行,不管之前有没有在该节点获取锁成功

 【RedLock算法流程】:

Redis分布式锁机制_第1张图片

【Resis分布式锁sping配置依赖】:

 




  4.0.0

  com.caox
  spring-demo
  1.0-SNAPSHOT
  war

  spring-demo Maven Webapp
  
  http://www.example.com

  
    UTF-8
    1.7
    1.7
    UTF-8
    
    4.1.1.RELEASE
    
    3.2.6
    
    1.7.7
    1.2.17
    
    2.5.0
  

  
    
      junit
      junit
      4.11
      test
    

    
    
      org.springframework
      spring-test
      ${spring.version}
      test
    

    
    
      org.springframework
      spring-webmvc
      ${spring.version}
    

    
    
      org.springframework
      spring-core
      ${spring.version}
    

    
      org.springframework
      spring-beans
      ${spring.version}
    

    
      org.springframework
      spring-context
      ${spring.version}
    

    
      org.springframework
      spring-context-support
      ${spring.version}
    

    
      org.springframework
      spring-aop
      ${spring.version}
    

    
      org.springframework
      spring-aspects
      4.0.9.RELEASE
      
        
          
          
        
      
    

    
      org.springframework
      spring-tx
      ${spring.version}
    

    
      org.springframework
      spring-web
      ${spring.version}
    

    
      org.springframework
      spring-jdbc
      ${spring.version}
    

    
    
      org.aspectj
      aspectjrt
      1.7.4
    
    
      org.aspectj
      aspectjweaver
      1.7.4
    
    
      cglib
      cglib
      3.1
    
    

    
    
      mysql
      mysql-connector-java
      5.1.34
    
    
    
      log4j
      log4j
      ${log4j.version}
    

    
      org.slf4j
      slf4j-api
      ${slf4j.version}
    

    
      org.slf4j
      slf4j-log4j12
      ${slf4j.version}
    
    
    
    
      javax.servlet
      javax.servlet-api
      3.0.1
      provided
    

    
      org.apache.commons
      commons-dbcp2
      2.0
    

    
    
      org.springframework.data
      spring-data-redis
      1.4.2.RELEASE
      
        
          
          
        
        
          
          
        
        
          
          
        
        
          
          
        
        
          
          
        
        
          
          
        
      
    

    
      redis.clients
      jedis
      2.6.2
      
        
          
          
        
      
    

    
      org.apache.commons
      commons-pool2
      2.4.2
    
    

    
      org.projectlombok
      lombok
      1.14.4
    
  

  
    spring-demo
    
      
        org.apache.maven.plugins
        maven-compiler-plugin
        
          1.7
          1.7
        
      
    

    
    
      
        src/main/java
        
          *.properties
          *.xml
        
        true
      
      
        src/main/java/resources
        
          *.properties
          *.xml
        
        true
      
    

    
  

【RedisLock Java 实现】:

 

package com.caox.redis;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.data.redis.core.StringRedisTemplate;

import java.util.Random;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;

/**
 * @author : nazi
 * @version : 1.0
 * @date : 2018/12/29 14:22
 */
public class RedisLock implements Lock{
    protected StringRedisTemplate redisStringTemplate;

    // 存储到redis中的锁标志
    private static final String LOCKED = "LOCKED";

    // 请求锁的超时时间(ms)
    private static final long TIME_OUT = 30000;

    // 锁的有效时间(s)
    public static final int EXPIRE = 60;

    // 锁标志对应的key;
    private String key;

    // state flag
    private volatile boolean isLocked = false;

    public RedisLock(String key) {
        this.key = key;
        @SuppressWarnings("resource")
        ApplicationContext ctx =  new ClassPathXmlApplicationContext("classpath*:application-context.xml");
        redisStringTemplate = (StringRedisTemplate)ctx.getBean("redisStringTemplate");
    }

    @Override
    public void lock() {
        //系统当前时间,毫秒
        long nowTime = System.nanoTime();
        //请求锁超时时间,毫秒
        long timeout = TIME_OUT*1000000;
        final Random r = new Random();
        try {
            //不断循环向Master节点请求锁,当请求时间(System.nanoTime() - nano)超过设定的超时时间则放弃请求锁
            //这个可以防止一个客户端在某个宕掉的master节点上阻塞过长时间
            //如果一个master节点不可用了,应该尽快尝试下一个master节点
            while ((System.nanoTime() - nowTime) < timeout) {
                //将锁作为key存储到redis缓存中,存储成功则获得锁
                if (redisStringTemplate.getConnectionFactory().getConnection().setNX(key.getBytes(),
                        LOCKED.getBytes())) {
                    //设置锁的有效期,也是锁的自动释放时间,也是一个客户端在其他客户端能抢占锁之前可以执行任务的时间
                    //可以防止因异常情况无法释放锁而造成死锁情况的发生
                    redisStringTemplate.expire(key, EXPIRE, TimeUnit.SECONDS);
                    isLocked = true;
                    //上锁成功结束请求
                    break;
                }
                //获取锁失败时,应该在随机延时后进行重试,避免不同客户端同时重试导致谁都无法拿到锁的情况出现
                //睡眠3毫秒后继续请求锁
                Thread.sleep(3, r.nextInt(500));
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    public void unlock() {
        //释放锁
        //不管请求锁是否成功,只要已经上锁,客户端都会进行释放锁的操作
        if (isLocked) {
            redisStringTemplate.delete(key);
        }
    }

    @Override
    public void lockInterruptibly() throws InterruptedException {
        // TODO Auto-generated method stub

    }

    @Override
    public boolean tryLock() {
        // TODO Auto-generated method stub
        return false;
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        // TODO Auto-generated method stub
        return false;
    }

    @Override
    public Condition newCondition() {
        // TODO Auto-generated method stub
        return null;
    }
}

【参考文献】: 分布式缓存技术redis系列(五)——redis实战(redis与spring整合,分布式锁实现)

【springboot + redis分布式 + aop】:

package com.caox.aop;

import com.caox.annotions.RedisSync;
import com.caox.service.IRedisService;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.lang.reflect.Method;
import java.util.Objects;


/**
 * @author : nazi
 * @version : 1.0
 * @date : 2019/1/3 10:53
 */
@Slf4j
@Aspect
@Component
public class RedisSyncAop {
    private static final Logger logger = LoggerFactory.getLogger(RedisSyncAop.class);
    @Resource
    private IRedisService iRedisService;

    @Pointcut("@annotation(com.caox.annotions.RedisSync)")
    private void anyMethod(){
    }

    @Around("anyMethod()")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        Object result = null;
        //获得锁
        Method method = ((MethodSignature)pjp.getSignature()).getMethod();
        String key = method.toString();
        RedisSync redisSync = method.getAnnotation(RedisSync.class);
        long waitTime = redisSync.waitTime();
        long currTime = System.currentTimeMillis();
        Boolean state = iRedisService.setNx(key, currTime);
        long saveTime = 0L;
        while (!state) {
            // 之前存在key 并发开始
            Long tempSaveTime = iRedisService.get(key, Long.class);
            // 若锁被释放
            if (tempSaveTime == null) {
                // 重新加锁
                state = iRedisService.setNx(key, currTime);
                continue;
            }
            // 锁被重新获取
            if (!tempSaveTime.equals(saveTime)) {
                currTime = System.currentTimeMillis();
                saveTime = tempSaveTime;
            }
            // 判断是否超时
            if (saveTime + redisSync.timeout() < currTime) {
                // 超时,直接获得锁  获取上一个锁的时间value
                Object tempTime = iRedisService.getSet(key, currTime);
                if(tempTime == null){
                    state = iRedisService.setNx(key, currTime);
                    continue;
                }
                // 判断锁是否被释放 或 未被抢先获取  saveTime = tempSaveTime; tempTime(获取上一个锁时间value)
                if (Objects.equals(saveTime, tempTime)) {
                    logger.warn("方法:{},执行超时,已被强制解锁!", key);
                    break;
                }
            }
            // 等待
            if(waitTime > 0) {
                try {
                    Thread.sleep(waitTime);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            state = iRedisService.setNx(key, currTime);
        }
        // 执行方法
        result = pjp.proceed();
        Long currSaveTime = iRedisService.get(key, Long.class);
        // 判断锁未被判定为超时
        if (currSaveTime != null && Objects.equals(currSaveTime, currTime)) {
            // 释放锁
            iRedisService.del(key);
        }
        return result;
    }
}

【参考文献】:SpringBoot+Redis 从HelloWorld到分布式锁

 

 

 

你可能感兴趣的:(Redis)