• 分布式锁 & 分布式事务


    分布式锁

    1、锁:

    单进程的系统中,存在多线程同时操作一个公共变量,此时需要加锁对变量进行同步操作,保证多线程的操作线性执行消除并发修改。解决的是单进程中的多线程并发问题。

    2、分布式锁:

    只要的应用场景是在集群模式的多个相同服务,可能会部署在不同机器上,解决进程间安全问题,防止多进程同时操作一个变量或者数据库。解决的是多进程的并发问题。

     一、基于Redis实现

    在Redis2.6.12版本之前,使用setnx命令设置key-value、使用expire命令设置key的过期时间获取分布式锁,使用del命令释放分布式锁,但是这种实现有如下一些问题: 

    1. setnx命令设置完key-value后,还没来得及使用expire命令设置过期时间,当前线程挂掉了,会导致当前线程设置的key一直有效,后续线程无法正常通过setnx获取锁,造成死锁

    这个问题很好解决,只因为这两个不是一个原子操作。2.6.12之后的版本,set命令进行了增强SET key value [EX seconds] [PX milliseconds] [NX|XX]

    • EX second :设置键的过期时间为 second 秒。 SET key value EX second 效果等同于 SETEX key second value 。
    • PX millisecond :设置键的过期时间为 millisecond 毫秒。 SET key value PX millisecond 效果等同于 PSETEX key millisecond value 。
    • NX :只在键不存在时,才对键进行设置操作。 SET key value NX 效果等同于 SETNX key value 。
    • XX :只在键已经存在时,才对键进行设置操作。

    2. 在分布式环境下,进程A通过这种实现方式获取到了锁,但是在获取到锁之后,执行被阻塞,阻塞时间大于key超时时间导致该锁失效;之后进程B获取到该锁,之后进程A恢复执行,执行完成后释放该锁,将会把进程B的锁也释放掉。也就是把他人的锁释放掉的问题,实际上还有另一个问题就是任务完成之前key失效的问题

    这个问题也很好解决,只需要在value中存放一个唯一标识符,释放的时候判断是不是自己的标识符即可,如果是自己的,就可以释放

    3. 为了实现高可用,将会选择主从复制机制,但是主从复制机制是异步的,会出现数据不同步的问题,可能导致多个机器的多个线程获取到同一个锁。

    解决方案是不采用主从复制,使用RedLock算法(官方推荐)

    1.获取当前Unix时间,以毫秒为单位

    2.依次尝试从N个实例,使用相同的key和随机值获取锁。当向Redis设置锁时,客户端应该设置一个网络连接和响应超时时间,这个超时时间应该小于锁的失效时间。例如你的锁自动失效时间为10秒,则超时时间应该在5-50毫秒之间。这样可以避免服务器端Redis已经挂掉的情况下,客户端还在死死地等待响应结果。如果服务器端没有在规定时间内响应,客户端应该尽快尝试另外一个Redis实例

    3.客户端使用当前时间减去开始获取锁时间(步骤1记录的时间)就得到获取锁使用的时间。当且仅当从大多数(这里是3个节点)的Redis节点都取到锁,并且使用的时间小于锁失效时间时,锁才算获取成功

    4.如果取到了锁,key的真正有效时间等于有效时间减去获取锁所使用的时间(步骤3计算的结果)

    5.如果因为某些原因,获取锁失败(没有在至少N/2+1个Redis实例取到锁或者取锁时间已经超过了有效时间),客户端应该在所有的Redis实例上进行解锁(即便某些Redis实例根本就没有加锁成功)。

    这里我提供两种实现(测试的话新建一个SpringBoot工程,引入spring-boot-starter-data-redis依赖)

    利用set

    package com.example.demo.utils;
    
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.data.redis.core.BoundValueOperations;
    import org.springframework.data.redis.core.StringRedisTemplate;
    import org.springframework.util.StringUtils;
    
    import java.util.Random;
    import java.util.concurrent.TimeUnit;
    
    /**
     * 要求:
     * 1. 在分布式系统环境下,一个方法或者变量同一时间只能被一个线程操作
     * 2. 具备锁失效机制,网络中断或宕机无法释放锁时,锁必须被删除,防止死锁
     * 3. 具备阻塞锁特性,即没有获取到锁,则继续等待获取锁
     * 4. 具备非阻塞锁特性,即没有获取到锁,则直接返回获取锁失败
     * 5. 具备可重入特性,一个线程中可以多次获取同一把锁
     *
     * 问题:
     * 1. setnx后发生了异常而没有设置过期时间,导致key一直有效,造成死锁(SEX命令代替SEXNX+EXPIRE)
     * 2. 分布式环境下,进程A获取到了锁,但是执行时间过长,锁已经自动失效了,此时进程B获取到了锁,然后进程A执行完毕,执行del命令,把锁释放掉,结果把进程B的也释放了,导致不可预知的问题(可以把value设置成自己唯一标识符,这样只能删除自己的锁)
     * 3. Redis集群中主从数据同步是从库异步读取主库,会出现数据不同步的问题,这样导致多个进程获取同一把锁(不采用主从复制,使用RedLock算法)
     * 4. 在任务执行完之前,key失效了怎么办?(锁获取成功后,注册一个定时任务,每隔一定时间(this.internalLockLeaseTime / 3L)就去续约)
     *
     * 扩展:Redis 集群没有使用一致性hash, 而是引入了哈希槽的概念。
     * Reds 集群有16384个哈希槽,每个key通过CRC16校验后对16384取模来决定放置哪个槽.集群的每个节点负责一部分hash槽。这种结构很容易添加或者删除节点,并且无论是添加删除或者修改某一个节点,都不会造成集群不可用的状态。
     * 使用哈希槽的好处就在于可以方便的添加或移除节点。
     * 当需要增加节点时,只需要把其他节点的某些哈希槽挪到新节点就可以了;
     * 当需要移除节点时,只需要把移除节点上的哈希槽挪到其他节点就行了;
     *
     * 两种实现:
     * 1. 利用set操作,设置失效时间。(问题:不好控制锁的失效时间,可能在key失效之前,同步任务没有完成)
     * 2. 利用setnx、get、getset操作,不设置失效时间,失效时间作为value存储
     *
     *
     * SET key value [EX seconds] [PX milliseconds] [NX|XX]
     * EX-过期时间秒;PX-过期时间毫秒;NX-key不存在则设置成功;XX-key存在则设置成功
     *
     * GETSET key value(原子操作)
     * 将给定 key 的值设为 value ,并返回 key 的旧值(old value)。
     */
    @Slf4j
    public class RedisLockUtils {
    
        // key失效时间:60000毫秒
        private static final Long EXPIRE_TIME = 60L * 1000;
    
        // 线程等待时间:10000毫秒
        private static final Long WAIT_MAX_TIMEOUT = 10L * 1000;
    
        /**
         * 获取锁
         * @param redis     redis服务
         * @param key       key
         * @param ownerFlag 锁拥有者标识,防止把其他人的释放了
         * @param operation 业务逻辑
         * @param <T>
         * @return
         */
        public static <T> T executeOperation(StringRedisTemplate redis, String key, String ownerFlag, RedisLockUtils.Operation<T> operation) {
            validParam(redis, key, ownerFlag);
            long start = System.currentTimeMillis();
            boolean locked = false;
            T resultObj;
    
            try {
                Random rand = new Random();
                for (;;) {
                    BoundValueOperations<String, String> ops = redis.boundValueOps(key);
                    // 设置key
                    if (ops.setIfAbsent(ownerFlag, EXPIRE_TIME, TimeUnit.SECONDS)) {
                        locked = true;
                        break;
                    }
                    // 超时
                    if ((System.currentTimeMillis() - start) >= WAIT_MAX_TIMEOUT){
                        break;
                    }
                    // 获取不到锁,睡眠随机时间
                    int sleepTime = rand.nextInt(1001);  // 取值:0-1000毫秒
                    Thread.sleep(sleepTime);
                }
                // 执行业务逻辑
                resultObj = operation.invoke(locked);
    
                if (log.isDebugEnabled()) {
                    log.debug("executeOperation success");
                }
            } catch (Exception e) {
                throw new RuntimeException(e);
            } finally {
                // 获取锁持有者的标识
                String v = redis.boundValueOps(key).get();
                log.info("release lock,value:{},ownerFlag:{}", v, ownerFlag);
                // 只能释放自己的
                if (ownerFlag.equals(v)){
                    redis.boundValueOps(key).expire(0L, TimeUnit.SECONDS);
                }
            }
    
            return resultObj;
        }
    
        private static void validParam(StringRedisTemplate redis, String key, String ownerFlag){
            if (redis == null){
                throw new RuntimeException("redisService can't be null");
            }
            if (StringUtils.isEmpty(key)){
                throw new RuntimeException("key can't be null");
            }
            if (StringUtils.isEmpty(ownerFlag)){
                throw new RuntimeException("ownerFlag can't be null");
            }
        }
    
        @FunctionalInterface
        public interface Operation<T>{
            T invoke(boolean locked);
        }
    }

    测试

    package com.example.demo;
    
    import com.example.demo.utils.RedisDistributeLock;
    import com.example.demo.utils.RedisLockUtils;
    import org.junit.jupiter.api.Test;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.context.SpringBootTest;
    import org.springframework.data.redis.core.StringRedisTemplate;
    
    import java.util.concurrent.*;
    
    @SpringBootTest
    class DemoApplicationTests {
    
        @Autowired
        private StringRedisTemplate stringRedisTemplate;
    
        // 共享变量
        private int share = 0;
    
        @Test
        void testOne() throws InterruptedException {
            // 线程数量
            int threadCount = 100;
            CountDownLatch countDownLatch = new CountDownLatch(threadCount);
            CyclicBarrier cyclicBarrier = new CyclicBarrier(threadCount);
            ExecutorService executorService = Executors.newFixedThreadPool(threadCount);
    
            for (int i = 0; i < threadCount; i++){
                executorService.execute(() -> {
                    try {
                        // 让所有线程同时开始执行
                        cyclicBarrier.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } catch (BrokenBarrierException e) {
                        e.printStackTrace();
                    }
                    Integer result = RedisLockUtils.executeOperation(stringRedisTemplate, "redis_lock", Thread.currentThread().getName(), locked -> {
                        if (locked) {
                            return share++;
                        }
                        return -1;
                    });
                    System.out.println(result);
                    countDownLatch.countDown();
                });
            }
            // 为了等线程全部执行完毕才离开测试代码块
            countDownLatch.await();
            System.out.println("==================================");
        }
    }

    输出:

    利用setnx + get + getset

    package com.example.demo.utils;
    
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.data.redis.core.StringRedisTemplate;
    
    import java.util.Random;
    import java.util.concurrent.TimeUnit;
    
    @Slf4j
    public class RedisDistributeLock {
    
        /**
         * 锁超时时间,防止线程在入锁以后,无限的执行等待
         */
        private static int EXPIRE_TIMEOUT = 60 * 1000;
    
        /**
         * 锁等待时间,防止线程饥饿
         */
        private static int WAIT_TIMEOUT = 10 * 1000;
    
        public static <T> T executeOperation(StringRedisTemplate redisService, String lockKey, RedisDistributeLock.Operation<T> operation) {
            T resultObj;
            long start = System.currentTimeMillis();
            Random rand = new Random();
            boolean locked = false;
    
            try {
                for (;;) {
                    long expireTime = System.currentTimeMillis() + EXPIRE_TIMEOUT + 1;
                    String expireValue = String.valueOf(expireTime); // 锁到期时间
    
                    // 1. setnx:尝试获取锁
                    if (redisService.boundValueOps(lockKey).setIfAbsent(expireValue)) {
                        // 设置成功,获取到锁
                        locked = true;
                        break;
                    }
    
                    // 2. get:获取已占用的锁设置的value【走到这,说明上一步中锁被占用】
                    String currentExpireValue = redisService.boundValueOps(lockKey).get(); // redis里的时间
    
                    // redis里的时间小于当前时间【说明已经超时,其它线程可以抢占了】
                    if (currentExpireValue != null && Long.parseLong(currentExpireValue) < System.currentTimeMillis()) {
                        // 3. getset:设置新时间,返回旧时间
                        String oldExpireValue = redisService.boundValueOps(lockKey).getAndSet(expireValue);
    
                        // 只有一个线程能成功getset,当get的返回值和getset的返回值一样,才能获取到锁,如果不一致,则证明被其它线程获取了
                        if (oldExpireValue != null && oldExpireValue.equals(currentExpireValue)) {
                            // getset成功,获取到锁
                            locked = true;
                            break;
                        }
                    }
    
                    // 4. 获取锁超时
                    if ((System.currentTimeMillis() - start) >= WAIT_TIMEOUT){
                        break;
                    }
                    // 获取不到锁,睡眠随机时间
                    int sleepTime = rand.nextInt(1001);  // 取值:0-1000毫秒
                    Thread.sleep(sleepTime);
                }
    
                // 执行业务逻辑
                resultObj = operation.invoke(locked);
            } catch (Exception e) {
                throw new RuntimeException(e);
            } finally {
                // 5. 最后,如果未超时,则删除锁
                if (System.currentTimeMillis() < Long.parseLong(redisService.boundValueOps(lockKey).get())) {
                    redisService.boundValueOps(lockKey).expire(0L, TimeUnit.SECONDS);
                }
            }
            return resultObj;
        }
        
        @FunctionalInterface
        public interface Operation<T>{
            T invoke(boolean locked);
        }
    
    }

    测试

    package com.example.demo;
    
    import com.example.demo.utils.RedisDistributeLock;
    import com.example.demo.utils.RedisLockUtils;
    import org.junit.jupiter.api.Test;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.context.SpringBootTest;
    import org.springframework.data.redis.core.StringRedisTemplate;
    
    import java.util.concurrent.*;
    
    @SpringBootTest
    class DemoApplicationTests {
    
        @Autowired
        private StringRedisTemplate stringRedisTemplate;
    
        // 共享变量
        private int share = 0;
    
        @Test
        void testTwo() throws InterruptedException {
            // 线程数量
            int threadCount = 100;
            CountDownLatch countDownLatch = new CountDownLatch(threadCount);
            CyclicBarrier cyclicBarrier = new CyclicBarrier(threadCount);
            ExecutorService executorService = Executors.newFixedThreadPool(threadCount);
    
            for (int i = 0; i < threadCount; i++){
                executorService.execute(() -> {
                    try {
                        // 让所有线程同时开始执行
                        cyclicBarrier.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } catch (BrokenBarrierException e) {
                        e.printStackTrace();
                    }
                    Integer result = RedisDistributeLock.executeOperation(stringRedisTemplate, "redis_lock_time", locked -> {
                        if (locked) {
                            return share++;
                        }
                        return -1;
                    });
                    System.out.println(result);
                    countDownLatch.countDown();
                });
            }
            // 为了等线程全部执行完毕才离开测试代码块
            countDownLatch.await();
            System.out.println("==================================");
        }
    
    }

    输出:

    二、基于ZooKeeper的实现方式

    利用zookeeper规定同一个目录下只能有一个唯一文件名的特性。

    1. 创建一个目录mylock; 

    2. 线程A想获取锁就在mylock目录下创建临时顺序节点;

    3. 获取mylock目录下所有的子节点,然后获取比自己小的兄弟节点,如果不存在,则说明当前线程顺序号最小,获得锁;

    4. 线程B获取所有节点,判断自己不是最小节点,设置监听比自己次小的节点;

    5. 线程A处理完,删除自己的节点,线程B监听到变更事件,判断自己是不是最小的节点,如果是则获得锁。

    ------------------------------------------------------------------------------------------------

    另附:Apache的开源ZooKeeper客户端Curator,提供了分布式锁的实现 InterProcessMutex,acquire方法用于获取锁,release方法用于释放锁。

    优点:具备高可用、可重入、阻塞锁特性,可解决失效死锁问题。

    缺点:因为需要频繁的创建和删除节点,性能上不如Redis方式。

    分布式事务

    1、事务

    解决一个会话过程中,上下文的修改对所有数据库表的操作要么全部成功,要不全部失败。所以应用在service层。解决的是一个会话中的操作的数据一致性。

    2、分布式事务

    比如一个转账操作:

    (1)招行账户减少100

    (2)建行账户增加100

    这时候需要保证对两个服务的操作全部成功或者全部回退。解决的是组合服务的数据操作的一致性问题。

    ACID

    数据库事务的四大特性(ACID):原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)。

    ⑴ 原子性(Atomicity)
    一个事务内所有操作共同组成一个原子包,要么全部成功,要么全部失败。

    ⑵ 一致性(Consistency)
    数据库事务的一致性就规定了事物提交前后,永远只可能存在事物提交前的状态和事物提交后的状态,从一个一致性的状态到另一个一致性状态,而不可能出现中间的过程态。也就是说事物的执行结果是量子化状态,而不是线性状态。

    ⑶ 隔离性(Isolation)
    事务的隔离性,基于原子性和一致性,因为事务是原子化,量子化的,所以,事务可以有多个原子包的形式并发执行,但是,每个事务互不干扰。

    ⑷ 持久性(Durability)
    持久性,当一个事物提交之后,数据库状态永远的发生了改变。

    CAP

    一个分布式系统不可能同时满足一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance),最多满足其中两项。

    一致性:指的是在分布式环境下,数据在多个副本之间是否能够保持一致的特性。比如系统的数据副本分布在不同节点上,对第一个节点的数据进行更新后,其它的节点也应该得到相应的更新。

    可用性:指的是系统提供的服务必须处于一直可用的状态。

    分区容错性:分布式系统在遇到任何网络分区故障的时候,仍然需要保证对外提供满足一致性和可用性的服务,除非是整个网络环境都发生了故障。

    BASE

    BASE理论:是Basically Available(基本可用),Soft state(软状态),Eventually consistent(最终一致性)三个短语的简写,其核心思想是,即使无法做到强一致性,每个应用也应该采用适当的方式来达到最终一致性。

    基本可用:出现不可预知的故障,允许损失部分可用性。比如发生故障的时候比正常慢了1秒。

    软状态:允许系统在不同的节点进行数据同步的过程中存在延时。

    最终一致性:系统中所有副本,在经过一段时间的同步后,最终能达到一致性的状态。

    一致性解决方案

    2PC(二阶段提交)

    • 第一阶段是表决阶段,所有参与者都将本事务能否成功的信息反馈发给协调者;
    • 第二阶段是执行阶段,协调者根据所有参与者的反馈,通知所有参与者,步调一致地在所有分支上提交或者回滚;

    3PC(三阶段提交,2PC的改进版本)

    • CanCommit:
    1. 事务询问:协调者向参与者发送一个包含事务内容的 canCommit 请求,询问是否可以执行事务提交操作,并等待响应。
    2. 各参与者向协调者反馈事务询问的响应,如果参与者认为自己可以顺利执行事务,就返回 Yes,否则反馈 No 响应。
    • PreCommit

      协调者在得到所有参与者的响应之后,会根据结果执行2种操作:执行事务预提交,或者中断事务。

      1. 执行事务预提交:

        首先协调者向所有参与者节点发出 preCommit 的请求,并等待响应。然后参与者受到 preCommit 请求后,会执行事务操作,并将结果返回。最后协调者得到了Ack响应,确定下一阶段是否为提交或者是终止操作。

      2. 中断事务也分为2个步骤:

        首先协调者向所有参与者节点发出 abort 请求 。然后参与者如果收到 abort 请求或者超时了,都会中断事务。

    • do Commit

      1. 执行提交:

        首先协调者发送提交请求,并等待Ack 响应。然后参与者收到 doCommit 请求后,执行事务并反馈事务提交结果,向协调者发送 Ack 消息。最后协调者接收 Ack 消息后,完成事务。

      2. 中断事务:

        中断事务是因为出现了异常,比如协调者一方出现了问题,或者是协调者与参与者之间出现了故障。

        首先协调者向所有的参与者发送中断请求。然后参与者接收到中断请求后,会利用其在二阶段记录的 undo 信息来执行事务回滚操作,并释放资源。接下来参与者在完成事务回滚之后,向协调者发送 Ack 消息。最后协调者接收到所有的 Ack 消息后,中断事务。

    TCC

    TCC 其实就是采用的补偿机制,其核心思想是:针对每个操作,都要注册一个与其对应的确认和补偿(撤销)操作。

    • Try 阶段主要是对业务系统做检测及资源预留
    • Confirm 阶段主要是对业务系统做确认提交,Try阶段执行成功并开始执行 Confirm阶段时,默认 Confirm阶段是不会出错的。即:只要Try成功,Confirm一定成功。
    • Cancel 阶段主要是在业务执行错误,需要回滚的状态下执行的业务取消,预留资源释放。

    可靠消息最终一致性

    可靠消息最终一致性事务适合执行周期长且实时性要求不高的场景,引入消息机制后,同步的事务操作变为基于消息执行的异步操作,避免分布式事务中的同步阻塞操作的影响,并实现了两个服务的解耦。

    比如:上游应用业务执行成功后发送消息,消息系统保证消息的传递,下游应用监听消息并执行相关业务。

  • 相关阅读:
    Django rest framework集成微博第三方登录
    Python web项目Django部署在Ubuntu18.04腾讯云主机上
    Mac中安装JDK1.8和JDK11双版本并任意切换
    Nginx完美解决前后端分离端口号不同导致的跨域问题
    Mac系统安装Tomcat服务器
    Python将数据渲染到docx文档指定位置
    IO多路复用select/poll/epoll详解以及在Python中的应用
    Tornado框架实现异步爬虫
    广州商学院Python正方教务系统爬虫(获取个人信息成绩课表修改密码)
    Python3使用tkinter编写GUI程序
  • 原文地址:https://www.cnblogs.com/LUA123/p/12802334.html
Copyright © 2020-2023  润新知