君临-行者无界

导航

java分布式锁的实现及在springboot中的应用

redis

分布式实现原理

SET resource_name my_random_value NX PX 30000
  • NX:表示只有 key 不存在的时候才会设置成功。(如果此时 redis 中存在这个 key,那么设置失败,返回 nil
  • PX 30000:意思是 30s 后锁自动释放。别人创建的时候如果发现已经有了就不能加锁了。
  • random_value 随机值,判断 value 一样才删除

jedis实现

	<dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>2.9.2</version>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
            <version>2.5.0</version>
        </dependency>
public class JedisUtil {

    private static JedisPool jedisPool;
    static {
        JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
        jedisPoolConfig.setMaxTotal(10000);
        jedisPoolConfig.setMaxWaitMillis(10000);
        jedisPoolConfig.setMaxIdle(1000);
        jedisPool = new JedisPool(jedisPoolConfig,"127.0.0.1",6379,100000);

    }

    public static Jedis getRerouse(){
        return  jedisPool.getResource();
    }


}
public class JedisDistributeLock {

    private static final Logger logger = LoggerFactory.getLogger(JedisDistributeLock.class);

    private static final String SUCCESS ="OK";
    private static final String SET_IF_NOT_EXIST ="NX";
    private static final String SET_WITH_EXPIRE_TIME ="EX";

    //释放锁成功标示
    private static final Long RELEASE_SUCCESS = 1L;

    //获取锁时的睡眠等待时间片,单位毫秒
    private static final long SLEEP_PER = 5;

    //默认过期时间
    public static final int DEFAULT_EXPIRE_1000_Milliseconds = 1000;

    private ThreadLocal<String> threadLocal = new ThreadLocal<>();

    private void setThreadLocal(String uuid){
        threadLocal.set(uuid);
    }

    private String getThreadLocal(){
        return threadLocal.get();
    }


    public void acquire(String key){
        String value = UUID.randomUUID().toString();
        lock(key,value,5000);
        setThreadLocal(value);
    }


    public void release(String key){
        String value = getThreadLocal();
        unlock(key,value);
    }


    private  Boolean tryGetLock(Jedis jedis,String key,String requestId,int expireTime){

        String result = jedis.set(key,requestId,SET_IF_NOT_EXIST,SET_WITH_EXPIRE_TIME,expireTime);
        if(SUCCESS.equals(result)){
            return true;
        }
        return false;
    }


    private  Boolean releaseLock(Jedis jedis,String key,String requestId){
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        Object result = jedis.eval(script, Collections.singletonList(key),Collections.singletonList(requestId));
        if(RELEASE_SUCCESS.equals(result)){
            return true;
        }
        return false;
    }

    private  void lock(String key,String value,int expireTime){
        try (Jedis jedis = JedisUtil.getRerouse()){
            while(!tryLock(key,value,expireTime)){
                //缺点,睡眠期间其它线程释放锁不能及时收到
                try {
                    Thread.sleep(SLEEP_PER);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    private  boolean tryLock(String key,String value,int expireTime){
        try (Jedis jedis = JedisUtil.getRerouse()){
            return  tryGetLock(jedis,key,value,expireTime);
        }
    }

    private   void unlock(String key,String value){
        try (Jedis jedis = JedisUtil.getRerouse()){
            releaseLock(jedis,key,value);
        }
    }

    public static void main(String[] args) {
        JedisDistributeLock lock = new JedisDistributeLock();
        try {
            lock.acquire("order");
            logger.info("01获取锁");
            Thread thread = new Thread(new Runnable() {
                public void run() {
                    try {
                        lock.acquire("order");
                        logger.info("02获取锁");
                        lock.release("order");
                        logger.info("02释放锁");
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            });
            thread.start();
            Thread.sleep(2000);
            lock.release("order");
            logger.info("01释放锁");
        }catch (Exception e){
            e.printStackTrace();
        }



    }
}

lettuce结合springboot实现

springboot2.x后redis客户端默认使用lettuce

 	<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
@Configuration
@EnableCaching
public class RedisConfig {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory lettuceConnectionFactory) {
        //设置序列化
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        // 配置redisTemplate
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<String, Object>();
        redisTemplate.setConnectionFactory(lettuceConnectionFactory);
        RedisSerializer stringSerializer = new StringRedisSerializer();
        redisTemplate.setKeySerializer(stringSerializer); // key序列化
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer); // value序列化
        redisTemplate.setHashKeySerializer(stringSerializer); // Hash key序列化
        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer); // Hash value序列化
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }

}

@Component
public class SpringRedisLock {

    private static final Logger logger = LoggerFactory.getLogger(SpringRedisLock.class);

    private static final RedisScript<Long> lua_lock = new DefaultRedisScript<>("if redis.call(\"setnx\", KEYS[1], KEYS[2]) == 1 then return redis.call(\"pexpire\", KEYS[1], KEYS[3]) else return 0 end", Long.class);

    private static final RedisScript<Long> lua_unlock = new DefaultRedisScript<>("if redis.call(\"get\",KEYS[1]) == KEYS[2] then return redis.call(\"del\",KEYS[1]) else return -1 end", Long.class);

    @Autowired
    private RedisTemplate redisTemplate;
    Integer timeOut =1;
    ThreadLocal<String> threadLocal = new ThreadLocal<>();
    // 多台机器的情况下,会出现大量的等待,加重redis的压力。 在lock方法上,加入同步关键字。单机同步,多机用redis
    Lock lock = new ReentrantLock();

    public void lock(String key ) {
        try {
            lock.lock();
            while(!tryLock(key)){

                redisTemplate.execute(new RedisCallback<Boolean>() {
                    @Override
                    public Boolean doInRedis(RedisConnection connection) throws DataAccessException {
                        CountDownLatch waiter = new CountDownLatch(1);
                        // 等待通知结果,使用jedis在此处会阻塞
                        connection.subscribe((message, pattern) -> {
                            // 收到通知,不管结果,立刻再次抢锁
                            waiter.countDown();
                        }, (key + "_unlock_channel").getBytes());
                        try {
                            // 等待一段时间,超过这个时间都没收到消息,肯定有问题
                            waiter.await(timeOut, TimeUnit.SECONDS);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        return true; //随便返回一个值都没问题
                    }
                });
                logger.info("继续下一次循环");
            }
        }  finally {
            lock.unlock();
        }



    }


    public boolean tryLock(String key ) {
        String value = UUID.randomUUID().toString();
        List<String> keys = Arrays.asList(key, value, String.valueOf(1000));
        Long result = (Long) redisTemplate.execute(lua_lock,keys);
        if(result==1){
            logger.info(Thread.currentThread().getName() + "获取到锁");
            threadLocal.set(value);
            return true;
        }else{
            return false;
        }
    }


    public void unlock(String key) {
        //1、 要比对内部的值,同一个线程,才能够去释放锁。 2、 同时发出通知
        String value = threadLocal.get();
        try {
            List<String> keys = Arrays.asList(key, value);
            Long result = (Long) redisTemplate.execute(lua_unlock,keys);
            if(result !=-1){
                logger.info(Thread.currentThread().getName() + "释放锁");
                redisTemplate.execute(new RedisCallback() {
                    @Override
                    public Object doInRedis(RedisConnection redisConnection) throws DataAccessException {
                        redisConnection.publish((key + "_unlock_channel").getBytes(),"".getBytes());
                        return null;
                    }
                });
            }
        }finally {
            threadLocal.remove();
        }
    }

}

测试类

@Service
public class TicketService {

    @Autowired
    private SpringRedisLock springRedisLock;

    private Long tickets = 0l;

    public void buyTicket(String userId) {
        try {
            springRedisLock.lock("ticket");
            System.out.println(userId + "正在买第 " + ++tickets + " 张票");

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            springRedisLock.unlock("ticket");
        }
    }
}
@Test
    public void testRe(){
        int currency = 200;
        CyclicBarrier cyclicBarrier = new CyclicBarrier(currency);
        for (int i = 0; i <currency ; i++) {
            String userId = "Tony" +i;
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName()+"线程准备好");
                try {
                    cyclicBarrier.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
                ticketService.buyTicket(userId);
            }).start();
        }
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

redission实现

实现原理如下


代码

	<dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.9.1</version>
        </dependency>
public class RedissionLock {

    private static final Logger logger = LoggerFactory.getLogger(RedissionLock.class);

    private RLock rLock;

    private RedissionLock(String key) {
        rLock = getRlock(key);
    }

    private RLock getRlock(String key) {
        Config config = new Config();
        config.useSingleServer().setAddress("redis://127.0.0.1:6379");
        RedissonClient client = Redisson.create(config);
        RLock rLock = client.getLock("order");
        return rLock;
    }

    public void acquire() {
        rLock.lock();
    }

    public void release() {
        rLock.unlock();
    }

    public static void main(String[] args) {
        RedissionLock lock = new RedissionLock("order");
        try {
            lock.acquire();
            logger.info("01获取锁");
            Thread thread = new Thread(new Runnable() {
                public void run() {
                    try {
                        lock.acquire();
                        logger.info("02获取锁");
                        lock.release();
                        logger.info("02释放锁");
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            });
            thread.start();
            Thread.sleep(2000);
            lock.release();
            logger.info("01释放锁");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}

redlock

分布式架构中的CAP理论,分布式系统只能同时满足两个: 一致性(Consistency)、可用性(Availability)、分区容错性(Partition tolerance)。

上述redis分布式锁是AP模式,当锁存在的redis节点宕机,可能会被误判为锁失效,或者没有加锁。具体如下:

  1. A客户端请求主节点获取到了锁
  2. 主节点挂掉了,但是还没把锁的信息同步给其他从节点
  3. 由于主节点挂了,这时候开始主从切换,从节点成为主节点继续工作,但是新的主节点上,没有A客户端的加锁信息
  4. 这时候B客户端来加锁,因为目前是一个新的主节点,上面没有其他客户端加锁信息,所以B客户端获取锁成功
  5. 这时候就存在问题了,A和B两个客户端同时都持有锁,同时在执行代码,那么这时候分布式锁就失效了。

在Redis的分布式环境中,我们假设有N个Redis master。这些节点完全互相独立,不存在主从复制或者其他集群协调机制。我们确保将在N个实例上使用与在Redis单实例下相同方法获取和释放锁。现在我们假设有5个Redis master节点,同时我们需要在5台服务器上面运行这些Redis实例,这样保证他们不会同时都宕掉。

RedLock算法思想,意思是不能只在一个redis实例上创建锁,应该是在多个redis实例上创建锁,n / 2 + 1,必须在大多数redis节点上都成功创建锁,才能算这个整体的RedLock加锁成功,避免说仅仅在一个redis实例上加锁而带来的问题。

redisson已经有对redlock算法封装。具体使用如下:

Config config = new Config();
config.useSentinelServers().addSentinelAddress("127.0.0.1:6369","127.0.0.1:6379", "127.0.0.1:6389")
		.setMasterName("masterName")
		.setPassword("password").setDatabase(0);
RedissonClient redissonClient = Redisson.create(config);
RLock redLock = redissonClient.getLock("REDLOCK_KEY");
boolean isLock;
try {
	isLock = redLock.tryLock(500, 10000, TimeUnit.MILLISECONDS);
	if (isLock) {
		//TODO if get lock success, do something;
	}
} catch (Exception e) {
} finally {
	redLock.unlock();
}

redlock可能失效的场景

  • 某个节点没有持久化导致锁失效,A、B、C、D、E,user01 锁住A、B、C,而后C宕机重启,user02锁住C、D、E,可以使用延时重启(延时时间要结合业务锁失效时间)
  • 某个节点发生时钟跳跃导致锁失效
  • 在锁释放前已经过了失效时间(单机也有这种问题),比如GC、操作系统cpu上下文切换、网络延时

zookeeper

临时节点

多个客户端去创建相同的临时节点,创建成功则表示拿到了锁

	<dependency>
            <groupId>com.101tec</groupId>
            <artifactId>zkclient</artifactId>
            <version>0.10</version>
        </dependency>
public class ZkLock {

    private static final Logger logger = LoggerFactory.getLogger(ZkLock.class);

    private ZkClient zkClient;

    private String lockPath;

    private final static String ROOT_PATH = "/lock/";

    private  ZkLock(String lockPath){
        zkClient = new ZkClient("127.0.0.1:2181");
        zkClient.setZkSerializer(new MyZkSerializer());
        this.lockPath = ROOT_PATH + lockPath;
    }


    public void lock() {
        if(!tryLock()){
            waitForLock();
            lock();
        }
    }

    public void unlock() {
        zkClient.delete(lockPath);

    }

    public boolean tryLock() {
        try {
            zkClient.createEphemeral(lockPath);
        } catch (ZkNodeExistsException e) {
            return false;
        }
        return true;
    }

    private void waitForLock(){
        CountDownLatch countDownLatch = new CountDownLatch(1);
        IZkDataListener listener = new IZkDataListener() {
            @Override
            public void handleDataChange(String s, Object o) throws Exception {
            }
            @Override
            public void handleDataDeleted(String s) throws Exception {
               logger.info(s+ "节点被删除了");
               countDownLatch.countDown();
            }
        };
        zkClient.subscribeDataChanges(lockPath,listener);
        if(zkClient.exists(lockPath)){
            try {
                countDownLatch.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        zkClient.unsubscribeDataChanges(lockPath,listener);
    }


    public static void main(String[] args) {

        ZkLock zkLock01 = new ZkLock("order");
        ZkLock zkLock02 = new ZkLock("order");
        try {
            zkLock01.lock();
            logger.info("zkLock01获取锁");
            Thread thread = new Thread(new Runnable() {

                public void run() {
                    try {
                        zkLock02.lock();
                        logger.info("zkLock02第一次获取锁");
                        zkLock02.unlock();
                        logger.info("zkLock02第一次释放锁");
                        zkLock02.lock();
                        logger.info("zkLock02第二次获取锁");
                        zkLock02.unlock();
                        logger.info("zkLock02第二次释放锁");
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            });
            thread.start();
            Thread.sleep(2000);
            zkLock01.unlock();
            logger.info("zkLock01释放锁");
        }catch (Exception e){
            e.printStackTrace();
        }


    }
}

临时顺序节点

多个客户端在一个节点下创建临时顺序节点,按照节点顺序依次获取锁,避免了惊群效应,提高了性能

public class ZkLock02 {

    private static final Logger logger = LoggerFactory.getLogger(ZkLock02.class);
    private String lockPath;

    private ZkClient zkClient;

    private String beforePath;

    private String currentPath;

    private final static String ROOT_PATH = "/lock/";


    public ZkLock02(String path){
        this.lockPath = ROOT_PATH +path;
        zkClient = new ZkClient("127.0.0.1:2181");
        zkClient.setZkSerializer(new MyZkSerializer());
        if(!zkClient.exists(lockPath)){
            try {
                zkClient.createPersistent(lockPath);
            } catch (RuntimeException e) {
                e.printStackTrace();
            }
        }
    }

    public void unlock() {
        zkClient.delete(currentPath);

    }

    public boolean tryLock() {
        if(currentPath==null){
            currentPath = zkClient.createEphemeralSequential(lockPath+"/","asd");
        }
        List<String> childs = zkClient.getChildren(lockPath).stream().sorted((a, b)->a.compareTo(b)).collect(Collectors.toList());
        if(currentPath.equals(lockPath +"/" + childs.get(0))){
            return true;
        }else{
            int curIndex = childs.indexOf(currentPath.substring(lockPath.length()+1));
            beforePath = lockPath +"/" + childs.get(curIndex-1);
        }
        return false;
    }

    public void lock() {
        if(!tryLock()){
            waitForLock();
            lock();
        }
    }

    private void waitForLock(){
        CountDownLatch countDownLatch = new CountDownLatch(1);
        IZkDataListener listener = new IZkDataListener() {
            @Override
            public void handleDataChange(String s, Object o) throws Exception {
            }
            @Override
            public void handleDataDeleted(String s) throws Exception {
                logger.info(s+ "节点被删除了");
                countDownLatch.countDown();
            }
        };
        zkClient.subscribeDataChanges(beforePath,listener);
        if(zkClient.exists(beforePath)){
            try {
                countDownLatch.await();

            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        zkClient.unsubscribeDataChanges(beforePath,listener);

    }

    public static void main(String[] args) {

        ZkLock02 zkLock01 = new ZkLock02("order");
        ZkLock02 zkLock02 = new ZkLock02("order");
        ZkLock02 zkLock03 = new ZkLock02("order");
        try {
            zkLock01.lock();
            logger.info("zkLock01获取锁");
            Thread thread = new Thread(new Runnable() {

                public void run() {
                    try {
                        zkLock02.lock();
                        logger.info("zkLock02第一次获取锁");
                        zkLock02.unlock();
                        logger.info("zkLock02第一次释放锁");
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            });
            thread.start();
            Thread thread02 = new Thread(new Runnable() {

                public void run() {
                    try {
                        zkLock03.lock();
                        logger.info("zkLock03第一次获取锁");
                        zkLock03.unlock();
                        logger.info("zkLock03第一次释放锁");
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            });
            thread02.start();
            Thread.sleep(2000);
            zkLock01.unlock();
            logger.info("zkLock01释放锁");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public void close(){
        zkClient.close();
    }

}

curator实现及与springboot集成

	<dependency>
            <groupId>org.apache.curator</groupId>
            <artifactId>curator-framework</artifactId>
            <version>2.8.0</version>
        </dependency>
        <dependency>
            <groupId>org.apache.curator</groupId>
            <artifactId>curator-recipes</artifactId>
            <version>2.8.0</version>
        </dependency>
@Configuration
public class CuratorConfiguration {

    @Value("${curator.retryCount}")
    private int retryCount;

    @Value("${curator.elapsedTimeMs}")
    private int elapsedTimeMs;

    @Value("${curator.connectString}")
    private String connectString;

    @Value("${curator.sessionTimeoutMs}")
    private int sessionTimeoutMs;

    @Value("${curator.connectionTimeoutMs}")
    private int connectionTimeoutMs;

    @Bean(initMethod = "start")
    public CuratorFramework curatorFramework() {
        return CuratorFrameworkFactory.newClient(
                connectString,
                sessionTimeoutMs,
                connectionTimeoutMs,
                new RetryNTimes(retryCount, elapsedTimeMs));
    }
}
@Service
public class DistributedLockUtil {

    private static final Logger logger = LoggerFactory.getLogger(DistributedLockUtil.class);

    private final static String ROOT_PATH_LOCK = "/lock/";

    @Autowired
    private CuratorFramework curatorFramework;

    /**
     * InterProcessMutex:分布式可重入排它锁
     * InterProcessSemaphoreMutex:分布式排它锁
     * InterProcessReadWriteLock:分布式读写锁
     * InterProcessMultiLock:将多个锁作为单个实体管理的容器
     */
    public InterProcessMutex getLock(String path){
        return new InterProcessMutex(curatorFramework, ROOT_PATH_LOCK + path);
    }
}

测试代码

@Service
public class OrderNumberService {

    private int number = 0;

    @Autowired
    DistributedLockUtil distributedLockUtil;


    public String generateNo(){
        String orderNumber ="";
        try {
            Thread.sleep(new Random().nextInt(500));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        orderNumber =  "订单号:"+ (++number);
//        InterProcessMutex lock = distributedLockUtil.getLock("order");
//        try{
//            lock.acquire();
//            orderNumber =  "订单号:"+ (++number);
//        }catch (Exception e){
//            e.printStackTrace();
//        }
//        finally {
//            try {
//                lock.release();
//            } catch (Exception exception) {
//                exception.printStackTrace();
//            }
//        }
//
        return orderNumber;
    }

}

    @Test
    public void testZk(){
        int currency = 200;
        CyclicBarrier cyclicBarrier = new CyclicBarrier(currency);
        for (int i = 0; i <currency ; i++) {

            new Thread(() -> {
                System.out.println(Thread.currentThread().getName()+"线程准备好");
                try {
                    cyclicBarrier.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
                String s = orderNumberService.generateNo();
                System.out.println(s);
            }).start();
        }
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

比较

  • redis 分布式锁,每次锁释放后,每个client端都会去尝试获取锁。zookeeper可以使用顺序节点解决这个问题。

  • redis 获取锁的那个客户端 出现 bug 挂了,那么只能等待超时时间之后才能释放锁;而 zk 的话,因为创建的是临时 znode,只要客户端挂了,znode 就没了,此时就自动释放锁。

  • redis本身的读写性能很高,因此基于redis的分布式锁效率比较高

  • redis中为了解决单点故障引发的脏数据问题,需要引入redlock,而redlock需要多个独立的master节点;需要同时对多个节点申请锁,降低了一些效率 ,同时加大了资源的消耗。

  • 个人觉得单从性能考虑或者公司技术栈考虑(毕竟引入redis的概率比zk高很多),可以先择使用redis,其它情况应该选择更靠高的zookeeper,性能要求不高的话可以考虑基于数据库实现分布式锁。

posted on 2021-03-15 13:13  请叫我西毒  阅读(417)  评论(0编辑  收藏  举报