其他分享
首页 > 其他分享> > jdk8新特性以及微服务:微服务保护和分步式事务

jdk8新特性以及微服务:微服务保护和分步式事务

作者:互联网

JAVA8其他新特性

​ Java 8 (又称为 jdk 1.8) 是 Java 语言开发的一个主要版本。 Java 8 是oracle公司于2014年3月发布,可以看成是自Java 5 以 来最具革命性的版本。Java 8为Java语言、编译器、类库、开发 工具与JVM带来了大量新特性。

Lambda表达式

Lambda 是一个匿名函数,我们可以把 Lambda 表达式理解为是一段可以传递的代码(将代码像数据一样进行传递)。使用它可以写出更简洁、更 灵活的代码。作为一种更紧凑的代码风格,使Java的语言表达能力得到了 提升。

Lambda表达式是一个接口的实例,该接口应当是一个函数式接口(有且仅有一个抽象方法)

案例

public static void main(String[] args) {
    Runnable r1 = new Runnable() {
        @Override
        public void run() {
            System.out.println("hello");
        }
    };
    r1.run();

    Runnable r2 = ()-> System.out.println("hello,too");
    r2.run();
}

案例二

@Test
public void test1(){
    Comparator<Integer> c1 = new Comparator<Integer>() {
        @Override
        public int compare(Integer o1, Integer o2) {
            return Integer.compare(o1,o2);
        }
    };
    int result1 = c1.compare(12,21);
    System.out.println(result1);
	//lambda表达式
    Comparator<Integer> c2 = (o1,o2)->Integer.compare(o1,o2);
    int result2 = c2.compare(33,21);
    System.out.println(result2);

}

Lambda 表达式:在Java 8 语言中引入的一种新的语法元素和操 作符。这个操作符为 “->” , 该操作符被称为 Lambda 操作符 或箭头操作符。它将 Lambda 分为两个部分:

左侧:指定了 Lambda 表达式需要的参数列表

左边:lambda形参列表的参数类型可以省略(类型推断);如果lambda形参列表只有一个参数,其一对()也可以省略

右侧:指定了 Lambda 体,是抽象方法的实现逻辑,也即Lambda 表达式要执行的功能。

右边:lambda体应该使用一对{}包裹;如果lambda体只有一条执行语句(可能是return语句),省略这一对{}和return关键字

案例三:

@Test
public void test3() {
    Consumer<String> con = new Consumer<String>() {
        @Override
        public void accept(String o) {
            System.out.println(o);
        }
    };
    //lambda表达式
    Consumer<String> con2 = (String o) -> {
        System.out.println(o);
    };
    //数据类型可以省略,由编译器推断得出
    Consumer<String> con3 = (o) -> {
        System.out.println(o);
    };
    //参数只有一个时,参数小括号可以省略
    Consumer<String> con4 = o -> {
        System.out.println(o);
    };
    //有多个参数,多条语句,且有返回值
    Comparator<Integer> c1=(num1,num2)->{
        System.out.println(num1);
        System.out.println(num2);
        return num1.compareTo(num2);
    };
    //当lambda体只有一条时,return和大括号都可以省略
    Comparator<Integer> c2=(num1,num2)-> num1.compareTo(num2);
}

函数式接口

有且仅有一个抽象方法的接口成为函数式接口。@FunctionalInterface

你可以通过 Lambda 表达式来创建该接口的对象。(若 Lambda 表达式 抛出一个受检异常(即:非运行时异常),那么该异常需要在目标接口的抽 象方法上进行声明)。

在java.util.function包下定义了Java 8 的丰富的函数式接口

隔离

限流是一种预防措施,虽然限流可以尽量避免因高并发而引起的服务故障,但服务还会因为其它原因而故障。

而要将这些故障控制在一定范围,避免雪崩,就要靠线程隔离(舱壁模式)和熔断降级手段了。

线程隔离之前讲到过:调用者在调用服务提供者时,给每个调用的请求分配独立线程池,出现故障时,最多消耗这个线程池内资源,避免把调用者的所有资源耗尽。

Feign整合Sentinel

feign:
	sentinel:    
		enabled: true # 开启Feign的Sentinel功能

完成整合!

Sentinel支持的雪崩解决方案:

Feign整合Sentinel的步骤:


线程隔离有两种方式实现:

如图:

image-20210716123036937

线程池隔离:给每个服务调用业务分配一个线程池,利用线程池本身实现隔离效果

信号量隔离:不创建线程池,而是计数器模式,记录业务使用的线程数量,达到信号量上限时,禁止新的请求。

两者的优缺点:

image-20210716123240518


熔断降级

熔断降级是解决雪崩问题的重要手段。其思路是由断路器统计服务调用的异常比例、慢请求比例,如果超出阈值则会熔断该服务。即拦截访问该服务的一切请求;而当服务恢复时,断路器会放行访问该服务的请求。

断路器控制熔断和放行是通过状态机来完成的:

image-20210716130958518

状态机包括三个状态:

断路器熔断策略有三种:慢调用、异常比例、异常数


断路器熔断策略有三种:慢调用、异常比例、异常数

慢调用

例如:

image-20210716145934347

解读:RT超过500ms的调用是慢调用,统计最近10000ms内的请求,如果请求量超过10次,并且慢调用比例不低于0.5,则触发熔断,熔断时长为5秒。然后进入half-open状态,放行一次请求做测试。

案例

需求:给 UserClient的查询用户接口设置降级规则,慢调用的RT阈值为50ms,统计时间为1秒,最小请求数量为5,失败阈值比例为0.4,熔断时长为5

1)设置慢调用

修改user-service中的/user/{id}这个接口的业务。通过休眠模拟一个延迟时间:

image-20210716150234787

此时,orderId=101的订单,关联的是id为1的用户,调用时长为60ms:

image-20210716150510956

orderId=102的订单,关联的是id为2的用户,调用时长为非常短;

image-20210716150605208

2)设置熔断规则

下面,给feign接口设置降级规则:

image-20210716150654094

规则:

image-20210716150740434

超过50ms的请求都会被认为是慢请求

3)测试

在浏览器访问:http://localhost:8088/order/101,快速刷新5次,可以发现:

image-20210716150911004

触发了熔断,请求时长缩短至5ms,快速失败了,并且走降级逻辑,返回的null

在浏览器访问:http://localhost:8088/order/102,竟然也被熔断了:

image-20210716151107785

异常比例

异常比例或异常数:统计指定时间内的调用,如果调用次数超过指定请求数,并且出现异常的比例达到设定的比例阈值(或超过指定异常数),则触发熔断。

例如,一个异常比例设置:

image-20210716131430682

解读:统计最近1000ms内的请求,如果请求量超过10次,并且异常比例不低于0.4,则触发熔断。

一个异常数设置:

image-20210716131522912

解读:统计最近1000ms内的请求,如果请求量超过10次,并且异常比例不低于2次,则触发熔断。

总结

Sentinel熔断降级的策略有哪些?


授权规则

授权规则可以对调用方的来源做控制,有白名单和黑名单两种方式。

点击左侧菜单的授权,可以看到授权规则:

image-20210716152010750

比如:

image-20210716152349191

我们允许请求从gateway到order-service,不允许浏览器访问order-service,那么白名单中就要填写网关的来源名称(origin)

如何获取origin

Sentinel是通过RequestOriginParser这个接口的parseOrigin来获取请求的来源的。

public interface RequestOriginParser {
    /**
     * 从请求request对象中获取origin,获取方式自定义
     */
    String parseOrigin(HttpServletRequest request);
}

这个方法的作用就是从request对象中,获取请求者的origin值并返回。

默认情况下,sentinel不管请求者从哪里来,返回值永远是default,也就是说一切请求的来源都被认为是一样的值default。

因此,我们需要自定义这个接口的实现,让不同的请求,返回不同的origin

@Component
public class HeaderOriginParser implements RequestOriginParser {
    @Override
    public String parseOrigin(HttpServletRequest request) {
        //获取请求头信息
        String origin = request.getHeader("origin");
        //origin非空
        if (StringUtils.isEmpty(origin)){
            origin="balck";
        }
        return origin;
    }
}

使用网关添加请求头信息

- AddRequestHeader=origin,hello # 添加请求头

这样就只有网关过来的请求可以正常调用了

Blocked by Sentinel (flow limiting)


自定义异常结果

默认情况下,发生限流、降级、授权拦截时,都会抛出异常到调用方。异常结果都是flow limmiting(限流)。这样不够友好,无法得知是限流还是降级还是授权拦截。

而如果要自定义异常时的返回结果,需要实现BlockExceptionHandler接口:

public interface BlockExceptionHandler {
    /**
     * 处理请求被限流、降级、授权拦截时抛出的异常:BlockException
     */
    void handle(HttpServletRequest request, HttpServletResponse response, BlockException e) throws Exception;
}

这个方法有三个参数:

这里的BlockException包含多个不同的子类:

异常 说明
FlowException 限流异常
ParamFlowException 热点参数限流的异常
DegradeException 降级异常
AuthorityException 授权规则异常
SystemBlockException 系统规则异常

案例

@Component
public class SentinelExceptionHandler implements BlockExceptionHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, BlockException e) throws Exception {
        String msg = "未知异常";
        int status = 429;

        if (e instanceof FlowException) {
            msg = "请求被限流了";
        } else if (e instanceof ParamFlowException) {
            msg = "请求被热点参数限流";
        } else if (e instanceof DegradeException) {
            msg = "请求被降级了";
        } else if (e instanceof AuthorityException) {
            msg = "没有权限访问";
            status = 401;
        }

        response.setContentType("application/json;charset=utf-8");
        response.setStatus(status);
        response.getWriter().println("{\"msg\": " + msg + ", \"status\": " + status + "}");
    }
}

规则持久化

规则是否能持久化,取决于规则管理模式,sentinel支持三种规则管理模式:


pull模式

pull模式:控制台将配置的规则推送到Sentinel客户端,而客户端会将配置规则保存在本地文件或数据库中。以后会定时去本地文件或数据库中查询,更新本地规则。

image-20210716154155238


push模式

push模式:控制台将配置规则推送到远程配置中心,例如Nacos。Sentinel客户端监听Nacos,获取配置变更的推送消息,完成本地配置更新。

image-20210716154215456


实现push模式

要改源码。。。算了算了。


分步式事务

分布式事务,就是指不是在单个服务或单个数据库架构下,产生的事务,例如:

在数据库水平拆分、服务垂直拆分之后,一个业务操作通常要跨多个数据库、服务才能完成。例如电商行业中比较常见的下单付款案例,包括下面几个行为:

完成上面的操作需要访问三个不同的微服务和三个不同的数据库。

image-20210724165338958

订单的创建、库存的扣减、账户扣款在每一个服务和数据库内是一个本地事务,可以保证ACID原则。

但是当我们把三件事情看做一个"业务",要满足保证“业务”的原子性,要么所有操作全部成功,要么全部失败,不允许出现部分成功部分失败的现象,这就是分布式系统下的事务了。

此时ACID难以满足,这是分布式事务要解决的问题


CAP定理

1998年,加州大学的计算机科学家 Eric Brewer 提出,分布式系统有三个指标。

  • Consistency(一致性)
  • Availability(可用性)
  • Partition tolerance (分区容错性)

当P(分区出现时),A(一致性)和C(可用性)不能同时存在。

image-20210724170517944

它们的第一个字母分别是 C、A、P。

Eric Brewer 说,这三个指标不可能同时做到。这个结论就叫做 CAP 定理。

BASE理论

BASE理论是对CAP的一种解决思路,包含三个思想:

CAP定理和Base理论为解决分步式事务提供了理论基础。

而分布式事务最大的问题是各个子事务的一致性问题,因此可以借鉴CAP定理和BASE理论:

•AP模式:各子事务分别执行和提交,允许出现结果不一致,然后采用弥补措施恢复数据即可,实现最终一致。

•CP模式:各个子事务执行后互相等待,同时提交,同时回滚,达成强一致。但事务等待过程中,处于弱可用状态。


小结


Seata

Seata是 2019 年 1 月份蚂蚁金服和阿里巴巴共同开源的分布式事务解决方案。致力于提供高性能和简单易用的分布式事务服务,为用户打造一站式的分布式解决方案。

Seata事务管理中有三个重要的角色:

TC (Transaction Coordinator) - 事务协调者:维护全局和分支事务的状态,协调全局事务提交或回滚。

TM (Transaction Manager) - 事务管理器:定义全局事务的范围、开始全局事务、提交或回滚全局事务。

RM (Resource Manager) - 资源管理器:管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。

Seata基于上述架构提供了四种不同的分布式事务解决方案:

无论哪种方案,都离不开TC,也就是事务的协调者。


整合Seata

<!--seata-->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
    <exclusions>
        <!--版本较低,1.3.0,因此排除--> 
        <exclusion>
            <artifactId>seata-spring-boot-starter</artifactId>
            <groupId>io.seata</groupId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>io.seata</groupId>
    <artifactId>seata-spring-boot-starter</artifactId>
    <!--seata starter 采用1.4.2版本-->
    <version>${seata.version}</version>
</dependency>
seata:
  registry: # TC服务注册中心的配置,微服务根据这些信息去注册中心获取tc服务地址
    type: nacos # 注册中心类型 nacos
    nacos:
      server-addr: 127.0.0.1:8848 # nacos地址
      namespace: "" # namespace,默认为空
      group: DEFAULT_GROUP # 分组,默认是DEFAULT_GROUP
      application: seata-tc-server # seata服务名称
      username: nacos
      password: nacos
  tx-service-group: seata-demo # 事务组名称
  service:
    vgroup-mapping: # 事务组与cluster的映射关系
      seata-demo: SH

微服务如何根据这些配置寻找TC的地址呢?

我们知道注册到Nacos中的微服务,确定一个具体实例需要四个信息:

以上四个信息,在刚才的yaml文件中都能找到:

image-20210724173654258

namespace为空,就是默认的public

结合起来,TC服务的信息就是:public@DEFAULT_GROUP@seata-tc-server@SH,这样就能确定TC服务集群了。然后就可以去Nacos拉取对应的实例信息了。


XA模式

XA 规范 是 X/Open 组织定义的分布式事务处理(DTP,Distributed Transaction Processing)标准,XA 规范 描述了全局的TM与局部的RM之间的接口,几乎所有主流的数据库都对 XA 规范 提供了支持。

正常情况:

image-20210724174102768

异常情况:

image-20210724174234987

一阶段:

二阶段:


优点和缺点


实现步骤


AT模式

AT模式同样是分阶段提交的事务模型,不过缺弥补了XA模型中资源锁定周期过长的缺陷。

基本流程图:

image-20210724175327511

阶段一RM的工作:

阶段二提交时RM的工作:

阶段二回滚时RM的工作:


AT与XA的区别

简述AT模式与XA模式最大的区别是什么?


防脏写问题

在多线程并发访问AT模式的分布式事务时,有可能出现脏写问题,如图:

image-20210724181541234

解决思路就是引入了全局锁的概念。在释放DB锁之前,先拿到全局锁。避免同一时刻有另外一个事务来操作当前数据。

image-20210724181843029

在非常极端的情况,由非seata管理的全局事务,修改了被全局锁锁住的字段。

seata会将修改后的快照和回滚后的快照进行对比。

如果不同,则发送警告,由人工介入。

这是一种非常极端的情况,有多种方法可以规避。在业务中,更新数据库中必须使用由seata管理的全局事务等等。

image-20220404003507335


优缺点

AT模式的优点:

AT模式的缺点:


实现AT模式

AT模式中的快照生成、回滚等动作都是由框架自动完成,没有任何代码侵入,因此实现非常简单。

只不过,AT模式需要一个表来记录全局锁、另一张表来记录数据快照undo_log。

1)导入数据库表,记录全局锁

导入Sql文件:seata-at.sql,其中lock_table导入到TC服务关联的数据库,undo_log表导入到微服务关联的数据库:

image-20210724182217272

2)修改application.yml文件,将事务模式修改为AT模式即可:

seata:
  data-source-proxy-mode: AT # 默认就是AT

3)重启服务并测试

标签:事务,调用,服务,请求,模式,熔断,分步,jdk8,异常
来源: https://www.cnblogs.com/Boerk/p/16098215.html