分布式解决方案

我可以等在这路口,不管你会不会经过

在这里插入图片描述

一、引言

在分布式环境下,传统的一些技术会失败,比如传统的synchronized或者lock锁,以及创建数据库的事务,无法保证ACID,还有定时任务也可能会出现重复执行的问题

二、分布式锁介绍


由于传统的锁是基于Tomcat服务器内部对象,搭建了集群之后,导致锁失效,所以要使用分布式锁来处理↓

分布式锁介绍
在这里插入图片描述

三、分布式锁解决方案【重点


3.1 搭建环境

创建SpringBoot工程,起名为秒杀second-kill,勾选web依赖↓

在这里插入图片描述

在这里插入图片描述

拷贝抢购的业务↓

package com.itheima.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.Map;

@RestController
public class SecondKillController {
    //1. 准备商品的库存
    public static Map<String,Integer> itemStock = new HashMap<>();
    //2. 准备商品的订单
    public static Map<String,Integer> itemOrder = new HashMap<>();

    static{
        itemStock.put("牙刷",10000);
        itemOrder.put("牙刷",0);
    }

    @GetMapping("/kill")//item表示用户访问的商品名称,等会浏览器地址栏我会输入牙刷
    public String kill(String item) throws InterruptedException {
        //1. 减库存
        Integer stock = itemStock.get(item);
        
        if(stock <= 0){
            return "商品库存数不足!!!";
        }
        
        Thread.sleep(100);//用睡眠来模拟消耗时间
        itemStock.put(item,stock-1);

        //2. 创建订单
        Thread.sleep(100);
        itemOrder.put(item,itemOrder.get(item)+1);

        //3. 返回信息
        return "抢购成功!!!" + item + ": 剩余库存数为" + itemStock.get(item) + ",订单数为" + itemOrder.get(item);
    }
}

用浏览器访问我们的控制器方法,库存和订单确实加起来是10000,没有超卖,但是我们别忘了还有其他用户的访问↓

http://localhost:8080/kill?item=牙刷

在这里插入图片描述

下载ab压力测试工具软件,解压软件即可,找到bin目录,cmd打开,输入下面地址来模拟1000个请求和500并发↓

ab -n 请求数 -c 并发数 访问的路径(复制浏览器地址过来,浏览器地址栏对牙刷进行了url编码所以是这样的↓)
ab -n 1000 -c 500 http://localhost:8080/kill?item=%E7%89%99%E5%88%B7

在这里插入图片描述

enter回车,看到执行完了请求和并发,然后打开浏览器地址值,再访问发现库存和订单加起来不是10000,超卖了↓

在这里插入图片描述

3.2 Zookeeper实现分布式锁原理图解
Zookeeper实现分布式锁原理图解
在这里插入图片描述

在这里插入图片描述

这里用临时节点有个好处,断开连接,锁资源就会消失,不用设置过期时间,而下面用redis做分布式锁需要过期时间

3.3 Zookeeper实现分布式锁代码实现

在上面写的秒杀工程里面导入依赖来解决问题

<dependency>
    <groupId>org.apache.zookeeper</groupId>
    <artifactId>zookeeper</artifactId>
    <version>3.6.0</version>
    <exclusions>
        <exclusion>
            <artifactId>slf4j-api</artifactId>
            <groupId>org.slf4j</groupId>
        </exclusion>
        <exclusion>
            <artifactId>slf4j-log4j12</artifactId>
            <groupId>org.slf4j</groupId>
        </exclusion>
        <exclusion>
            <artifactId>log4j</artifactId>
            <groupId>log4j</groupId>
        </exclusion>
    </exclusions>
</dependency>

<dependency>
    <groupId>org.apache.curator</groupId>
    <artifactId>curator-recipes</artifactId>
    <version>4.0.1</version>
    <exclusions>
        <exclusion>
            <artifactId>slf4j-api</artifactId>
            <groupId>org.slf4j</groupId>
        </exclusion>
    </exclusions>
</dependency>

拷贝配置类

package com.itheima.config;

import org.apache.curator.RetryPolicy;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.retry.ExponentialBackoffRetry;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ZkConfig {
    @Bean
    public CuratorFramework cf(){
        RetryPolicy retryPolicy = new ExponentialBackoffRetry(3000,2);//需要指定重试间隔和重试次数

        CuratorFramework curatorFramework = CuratorFrameworkFactory.builder()
                .connectString("192.168.1.129:2181,192.168.1.129:2182,192.168.1.129:2183")
                .retryPolicy(retryPolicy)
                .build();

        curatorFramework.start();

        return curatorFramework;
    }
}

在业务代码中添加分布式锁

InterProcessMutex lock = new InterProcessMutex(cf,"/lock");

//加锁
lock.acquire();
lock.acquire(1,TimeUnit.SECONDS); //指定排队多久放弃获取锁资源

//----------------业务逻辑代码------------------------

//释放锁
lock.release();
package com.itheima.controller;

import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.recipes.locks.InterProcessMutex;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;

@RestController
public class SecondKillController {
    //1. 准备商品的库存
    public static Map<String,Integer> itemStock = new HashMap<>();
    //2. 准备商品的订单
    public static Map<String,Integer> itemOrder = new HashMap<>();

    static{
        itemStock.put("牙刷",10000);
        itemOrder.put("牙刷",0);
    }

    @Autowired
    private CuratorFramework cf;

    @GetMapping("/kill")//item表示用户访问的商品名称,等会浏览器地址栏我会输入牙刷
    public String kill(String item) throws Exception {
        InterProcessMutex lock = new InterProcessMutex(cf,"/lock");

        //加锁
        lock.acquire();
        //lock.acquire(1,TimeUnit.SECONDS); //指定排队多久放弃获取锁资源

        //----------------业务逻辑代码↓------------------------
        //1. 减库存
        Integer stock = itemStock.get(item);

        if(stock <= 0){
            return "商品库存数不足!!!";
        }

        Thread.sleep(100);//用睡眠来模拟消耗时间
        itemStock.put(item,stock-1);

        //2. 创建订单
        Thread.sleep(100);
        itemOrder.put(item,itemOrder.get(item)+1);
        //----------------业务逻辑代码↑------------------------

        //释放锁
        lock.release();

        //3. 返回信息
        return "抢购成功!!!" + item + ": 剩余库存数为" + itemStock.get(item) + ",订单数为" + itemOrder.get(item);
    }
}
@RequestMapping(value = "/seckill")
public String seckill(String gname) throws Exception {
    InterProcessMutex interProcessMutex = new InterProcessMutex(cf, "/seckill_lock");

    //获取锁
    //interProcessMutex.acquire();//没有获取到锁就一直等待
    if(interProcessMutex.acquire(1, TimeUnit.SECONDS)){//等待1s就放弃获取锁资源
        //1.先减库存
        Integer stock = goodsMap.get(gname);
        if (stock <= 0) {
            return "库存不足。。。。";
        }
        goodsMap.put(gname, stock - 1);
        Thread.sleep(100);

        //2.加订单数量
        Integer count = orderMap.get(gname);
        orderMap.put(gname, count + 1);
        Thread.sleep(100);

        //释放锁
        interProcessMutex.release();

        return "抢购成功【" + gname + "】,库存:" + goodsMap.get(gname) + ",订单:" + orderMap.get(gname);
    }else{
        return "请稍后再试";
    }
}

修改完代码重启秒杀工程,发现不管用浏览器访问,还是用之前的ab压测工具软件访问,库存和订单加起来都是10000

3.4 Redis实现分布式锁原理图解
Redis实现分布式锁原理
在这里插入图片描述

这里出现了异常不走后面释放锁资源的代码怎么办?简单,为锁资源设置过期时间来防止死锁的发生

3.5 Redis实现分布式锁代码实现

上面的秒杀工程导入redis依赖,添加配置文件

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
# yml配置文件,配置redis服务器的ip和端口↓
spring:
  redis:
    host: 192.168.199.109
    port: 6379

拷贝工具类

package com.itheima.utils;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.util.concurrent.TimeUnit;

@Component
public class RedisLockUtil {
    @Autowired
    private StringRedisTemplate redisTemplate;

    public boolean lock(String key,String value,int second){
        return redisTemplate.opsForValue().setIfAbsent(key,value,second, TimeUnit.SECONDS);
    }

    public void unlock(String key){
        redisTemplate.delete(key);
    }
}

修改业务逻辑代码

@Autowired
private RedisLockUtil lock;

@GetMapping("/redis/kill")
public String redisKill(String item) throws Exception {
    //加锁
    if(lock.lock(item,System.currentTimeMillis() + "",1)){
        //业务代码...
        //释放锁
        lock.unlock(item);
    }
}
package com.itheima.controller;

import com.itheima.utils.RedisLockUtil;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.recipes.locks.InterProcessMutex;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;

@RestController
public class SecondKillController {
    //1. 准备商品的库存
    public static Map<String,Integer> itemStock = new HashMap<>();
    //2. 准备商品的订单
    public static Map<String,Integer> itemOrder = new HashMap<>();

    static{
        itemStock.put("牙刷",10000);
        itemOrder.put("牙刷",0);
    }

    @Autowired
    private RedisLockUtil lock;

    @GetMapping("/kill")//item表示用户访问的商品名称,等会浏览器地址栏我会输入牙刷
    public String kill(String item) throws Exception {
        //加锁
        if(lock.lock(item,System.currentTimeMillis() + "",1)){
            //业务代码
            //----------------业务逻辑代码↓------------------------
            //1. 减库存
            Integer stock = itemStock.get(item);

            if(stock <= 0){
                return "商品库存数不足!!!";
            }

            Thread.sleep(100);//用睡眠来模拟消耗时间
            itemStock.put(item,stock-1);

            //2. 创建订单
            Thread.sleep(100);
            itemOrder.put(item,itemOrder.get(item)+1);
            //----------------业务逻辑代码↑------------------------

            //释放锁
            lock.unlock(item);
        }

        //3. 返回信息
        return "抢购成功!!!" + item + ": 剩余库存数为" + itemStock.get(item) + ",订单数为" + itemOrder.get(item);
    }
}

修改完代码重启秒杀工程,发现不管用浏览器访问,还是用之前的ab压测工具软件访问,库存和订单加起来都是10000

四、分布式任务介绍


分布式任务介绍
在这里插入图片描述

五、分布式任务解决方案【重点


5.1 Elastic-Job介绍

在这里插入图片描述

官网:http://elasticjob.io/index_zh.html

由当当网基于Quartz + Zookeeper的二次开放产品

  • 基于Zookeeper分布式锁,保证只有一个服务去执行定时任务

  • 基于Zookeeper实现了注册中心,自动帮助我们去调度指定的服务执行定时任务

  • 基于Zookeeper实现了注册中心,基于心跳的方式,自动去检测服务的健康情况

5.2 Elastic-Job实现分布式任务调度

跟上面创建秒杀工程套路一样,创建SpringBoot工程起名叫job,勾选web依赖↓

还要额外导入job依赖↓

<dependency>
    <groupId>com.dangdang</groupId>
    <artifactId>elastic-job-lite-spring</artifactId>
    <version>2.1.5</version>
</dependency>

新建job包,创建定时任务↓

package com.itheima.job;

import com.dangdang.ddframe.job.api.ShardingContext;
import com.dangdang.ddframe.job.api.simple.SimpleJob;
import org.springframework.stereotype.Component;

@Component
public class MyElasticJob implements SimpleJob {
    @Override
    public void execute(ShardingContext context) {
        switch (context.getShardingItem()) {
            case 0:
                System.out.println("执行0任务!!");
                break;
            case 1:
                System.out.println("执行1任务!!");
                break;
            case 2:
                System.out.println("执行2任务!!");
                break;
            // case n: ...
        }
    }
}

启动类,配置配置中心和任务调度↓

package com.itheima;

import com.dangdang.ddframe.job.config.JobCoreConfiguration;
import com.dangdang.ddframe.job.config.simple.SimpleJobConfiguration;
import com.dangdang.ddframe.job.lite.config.LiteJobConfiguration;
import com.dangdang.ddframe.job.lite.spring.api.SpringJobScheduler;
import com.dangdang.ddframe.job.reg.base.CoordinatorRegistryCenter;
import com.dangdang.ddframe.job.reg.zookeeper.ZookeeperConfiguration;
import com.dangdang.ddframe.job.reg.zookeeper.ZookeeperRegistryCenter;
import com.itheima.job.MyElasticJob;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;

@SpringBootApplication
public class JobApplication {
    public static void main(String[] args) {
        SpringApplication.run(JobApplication.class, args);
    }

    //配置注册中心到容器中
    @Bean
    public CoordinatorRegistryCenter center(){
        CoordinatorRegistryCenter regCenter = new ZookeeperRegistryCenter(
                new ZookeeperConfiguration("192.168.1.129:2181,192.168.1.129:2182,192.168.1.129:2183", "elastic-job-demo"));
        regCenter.init();
        return regCenter;
    }

    //配置执行任务调度信息
    @Bean
    public SpringJobScheduler scheduler(MyElasticJob job, CoordinatorRegistryCenter center){
        //定义作业核心配置,3代表三个任务
        JobCoreConfiguration simpleCoreConfig = JobCoreConfiguration.
                newBuilder("demoSimpleJob", "0/10 * * * * ?", 3)
                .shardingItemParameters("0=A,1=B,2=C").build();
        //定义SIMPLE类型配置
        SimpleJobConfiguration simpleJobConfig = new SimpleJobConfiguration(simpleCoreConfig, MyElasticJob.class.getCanonicalName());
        //定义Lite作业根配置
        LiteJobConfiguration simpleJobRootConfig = LiteJobConfiguration.newBuilder(simpleJobConfig).build();
        //定义SpringJobScheduler
        SpringJobScheduler scheduler = new SpringJobScheduler(job,center,simpleJobRootConfig);
        scheduler.init();
        return scheduler;
    }
}

测试,点击右上角的下三角出现配置界面,点击拷贝配置,在vm处输入-Dserver.port=8081点击ok开启两个应用↓

在这里插入图片描述

发现,开启两个应用,任务被分且不重复,把其中一个应用停掉,任务又能够去到没有停掉的应用去执行,问题解决↓

六、分布式事务介绍


6.1分布式事务介绍
分布式事务介绍
在这里插入图片描述
6.2 Base理论

CAP理论,C:一致性,A:可用性,P:分区容错性。分布式环境下,三者取其二。

  • Eureka:AP,保证了可用性,舍弃了一致性。

  • Zookeeper:CP,每一个节点必须能够找到Master才能对外提供服务,舍弃了可用性。

Base理论,BA:基本可用,S:中间状态,E:最终一致性。

  • 基于CAP理论演化而来的,是对CAP定理中一致性和可用性的一个权衡结果。

  • 核心思想:我们无法做到强一致性,但是每一个应用都可以根据自身的业务特点,采用一些适当的方式来权衡,最终达到一致性。

  • BA:分布式系统中因为一个原因,导致出现了一些问题,允许损失掉部分服务的可用性,保证我核心功能的高可用。

  • S:允许系统之间存在一个中间状态,并不会影响正常的去使用整个系统,允许数据的同步存在延迟。

  • E:系统中所有的数据副本经过一定时间后,最终能够达到一致的状态,不需要保证系统数据强一致性。

七、分布式事务解决方案【重点


7.1 2PC两段提交

两段提交分为两个阶段:

  • 第一个阶段是准备阶段,参与者需要开启事务,执行SQL,保证数据库中已经存在相应的数据。参与者会向TransactionManager准备OK。

  • 第二个阶段当TransactionManager收到了所有的参与者的通知之后,向所有的参与者发送Commit请求。

问题1:执行的性能是很低的。一般是传统事务的10倍以上。

问题2:TransactionManager是没有超时时间的。

问题3:TransactionManager存在单点故障的问题

2PC两段提交
在这里插入图片描述
7.2 3PC三段提交

三段提交在二段提交的基础上,引入了超时时间机制,并且在二段提交的基础上,又多了一个步骤,在提交事务之前,再询问一下,数据库的日志信息,是否已经完善。

3PC三段提交
在这里插入图片描述
7.3 TCC机制

TCC(Try,Confirm,Cancel),和你的业务代码切合在一起。

  • Try:尝试去预执行具体业务代码。 下单订ing。。。

  • try成功了:Confirm:再次执行Confirm的代码。

  • try失败了:Cancel:再次执行Cancel的代码。

TCC
在这里插入图片描述
7.4 MQ分布式事务

RabbitMQ在发送消息时,confirm机制,可以保证消息发送到MQ服务中,消费者有手动ack机制,保证消费到MQ中的消息。

MQ分布式事务
在这里插入图片描述
7.5 LCN实现分布式事务

基于三段提交和TCC实现的

创建一个协调者工程,创建两个服务

协调者:添加依赖

<dependency>
    <groupId>com.codingapi.txlcn</groupId>
    <artifactId>txlcn-tm</artifactId>
    <version>5.0.2.RELEASE</version>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

协调者:编写配置文件

server:
  port: 8080

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql:///lcn?serverTimezone=UTC
    username: root
    password: root
  redis:
    host: 192.168.199.109
    port: 6379
    # 协调的端口号
tx-lcn:
  manager:
    port: 8070

协调者:添加注解

@EnableTransactionManagerServer

协调者:准备表

# 创建表
CREATE TABLE `t_tx_exception`  (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `group_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `unit_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `mod_id` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `transaction_state` tinyint(4) NULL DEFAULT NULL,
  `registrar` tinyint(4) NULL DEFAULT NULL,
  `remark` varchar(4096) NULL DEFAULT  NULL,
  `ex_state` tinyint(4) NULL DEFAULT NULL COMMENT '0 未解决 1已解决',
  `create_time` datetime(0) NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;

服务: 添加依赖

<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.1.2</version>
</dependency>

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>

<dependency>
    <groupId>com.codingapi.txlcn</groupId>
    <artifactId>txlcn-tc</artifactId>
    <version>5.0.2.RELEASE</version>
</dependency>

<dependency>
    <groupId>com.codingapi.txlcn</groupId>
    <artifactId>txlcn-txmsg-netty</artifactId>
    <version>5.0.2.RELEASE</version>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

服务: 编写配置文件

server:
  port: 8081

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql:///lcn?serverTimezone=UTC
    username: root
    password: root

tx-lcn:
  client:
    manager-address: localhost:8070

服务: 启动类添加注解

@EnableDistributedTransaction

服务:Service层添加注解

@Override
@Transactional
@LcnTransaction
public void createOrder() {
//        1. 减库存
    restTemplate.getForObject("http://localhost:8082/item",String.class);
    int i = 1/0;
//        2. 创建订单
    orderMapper.save();
}

测试,异常后,事务回滚

测试
在这里插入图片描述
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

卢卢在路上

人生苦短,及时行乐

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值