分布式工具的作用

技术结合业务来实现

  1. NGINX 主要用于反向代理和负载均衡,起到解决跨域的作用(可以对zuul进行负载均衡);
  2. zuul 在前后端之间建立网关,通过从eureka获取的路由,解决前后端之间的负载均衡;
  3. eureka 解决分布式服务的注册和管理;
  4. fegin 整合了ribbon和httpclient功能;
    1. ribbon 解决后端服务之间的负载均衡;
    2. httpclient 解决后端服务之间相互访问;
  5. config:进行统一配置的管理
  6. Hystrix:服务之间相互访问时的保护措施。在调用方声明开启降级机制,在被调用方开启熔断机制。
  7. redis:将数据库中的信息存储到redis中缓存,方便用户快速获取到信息。(可以充当消息队列)
  8. elasticSearch:搜索引擎,将数据库中的信息存储到搜索引擎中,方便用户从大连数据中快速查询到所需要的信息
  9. RabbitMQ:消息队列,将数据库的信息存储到消息队列中,通过异步的方式,使用户更快地得到反馈。(应用解耦,日志处理,削峰填谷,消息通讯)

eurake

概述

eurake起到的作用是服务的管理和注册

eurake为每一个服务都提供一个唯一表示,如果标识重复,则表示该服务搭建了集群。

eurake集群模式下,服务会相互复制。

注意

  1. 每个服务的唯一表示不能重复!spring.aplication.name不能重复!
  2. 默认的ip地址是自己本机的地址,并不是IPV6的地址,可以进行配置1,将两个服务交给eurake
  3. 一个服务调用用一个服务时,首先当前服务从eureka上获取到被调用服务的可用服务列表,获取大ip:port。使用restTemplate发起远程调用。

心跳机制

每隔90秒时间,客户端会向服务端发送心跳。保证自己还存活。死了就剔除。

保护机制

自我保护机制是eureka的一种防止客户端因为网络状况导致不可访问的情况的一种机制,多个服务同时掉线时,不剔除。

使用

导包

<!--eurake server的依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
<!--eurake client的依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

注解启动类

@SpringBootApplication
@EnableEurekaServer
public class ShopEurekaApplication {

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

}

配置文件

yml

server:
port: 8080

spring:
application:
name: shop-eurake

eureka:
client:
fetch-registry: false
register-with-eurake: false
service-url:
defaultZone: http://localhost:8080/eureka
server:
#关闭自我保护
enable-self-preservation: false

properties

#配置端口号
server.port=8000

#eureka的配置
#代表是否要将自己注册为eureka服务端。默认为true,当单机模式时,改为false,搭建集群式改为true
eureka.client.register-with-eureka=false
#是否从eureka服务端获取到注册信息 默认为true,当单机模式时,改为false,搭建集群式改为true
eureka.client.fetch-registry=false
#设置于eureka server交互地址,因为eureka的默认为 8079的端口号,将默认的地址改为自己的本机的 ip:port/eureka
eureka.client.service-url.defaultZone: http://localhost:8000/eureka/
#将自我保护机制关闭 注意:自我保护机制是eureka的一种防止客户端因为网络状况 导致不可访问的情况的一种机制,
#主要作用是不将不可访问你的服务剔除出服务列表
eureka.server.enable-self-preservation=false
#心跳机制:每隔90秒时间,客户端会向服务端发送心跳。保证自己还存活,eureka服务端不会剔除掉该服务。
#eureka.server.eviction-interval-timer-in-ms=1000

Feign

概述

fegin集成了httpclient和ribbon,用于远程调用和负载均衡

工作流程

  1. client1服务将自己注册到注册中心。

  2. client2从注册中心获取client1服务的地址。

  3. client2远程调用client1服务

HttpClient

概述

用于远程调用

shsu

特性

  1. 基于标准、纯净的java语言。实现了Http1.0和Http1.1

  2. 以可扩展的面向对象的结构实现了Http全部的方法(GET, POST, PUT, DELETE, HEAD, OPTIONS, and TRACE)。

  3. 支持HTTPS协议。

  4. 通过Http代理建立透明的连接。

  5. 利用CONNECT方法通过Http代理建立隧道的https连接。

  6. Basic, Digest, NTLMv1, NTLMv2, NTLM2 Session, SNPNEGO/Kerberos认证方案。

  7. 插件式的自定义认证方案。

  8. 便携可靠的套接字工厂使它更容易的使用第三方解决方案。

  9. 连接管理器支持多线程应用。支持设置最大连接数,同时支持设置每个主机的最大连接数,发现并关闭过期的连接。

  10. 自动处理Set-Cookie中的Cookie。

  11. 插件式的自定义Cookie策略。

  12. Request的输出流可以避免流中内容直接缓冲到socket服务器。

  13. Response的输入流可以有效的从socket服务器直接读取相应内容。

  14. 在http1.0和http1.1中利用KeepAlive保持持久连接。

  15. 直接获取服务器发送的response code和 headers。

  16. 设置连接超时的能力。

  17. 实验性的支持http1.1 response caching。

  18. 源代码基于Apache License 可免费获取。

使用步骤

  1. 创建HttpClient对象。

  2. 创建请求方法的实例,并指定请求URL。如果需要发送GET请求,创建HttpGet对象;如果需要发送POST请求,创建HttpPost对象。

  3. 如果需要发送请求参数,可调用HttpGet、HttpPost共同的setParams(HttpParams params)方法来添加请求参数;对于HttpPost对象而言,也可调用setEntity(HttpEntity entity)方法来设置请求参数。

  4. 调用HttpClient对象的execute(HttpUriRequest request)发送请求,该方法返回一个HttpResponse。

  5. 调用HttpResponse的getAllHeaders()、getHeaders(String name)等方法可获取服务器的响应头;调用HttpResponse的getEntity()方法可获取HttpEntity对象,该对象包装了服务器的响应内容。程序可通过该对象获取服务器的响应内容。

  6. 释放连接。无论执行方法是否成功,都必须释放连接

测试

导包
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.5</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.47</version>
</dependency>
HttpClient中的Get请求
@Test
public void testGet(){

// 获得Http客户端(可以理解为:你得先有一个浏览器;注意:实际上HttpClient与浏览器是不一样的)
CloseableHttpClient httpClient = HttpClientBuilder.create().build();

// 创建Get请求
HttpGet httpGet = new HttpGet("http://localhost:8083/user/findAll/1/10");

// 响应模型
CloseableHttpResponse response = null;
try {

// 由客户端执行(发送)Get请求
response = httpClient.execute(httpGet);
// 从响应模型中获取响应实体
HttpEntity responseEntity = response.getEntity();
System.out.println("响应状态为:" + response.getStatusLine());
if (responseEntity != null) {
System.out.println("响应内容长度为:" + responseEntity.getContentLength());
System.out.println("响应内容为:" + EntityUtils.toString(responseEntity));
}

} catch (ClientProtocolException e) {
e.printStackTrace();
} catch (ParseException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
// 释放资源
if (httpClient != null) {
httpClient.close();
}
if (response != null) {
response.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
HttpClient中的Post请求
 @Test
    public void testPost(){
    
    // 获得Http客户端(可以理解为:你得先有一个浏览器;注意:实际上HttpClient与浏览器是不一样的)
    CloseableHttpClient httpClient = HttpClientBuilder.create().build();

    // 参数
    StringBuffer params = new StringBuffer();
    
    try {
        // 字符数据最好encoding以下;这样一来,某些特殊字符才能传过去(如:某人的名字就是“&”,不encoding的话,传不过去)
        params.append("sendTo=" + URLEncoder.encode("541109497@qq.com", "utf-8"));
    } catch (UnsupportedEncodingException e1) {
        e1.printStackTrace();
    }

    // 创建Post请求
    HttpPost httpPost = new HttpPost("http://localhost:8083/user/senEmail" + "?" + params);

    // 设置ContentType(注:如果只是传普通参数的话,ContentType不一定非要用application/json)
    httpPost.setHeader("Content-Type", "application/json;charset=utf8");

    // 响应模型
    CloseableHttpResponse response = null;
    try {
        
        // 由客户端执行(发送)Post请求
        response = httpClient.execute(httpPost);
        // 从响应模型中获取响应实体
        HttpEntity responseEntity = response.getEntity();

        System.out.println("响应状态为:" + response.getStatusLine());
        if (responseEntity != null) {
            System.out.println("响应内容长度为:" + responseEntity.getContentLength());
            System.out.println("响应内容为:" + EntityUtils.toString(responseEntity));
        }
        
    } catch (ClientProtocolException e) {
        e.printStackTrace();
    } catch (ParseException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        try {
            // 释放资源
            if (httpClient != null) {
                httpClient.close();
            }
            if (response != null) {
                response.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
@Test
public void testPost(){
    CloseableHttpClient httpclient = HttpClientBuilder.create().build();
    
    HttpPost httpPost = new HttpPost("http://localhost:8081/save");

    Map param = new HashMap<>();

    param.put("id",1);
    param.put("username","王五");

    Object obj = JSONObject.toJSON(param);

    System.out.println(obj);
    //设置参数
    StringEntity stringEntity = new StringEntity(obj.toString(),"utf-8");
    //将参数设置到httppost请求中
    httpPost.setEntity(stringEntity);
    //设置请求头
    httpPost.setHeader("Content-Type","application/json;charset=utf8");

    CloseableHttpResponse res=null;
    try {
        res  = httpclient.execute(httpPost);

        System.out.println("返回状态:"+res.getStatusLine());
        System.out.println("返回值:"+EntityUtils.toString(res.getEntity()));
    } catch (IOException e) {
        e.printStackTrace();
    }finally {
        try {
            httpclient.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

}

Ribbon

概述

用于客户端的负载均衡

客户端负载均衡与服务端负载均衡的区别在于客户端要维护一份服务列表,Ribbon从Eureka Server获取服务列表,Ribbon根据负载均衡算法直接请求到具体的微服务,中间省去了负载均衡服务。

image-20220315201739117

  1. 在消费微服务中使用Ribbon实现负载均衡,Ribbon先从EurekaServer中获取服务列表。

  2. Ribbon根据负载均衡的算法去调用微服务。

使用

导包
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-ribbon</artifactId>
<version>1.1.5.RELEASE</version>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>3.9.1</version>
</dependency>
参数配置
ribbon:
MaxAutoRetries: 2 #最大重试次数,当Eureka中可以找到服务,但是服务连不上时将会重试
MaxAutoRetriesNextServer: 3 #切换实例的重试次数
OkToRetryOnAllOperations: false #对所有操作请求都进行重试,如果是get则可以,如果是post,put等操作没有实现幂等的情况下是很危险的,所以设置为false
ConnectTimeout: 5000 #请求连接的超时时间
ReadTimeout: 6000 #请求处理的超时时间
代码测试
  1. 启动两个client服务,注意端口要不一致

    启动完成观察Eureka Server的服务列表

  2. 定义RestTemplate,使用@LoadBalanced注解

    在client2的启动类中定义RestTemplate

    @Bean
    @LoadBalanced
    public RestTemplate restTemplate() {
    return new RestTemplate(new OkHttp3ClientHttpRequestFactory());
    }
  3. 测试代码

    在client2工程创建单元测试代码,远程调用test-producer的查询页面接口:

     @Autowired
    RestTemplate restTemplate;
    //负载均衡调用
    String serviceId ="TEST-PRODUCER1";
    for(int i=0;i<10;i++){
    //通过服务id调用
    String forObject = restTemplate.getForObject("http://" + serviceId + "/test/getString", String.class);

    System.out.println(forObject);
    }
  4. 结果

    添加@LoadBalanced注解后,restTemplate会走LoadBalancerInterceptor拦截器,此拦截器中会通过RibbonLoadBalancerClient查询服务地址,可以在此类打断点观察每次调用的服务地址和端口,两个client服务会轮流被调用。

    image-20220315202727619

使用

依赖

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

定义接口

@FeignClient(value = "TEST-PRODUCER1")
public interface ClientInterface {
//远程调用client服务中的test01接口
@GetMapping("/test/getString")//标识远程调用的http的方法类型是什么
public String test01();
}

注解

启动类添加@EnableFeignClients注解

测试

@SpringBootTest
@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
public class TestFeignClient {
@Autowired
private ClientInterface clientInterface;

@Test
public void test01(){
String s = clientInterface.test01();
System.out.println(s);
}
}

工作原理

  1. 启动类添加@EnableFeignClients注解,Spring会扫描标记了@FeignClient注解的接口,并生成此接口的代理对象

  2. @FeignClient(value = “TEST-CLIENT1”)即指定了cms的服务名称,Feign会从注册中心获取cms服务列表,并通过负载均衡算法进行服务调用。

  3. 在接口方法 中使用注解 @GetMapping(“/test/getString”),指定调用的url,Feign将根据url进行远程调用。

注意

SpringCloud对Feign进行了增强兼容了SpringMVC的注解 ,我们在使用SpringMVC的注解时需要注意:

  1. feignClient接口 有参数在参数必须加@PathVariable(“XXX”)和@RequestParam(“XXX”)

  2. feignClient返回值为复杂对象时其类型必须有无参构造函数。

实例

被调用方:orderController

@RequestMapping("/order")
@RestController
public class OrderController {

@Autowired
OrderService orderService;

/**
* 查询全部
*
* @param page
* @param size
* @return
*/
@RequestMapping("/findAll/{page}/{size}")
public BaseResp findAll(@PathVariable("page") Integer page, @PathVariable("size") Integer size) {
return orderService.findAll(page, size);
}

/**
* 新增
*
* @param order
* @return
*/
@RequestMapping("/save")
public BaseResp save(@RequestBody Order order) {
return orderService.save(order);
}
}

调用方:userController

@RequestMapping("/user")
@RestController
public class UserController {

@Autowired
OrderClients orderClients;

@RequestMapping("/findAll/{page}/{size}")
public BaseResp findAll(@PathVariable("page")Integer page, @PathVariable("size")Integer size){
return orderClients.findAll(page,size);
}

@RequestMapping("/save")
public BaseResp save(@RequestBody Order order){
return orderClients.save(order);
}

}

OrderClient

@FeignClient(name = "shop-order",path = "/order")
public interface OrderClients {

@RequestMapping("/findAll/{page}/{size}")
public BaseResp findAll(@PathVariable("page") Integer page, @PathVariable("size") Integer size);

@RequestMapping("/save")
public BaseResp save(@RequestBody Order order);
}

image-20220315232204466

调用方和被调用方必须完全一致!!!

Zuul

概述

一个网关,帮助前后端交互

主要用于提供动态路由、监控、安全控制、限流配额等,可以将内部微服务API统一暴露

作用

  1. 提供动态的路由
  2. 实现安全控制
  3. 将内部微服务的API统一暴露

前端通过NGINX访问zuul,Zuul通过从eureka拿到当前服务为唯一标识,并自动请求对应的端口。

使用

导包

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>

配置文件

用的是重定向!!!

spring.application.name=gateway-service-zuul
server.port=8005

#这里的配置表示,访问/it/** 直接重定向到http://www.1000phone.com/
zuul.routes.baidu.path=/it/**
zuul.routes.baidu.url=http://www.1000phone.com/

注解

@SpringBootApplication
@EnableZuulProxy
public class GatewayServiceZuulApplication {

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

测试

此时访问/it/**就会访问到http://www.1000phone.com/。

服务化

吧zuul注册到eureka上,zuul通过eureka中服务的唯一标识获取服务的ip和端口号

依赖

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
<version>1.3.1.RELEASE</version>
</dependency>

配置文件

spring.application.name=gateway-service-zuul
server.port=8005

#下面的内容也可以不写
#这里的配置表示,访问/it/** 直接重定向到http://www.mobiletrain.org/
#zuul.routes.baidu.path=/it/**
#zuul.routes.baidu.url=http://www.mobiletrain.org/
#zuul.routes.hello.path=/test/**
#zuul.routes.hello.url=http://localhost:8003/


zuul.routes.api-a.path=/producer/**
zuul.routes.api-a.serviceId=spring-cloud-producer
eureka.client.serviceUrl.defaultZone=http://localhost:8000/eureka/

路由规则

但是如果后端服务多达十几个的时候,每一个都这样配置也挺麻烦的,spring cloud zuul已经帮我们做了默认配置。默认情况下,Zuul会代理所有注册到Eureka Server的微服务,并且Zuul的路由规则如下:http://ZUUL_HOST:ZUUL_PORT/微服务在Eureka上的serviceId/**会被转发到serviceId对应的微服务。

我们注销掉gateway-service-zuul-eureka项目中关于路由的配置:

#zuul.routes.api-a.path=/producer/**
#zuul.routes.api-a.serviceId=test-producer

重新启动后,访问http://localhost:8005/test-producer/producer/getString,测试返回结果和上述示例相同,说明spirng cloud zuul默认已经提供了转发功能。

注意

zuul的负载均衡是对应前后端之间的,fegin的负载均衡是后端与后端之间得到。二者不一样!!!

Hystrix

概述

在调用方声明开启降级机制,在被调用方开启熔断机制

如果被调用方的服务挂了,导致线程大量堆积,为了防止雪崩,自动走降级方法;

如果被调用方的服务没挂,接口异常/接口处理时间过长导致线程大量堆积,为了防止雪崩,走熔断方法

服务熔断

服务熔断的作用类似于我们家用的保险丝,当某服务出现不可用或响应超时的情况时,为了防止整个系统出现雪崩,暂时停止对该服务的调用

服务降级

服务降级是从整个系统的负荷情况出发和考虑的,对某些负荷会比较高的情况,为了预防某些功能(业务场景)出现负荷过载或者响应慢的情况,在其内部暂时舍弃对一些非核心的接口和数据的请求,而直接返回一个提前准备好的fallback(退路)错误处理信息。这样,虽然提供的是一个有损的服务,但却保证了整个系统的稳定性和可用性。

Hystrix

设计目的

在一段时间内侦测到许多类似的错误,会强迫其以后的多个调用快速失败,不再访问远程服务器,从而防止应用程序不断地尝试执行可能会失败的操作进而导致资源耗尽。这时候Hystrix进行FallBack操作来服务降级,

Fallback相当于是降级操作. 对于查询操作, 我们可以实现一个fallback方法, 当请求后端服务出现异常的时候, 可以使用fallback方法返回的值,一般是设置的默认值或者来自缓存通知后面的请求告知这服务暂时不可用了。

使得应用程序继续执行而不用等待修正错误,或者浪费CPU时间去等到长时间的超时产生。Hystrix熔断器也可以使应用程序能够诊断错误是否已经修正,如果已经修正,应用程序会再次尝试调用操作。

设计原则

  1. 防止单个服务的故障,耗尽整个系统服务的容器(比如tomcat)的线程资源,避免分布式环境里大量级联失败。通过第三方客户端访问(通常是通过网络)依赖服务出现失败、拒绝、超时或短路时执行回退逻辑。

  2. 用快速失败代替排队(每个依赖服务维护一个小的线程池或信号量,当线程池满或信号量满,会立即拒绝服务而不会排队等待)和优雅的服务降级;当依赖服务失效后又恢复正常,快速恢复。

  3. 提供接近实时的监控和警报,从而能够快速发现故障和修复。监控信息包括请求成功,失败(客户端抛出的异常),超时和线程拒绝。如果访问依赖服务的错误百分比超过阈值,断路器会跳闸,此时服务会在一段时间内停止对特定服务的所有请求。

  4. 将所有请求外部系统(或请求依赖服务)封装到HystrixCommand或HystrixObservableCommand对象中,然后这些请求在一个独立的线程中执行。使用隔离技术来限制任何一个依赖的失败对系统的影响。每个依赖服务维护一个小的线程池(或信号量),当线程池满或信号量满,会立即拒绝服务而不会排队等待。

特性

请求熔断

1.请求熔断: 当Hystrix Command请求后端服务失败数量超过一定比例(默认50%), 断路器会切换到开路状态(Open). 这时所有请求会直接失败而不会发送到后端服务. 断路器保持在开路状态一段时间后(默认5秒), 自动切换到半开路状态(HALF-OPEN).

这时会判断下一次请求的返回情况, 如果请求成功, 断路器切回闭路状态(CLOSED), 否则重新切换到开路状态(OPEN). Hystrix的断路器就像我们家庭电路中的保险丝, 一旦后端服务不可用, 断路器会直接切断请求链, 避免发送大量无效请求影响系统吞吐量, 并且断路器有自我检测并恢复的能力.

服务降级

2.服务降级:Fallback相当于是降级操作. 对于查询操作, 我们可以实现一个fallback方法, 当请求后端服务出现异常的时候, 可以使用fallback方法返回的值. fallback方法的返回值一般是设置的默认值或者来自缓存.告知后面的请求服务不可用了,不要再来了。

依赖隔离

3.依赖隔离(采用舱壁模式,Docker就是舱壁模式的一种):在Hystrix中, 主要通过线程池来实现资源隔离. 通常在使用的时候我们会根据调用的远程服务划分出多个线程池。比如说,一个服务调用两外两个服务,你如果调用两个服务都用一个线程池,那么如果一个服务卡在哪里,资源没被释放

后面的请求又来了,导致后面的请求都卡在哪里等待,导致你依赖的A服务把你卡在哪里,耗尽了资源,也导致了你另外一个B服务也不可用了。这时如果依赖隔离,某一个服务调用A B两个服务,如果这时我有100个线程可用,我给A服务分配50个,给B服务分配50个,这样就算A服务挂了,我的B服务依然可以用。

请求缓存

4.请求缓存:比如一个请求过来请求我userId=1的数据,你后面的请求也过来请求同样的数据,这时我不会继续走原来的那条请求链路了,而是把第一次请求缓存过了,把第一次的请求结果返回给后面的请求。

请求合并

5.请求合并:我依赖于某一个服务,我要调用N次,比如说查数据库的时候,我发了N条请求发了N条SQL然后拿到一堆结果,这时候我们可以把多个请求合并成一个请求,发送一个查询多条数据的SQL的请求,这样我们只需查询一次数据库,提升了效率。

流程

  1. 每次调用创建一个新的HystrixCommand,把依赖调用封装在run()方法中.   

  2. 执行execute()/queue做同步或异步调用.   

  3. 判断熔断器(circuit-breaker)是否打开,如果打开跳到步骤8,进行降级策略,如果关闭进入步骤5.   

  4. 判断线程池/队列/信号量是否跑满,如果跑满进入降级步骤8,否则继续后续步骤6.

  5. 调用HystrixCommand的run方法.运行依赖逻辑   

    1. 依赖逻辑调用超时,进入步骤8.

      

  6. 判断逻辑是否调用成功   

    1. 返回成功调用结果   
    2. 调用出错,进入步骤8.

      

  7. 计算熔断器状态,所有的运行状态(成功, 失败, 拒绝,超时)上报给熔断器,用于统计从而判断熔断器状态.   

  8. getFallback()降级逻辑.以下四种情况将触发getFallback调用:

      1. run()方法抛出非HystrixBadRequestException异常。     
      2. run()方法调用超时     
      3. 熔断器开启拦截调用     
      4. 线程池/队列/信号量是否跑满

        

    1. a:没有实现getFallback的Command将直接抛出异常   
    2. b:fallback降级逻辑调用成功直接返回   
    3. c:降级逻辑调用失败抛出异常

      

  9. 返回执行成功结果

这里接着前面的Ribbon进行Hystrix集成。说白了你想对一个请求进行熔断,必然不能让客户直接去调用那个请求,你必然要要对别人的请求进行包装一层和拦截,才能做点手脚,比如进行熔断,所以说要在Ribbon上动手脚。因为它是请求发起的地方。

我们刚开始请求一个服务,为了负载均衡进行了拦截一次,现在我们要进行熔断,所以必须跟Ribbon集成一次,再进行请求拦截来熔断。

使用

导包

<!-- Hystrix 依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>

启动类注解

在ProducerApplication启动类上增加@EnableCircuitBreaker注解

修改controller(调用端)

@Controller
@RequestMapping("/test")
public class TestController {

@RequestMapping("/getString")
@HystrixCommand(fallbackMethod = "getMsgFallback")
@ResponseBody
public String test01(){
int i =1/0;
return "你好,调用成功";
}

public String getMsgFallback() {
return "祝您 2019 猪年大吉,'猪'事如意!";
}
}

我们发现调用成功后,打印了fallback的方法,也就是当前业务产生了错误,会自动调用fallbackMethod 方法来进行熔断。

注意:被调用方的方法有参数的话,必须在fallbackMethod的参数中一一对应起来

Feign结合Hystrix

导包

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>

消费启动类开启@EnableCircuitBreaker

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

@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate(new OkHttp3ClientHttpRequestFactory());
}
}

配置文件

feign.hystrix.enabled=true
#hystric默认请求超过1秒未响应就降级,配置为3秒未响应再降级
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=3000

指定处理类

通过配置@FeignClient注解的fallback属性来位MessageServiceClient指定一个自定义的fallback处理类(MessageServiceFallback)。

@FeignClient(value = "TEST-CLIENT1",fallback = MessageServiceFallback.class)
public interface ClientInterface {
//远程调用client服务中的test01接口
@GetMapping("/test/getString")//标识远程调用的http的方法类型是什么
public String test01();
}

创建Fallback处理类

@Component
public class MessageServiceFallback implements ClientInterface {

@Override
public String test01() {
System.out.println("调用消息接口失败,对其进行降级处理!");
return "消息接口繁忙,请稍后重试!";
}
}

实例

调用方(降级处理):

@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients
//开启降级机制
@EnableHystrix
public class QfliveShopcardApplication {

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

}
@FeignClient(name = "qflive-course", path = "/course",fallback = CourseClientFallBack.class)
public interface CourseClient {

@RequestMapping("/findCourseDetailById/{id}")
public BaseResp findCourseDetailById(@PathVariable("id") Integer id);

}
@Component
public class CourseClientFallBack implements CourseClient{
@Override
public BaseResp findCourseDetailById(Integer id) {
System.out.println("course服务不可用,直接进入到降级方法中,走预定好的退路方法!");
return new BaseResp(0,"当前服务不可用!",null,null);
}
}

配置文件:

properties

#ribbon连接的超时时长 以及 处理时长
ribbon.ConnectTimeout: 5000
ribbon.ReadTimeout: 5000

#开启feign与Hystrix的结合
feign.hystrix.enabled: true
#将hystrix默认的超时时长更改为5
hystrix.command.default.execution.timeout.enabled: true
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds: 5000

yml

#开启feign与Hystrix的结合
feign:
hystrix:
enabled: true
#将hystrix默认的超时时长更改为5
hystrix:
command:
default:
execution:
timeout:
enabled: true
isolation:
thread:
timeoutInMilliseconds: 5000
#ribbon连接的超时时长 以及 处理时长
ribbon:
ConnectTimeout: 5000
ReadTimeout: 5000

对被调用方(熔断处理):

@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients
//开启熔断
@EnableCircuitBreaker
public class QfliveCourseApplication {

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

}

接口出现异常时

@RequestMapping("/findCourseDetailById/{id}")
//当接口出现了异常时,直接走快速的失败方法。
@HystrixCommand(fallbackMethod = "findCourseDetailByIdFallBack")
public BaseResp findCourseDetailById(@PathVariable("id") Integer id) {
return courseService.findCourseDetailById(id);
}

/**
* 熔断的退路方法要与接口方法保持一致。返回值,参数必须一致。
* 1.当接口出现了异常时,直接走快速的失败方法。
* 2.当接口处理时间过长时 导致线程的大量堆积,就有可能导致我们的服务的资源耗尽,导致宕机
* @param id
* @return
*/
public BaseResp findCourseDetailByIdFallBack(@PathVariable("id")Integer id){
System.out.println("课程id:"+id+"的接口请求出现了异常!,快速熔断失败!");
return new BaseResp(0,"快速失败!",null,null);
}

当接口超时时间过长时

@RequestMapping("/findCourseDetailById/{id}")

//当接口处理时间过长时 导致线程的大量堆积,就有可能导致我们的服务的资源耗尽,导致宕机
@HystrixCommand(commandProperties = {
//策略使用thread的线程池
@HystrixProperty(name="execution.isolation.strategy",value = "THREAD"),
//当前线程的超时时间,如果超过配置的时间,则直接熔断
@HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds",value = "3000"),
},fallbackMethod = "findCourseDetailByIdFallBack")
public BaseResp findCourseDetailById(@PathVariable("id") Integer id) {
return courseService.findCourseDetailById(id);
}

/**
* 熔断的退路方法要与接口方法保持一致。返回值,参数必须一致。
* 1.当接口出现了异常时,直接走快速的失败方法。
* 2.当接口处理时间过长时 导致线程的大量堆积,就有可能导致我们的服务的资源耗尽,导致宕机
* @param id
* @return
*/
public BaseResp findCourseDetailByIdFallBack(@PathVariable("id")Integer id){
System.out.println("课程id:"+id+"的接口请求出现了异常!,快速熔断失败!");
return new BaseResp(0,"快速失败!",null,null);
}

短路器:

三种状态:

  1. open:
    1. 表示断路器打开,打开后接口直接失败,不再继续请求;
  2. halfopen
    1. 每个5秒的时间,为半开的状态,会放行一个请求到接口中,如果能够拿到结果,则将断路器关闭。如果还有问题则继续open;
  3. close
    1. 断路器关闭状态,所有的都可以访问我们的接口。
@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients
//开启熔断
@EnableCircuitBreaker
//开启断路器
@EnableHystrixDashboard
public class QfliveCourseApplication {

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

}
//配置断路器
@HystrixCommand(fallbackMethod = "findCourseDetailByIdFallBack",commandProperties = {
@HystrixProperty(name="execution.isolation.strategy",value = "THREAD"),
//当前线程的超时时间,如果超过配置的时间,则直接熔断
@HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds",value = "3000"),
@HystrixProperty(name="circuitBreaker.enabled",value = "true"),
//失败阀值的总请求数
@HystrixProperty(name="circuitBreaker.requestVolumeThreshold",value = "10"),
//总请求数的失败率达到%时
@HystrixProperty(name="circuitBreaker.errorThresholdPercentage",value = "50"),
//open状态后,多少秒拒绝请求
@HystrixProperty(name="circuitBreaker.sleepWindowInMilliseconds",value = "5000"),
})
public BaseResp findCourseDetailById(@PathVariable("id") Integer id) {
return courseService.findCourseDetailById(id);
}

Config

概述

Spring Cloud Config项目是一个解决分布式系统的配置管理方案。它包含了Client和Server两个部分,server提供配置文件的存储、以接口的形式将配置文件的内容提供出去,client通过接口获取数据、并依据此数据初始化自己的应用。

使用

Server端配置

导包

 <dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-server</artifactId>
</dependency>
</dependencies>

配置application.yml

本地

server:
port: 端口号
spring:
application:
name: 服务名
profiles:
active: native # 配置使用本地储存
cloud:
config:
server:
native:
search-locations: classpath:properties/ # 搜索src/main/resource 下的propertie 文件夹下的文件
eureka:
client:
serviceUrl:
defaultZone: http://admin:123456@localhost:8000/eureka/ #eureka服务器的网址

远程

server:
port: 8200
spring:
application:
name: tk-config
cloud:
config:
server:
git:
uri: https://gitee.com/water_00000001/config.git
search-paths: config

eureka:
client:
service-url:
defaultZone: http://localhost:8000/eureka

在resources下的配置文件(查看gittee的配置文件)

config-dev.properties/yml


comfig-pro.properties/yml


config-test.properties/yml


启动类

启动类添加@EnableConfigServer,激活对配置中心的支持

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

Client端配置

导包

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>

配置文件

bootstrap.properties/yml

server:
port: 8501
spring:
application:
name: tk-shop
cloud:
config:
name: xm-config #配置文件的名称
profile: dev #配置文件的环境
discovery:
enabled: true #从eureka获取到config服务的地址
service-id: tk-config

eureka:
client:
service-url:
defaultZone: http://localhost:8000/eureka
spring.application.name:对应{application}部分
spring.cloud.config.profile:对应{profile}部分
spring.cloud.config.label:对应git的分支。如果配置中心使用的是本地存储,则该参数无用
spring.cloud.config.uri:配置中心的具体地址
spring.cloud.config.discovery.service-id:指定配置中心的service-id,便于扩展为高可用配置集群。

启动类

@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class TkDistributionApplication {

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

}

开启更新机制

导包

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

增加了spring-boot-starter-actuator包,spring-boot-starter-actuator是一套监控的功能,可以监控程序在运行时状态,其中就包括/refresh的功能。

注解

@RestController
@RefreshScope // 使用该注解的类,会在接到SpringCloud配置中心配置刷新的时候,自动将新的配置更新到该类对应的字段中。
class HelloController {

@Value("${neo.hello}")
private String hello;

@RequestMapping("/hello")
public String from() {
return this.hello;
}
}

需要给加载变量的类上面加载@RefreshScope,在客户端执行/refresh的时候就会更新此类下面的变量值。

@RefreshScope // 使用该注解的类,会在接到SpringCloud配置中心配置刷新的时候,自动将新的配置更新到该类对应的字段中。

测试

Spring boot 2.0的改动较大,/bus/refresh全部整合到actuador里面了,所以之前1.x的management.security.enabled全部失效,不适用于2.0
适用于2.0的配置是这样的:

management.endpoints.web.exposure.include=refresh

高可用环境搭配

server端改造

pom文件改造

<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
</dependencies>

需要多引入spring-cloud-starter-netflix-eureka-client包,来添加对eureka的支持。

配置文件改造

server:
server:
port: 8001
spring:
application:
name: spring-cloud-config-server
cloud:
config:
server:
git:
uri: https://github.com/ityouknow/spring-cloud-starter/ # 配置git仓库的地址
search-paths: config-repo # git仓库地址下的相对地址,可以配置多个,用,分割。
username: username # git仓库的账号
password: password # git仓库的密码
eureka:
client:
serviceUrl:
defaultZone: http://admin:123456@localhost:8000/eureka/ ## 注册中心eurka地址

增加了eureka注册中心的配置

启动类

启动类添加@EnableDiscoveryClient激活对配置中心的支持

/**
* Created by 54110 on 2019-07-01.
*/
@EnableConfigServer
@SpringBootApplication
@EnableDiscoveryClient
public class ConfigServerApplication {

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

这样server端的改造就完成了。先启动eureka注册中心,在启动server端,在浏览器中访问:http://localhost:8000/ 就会看到server端已经注册了到注册中心了。

1561965503657

客户端改造

添加依赖

<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

需要多引入spring-cloud-starter-netflix-eureka-client包,来添加对eureka的支持。

修改bootstrap.properties配置文件

spring.cloud.config.name=neo-config
spring.cloud.config.profile=dev
spring.cloud.config.label=master

spring.cloud.config.discovery.enabled=true
spring.cloud.config.discovery.serviceId=spring-cloud-config-server
eureka.client.serviceUrl.defaultZone=http://admin:123456@localhost:8000/eureka/

主要是去掉了spring.cloud.config.uri直接指向server端地址的配置,增加了最后的三个配置:

  • spring.cloud.config.discovery.enabled :开启Config服务发现支持
  • spring.cloud.config.discovery.serviceId :指定server端的name,也就是server端spring.application.name的值
  • eureka.client.serviceUrl.defaultZone :指向配置中心的地址

这三个配置文件都需要放到bootstrap.properties的配置中

启动类改造

启动类添加@EnableDiscoveryClient激活对配置中心的支持

@EnableDiscoveryClient
@SpringBootApplication
public class ConfigClientApplication {

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

启动client端,在浏览器中访问:http://localhost:8000/ 就会看到server端和client端都已经注册了到注册中心了。

1561966069971

高可用

为了模拟生产集群环境,我们改动server端的端口为8003,再启动一个server端来做服务的负载,提供高可用的server端支持

1561966207638

如上图就可发现会有两个server端同时提供配置中心的服务,防止某一台down掉之后影响整个系统的使用。

我们先单独测试服务端,分别访问:http://localhost:8006/neo-config/devhttp://localhost:8009/neo-config/dev返回信息:

{"name":"neo-config","profiles":["dev"],"label":null,"version":"6eecd82c8cbbab7d1fc167a5b4e543cdede44cd1","state":null,"propertySources":[{"name":"https://github.com/miaohangbo/config-repo//neo-config-dev.properties","source":{"neo.hello":"hello im dev11123232"}}]}

说明两个server端都正常读取到了配置信息。

再次访问:http://localhost:8002/hello,返回:hello im dev update。说明客户端已经读取到了server端的内容,我们随机停掉一台server端的服务,再次访问http://localhost:8002/hello,返回:hello im dev update,说明达到了高可用的目的。

RabbitMQ

详见MQ笔记

ElasticSearch

详见ES笔记

Redis

详见Redis笔记

分布式锁

通过redis实现分布式锁

分布式锁,redis(reddsion)

基础版

public void testLock(){
//1获取锁,setnx
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "1");
//2获取锁成功、查询num的值
if(lock){
Object value = redisTemplate.opsForValue().get("num");
//2.1判断num为空return
if(StringUtils.isEmpty(value)){
return;
}
//2.2有值就转成成int
int num = Integer.parseInt(value+"");
//2.3把redis的num加1
redisTemplate.opsForValue().set("num", ++num);
//2.4释放锁,del
redisTemplate.delete("lock");
}else{
//3获取锁失败、每隔0.1秒再获取
try {
Thread.sleep(100);
testLock();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

问题:获取到锁时,业务逻辑出错后,导致锁无法被释放。

设置锁的过期时间

public void testLock(){
//1获取锁,setne
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "1",3,Tumeout.SEXOUNDS);
//2获取锁成功、查询num的值
if(lock){
Object value = redisTemplate.opsForValue().get("num");
//2.1判断num为空return
if(StringUtils.isEmpty(value)){
return;
}
//2.2有值就转成成int
int num = Integer.parseInt(value+"");
//2.3把redis的num加1
redisTemplate.opsForValue().set("num", ++num);
//2.4释放锁,del
redisTemplate.delete("lock");
}else{
//3获取锁失败、每隔0.1秒再获取
try {
Thread.sleep(100);
testLock();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

问题:在获取锁时,设置一个指定的唯一值(UUID),释放前获取这个值,判断是否是自己的锁

加入UUID

public void testLock(){

String uuId = UUID.randomUUID().toString();

//1获取锁,setne
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuId,3,Tumeout.SEXOUNDS);
//2获取锁成功、查询num的值
if(lock){
Object value = redisTemplate.opsForValue().get("num");
//2.1判断num为空return
if(StringUtils.isEmpty(value)){
return;
}
//2.2有值就转成成int
int num = Integer.parseInt(value+"");
//2.3把redis的num加1
redisTemplate.opsForValue().set("num", ++num);
//2.4释放锁,del
if(uuId.equals((String)redisTemplate.opForValue().get("lock"))){
this.redisTemplate.delete("lock");
}
}else{
//3获取锁失败、每隔0.1秒再获取
try {
Thread.sleep(100);
testLock();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

问题:删除缺乏原子性

  1. index执行删除时,查询到的lock值确实和UUID相同
  2. index1执行删除时,lock刚好过期时间到了,被redis自动释放;
  3. index2获取到锁,index1执行删除时,就会把index2的lock删除掉
this.redisTemplate.delete("lock");

使用LUA脚本保证删除的原子性

@GetMapping("testLockLua")
public void testLockLua() {

//1 声明一个uuid ,将做为一个value 放入我们的key所对应的值中
String uuid = UUID.randomUUID().toString();

//2 定义一个锁:lua 脚本可以使用同一把锁,来实现删除!
String skuId = "25"; // 访问skuId 为25号的商品 100008348542
String locKey = "lock:" + skuId; // 锁住的是每个商品的数据

// 3 获取锁
Boolean lock = redisTemplate.opsForValue().setIfAbsent(locKey, uuid, 3, TimeUnit.SECONDS);

// 如果true
if (lock) {
// 执行的业务逻辑开始
// 获取缓存中的num 数据
Object value = redisTemplate.opsForValue().get("num");
// 如果是空直接返回
if (StringUtils.isEmpty(value)) {
return;
}
// 不是空 如果说在这出现了异常! 那么delete 就删除失败! 也就是说锁永远存在!
int num = Integer.parseInt(value + "");
// 使num 每次+1 放入缓存
redisTemplate.opsForValue().set("num", String.valueOf(++num));

/*使用lua脚本来锁*/
// 定义lua 脚本
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
// 使用redis执行lua执行
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
redisScript.setScriptText(script);
// 设置一下返回值类型 为Long
// 因为删除判断的时候,返回的0,给其封装为数据类型。如果不封装那么默认返回String 类型,
// 那么返回字符串与0 会有发生错误。
redisScript.setResultType(Long.class);
// 第一个要是script 脚本 ,第二个需要判断的key,第三个就是key所对应的值。
redisTemplate.execute(redisScript, Arrays.asList(locKey), uuid);

} else {
// 其他线程等待
try {
// 睡眠
Thread.sleep(1000);
// 睡醒了之后,调用方法。
testLockLua();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

基于数据库

悲观锁

表结构

DROP TABLE IF EXISTS `method_lock`;
CREATE TABLE `method_lock` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
`method_name` varchar(64) NOT NULL COMMENT '锁定的方法名',
`desc` varchar(1024) NOT NULL COMMENT '备注信息',
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '保存数据时间,自动生成',
`PRIMARY KEY (`id`),
UNIQUE KEY `uidx_method_name` (`method_name`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 COMMENT='锁定中的方法';

获取锁

INSERT INTO method_lock (method_name, desc) VALUES ('methodName', 'methodName');

对method_name做了唯一性约束,这里如果有多个请求同时提交到数据库的话,数据库会保证只有一个操作可以成功。

乐观锁

表结构

DROP TABLE IF EXISTS `method_lock`;
CREATE TABLE `method_lock` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
`method_name` varchar(64) NOT NULL COMMENT '锁定的方法名',
`state` tinyint NOT NULL COMMENT '1:未分配;2:已分配',
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`version` int NOT NULL COMMENT '版本号',
`PRIMARY KEY (`id`),
UNIQUE KEY `uidx_method_name` (`method_name`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 COMMENT='锁定中的方法';

先获取锁信息

select id, method_name, state,version from method_lock where state=1 and method_name='methodName';

占有锁

update t_resoure set state=2, version=2, update_time=now() where method_name='methodName' and state=1 and version=2;

缺点

  1. 这把锁强依赖数据库的可用性,数据库是一个单点,一旦数据库挂掉,会导致业务系统不可用。

  2. 这把锁没有失效时间,一旦解锁操作失败,就会导致锁记录一直在数据库中,其他线程无法再获得到锁。

  3. 这把锁只能是非阻塞的,因为数据的insert操作,一旦插入失败就会直接报错。没有获得锁的线程并不会进入排队队列,要想再次获得锁就要再次触发获得锁操作。

  4. 这把锁是非重入的,同一个线程在没有释放锁之前无法再次获得该锁。因为数据中数据已经存在了。

解决方式

1、数据库是单点?搞两个数据库,数据之前双向同步。一旦挂掉快速切换到备库上。

2、没有失效时间?只要做一个定时任务,每隔一定时间把数据库中的超时数据清理一遍。

3、非阻塞的?搞一个while循环,直到insert成功再返回成功。

4、非重入的?在数据库表中加个字段,记录当前获得锁的机器的主机信息和线程信息,那么下次再获取锁的时候先查询数据库,如果当前机器的主机信息和线程信息在数据库可以查到的话,直接把锁分配给他就可以了。

分布式事务