get set原子性引发出的思考

经常在业务中碰到对变量先get变量判断是否为blank,如果不为blank直接返回,如果为blank需先获取设置到变量,在返回,伪代码如下:

1
2
3
4
5
6
7
8
9
10
private String getToken(){
// select
String accessToken = redisUtils.get(key);
if(StringUtils.isBlank(accessToken)){
// get set
accessToken = HTTPUtils.doGet(url);
redisUtils.setEx(key, accessToken, 7000, TimeUtils.SECONDS);
}
return accessToken;
}

这里我们就能发现问题了,我们操作的redis中的key相当于全局变量,如果此时不幸同一时刻多个线程同时获取accessToken,更不幸的是最新获取的accessToken才是有效的,那么有很大的几率前一个线程刚获取到accessToken开始表演的时候,被通知accessToken已失效导致整段垮掉。所以怎样保证线程安全的同时还能保证高性能呢?这期间也思考了多种方法:

简单粗暴型

直接对getToken整个方法进行同步

1
private syncronzied String getToken(){...}

这个方法完全解决了全局变量线程安全的问题,select->get->set 将这一整个操作保证原子性,但是劣处也是显而易见的,这里的锁颗粒度太大,导致所有线程必须乖乖的一个一个getToken, 性能不用说也知道极差,有没有一种方法可以保证多线程的读,但是只有一个线程能写呢?

只锁get set

我们来改变一下锁的位置:

1
2
3
4
5
6
7
8
9
10
11
12
13
private static final byte[] mutex = new byte[0];
private String getToken(){
// select
String accessToken = redisUtils.get(key);
if(StringUtils.isBlank(accessToken)){
syncronized(mutex){
// get set
accessToken = HTTPUtils.doGet(url);
redisUtils.setEx(key, accessToken, 7000, TimeUtils.SECONDS);
}
}
return accessToken;
}

当select accessToken为blank的时候才会进入同步代码块 get set,乍一看可能没问题了,其实当同一时刻多个线程访问的时候还是会有问题,比如多个线程已经进行了blank判断,都在等待mutext的锁,导致前一个得到mutex锁的线程获取到的accessToken因为下一个线程再次get set accessToken而失效。

双重检查

为了解决上一种方法碰到的问题,我们可以在获取到锁时再次进行确认:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private static final byte[] mutex = new byte[0];
private String getToken(){
// select
String accessToken = redisUtils.get(key);
if(StringUtils.isBlank(accessToken)){
syncronized(mutex){
// 再次确认
accessToken = redisUtils.get(key);
if(StringUtils.isBlank(accessToken)){
// get set
accessToken = HTTPUtils.doGet(url);
redisUtils.setEx(key, accessToken, 7000, TimeUtils.SECONDS);
}
}
}
return accessToken;
}

此时既保证了线程安全的同时,又尽可能的减少同步代码提升性能。

评论

Hibernate - no longer referenced异常

在使用hibernate级联关系时给了我们极大的便利,但是有时也会出现莫名其妙的异常,代码怎么看都没有问题,save的时候就是抛出了异常(气😠)。

最近就被同事问到了这样的问题,调用save的时候抛出了这个异常:

1
2
org.hibernate.HibernateException:
A collection with cascade="all-delete-orphan" was no longer referenced by the owning entity instance

字面意思是说,这个entity不再拥有实例的引用,我们反复检查了代码都没有发现问题。。。。

经过一顿操作之后(google😏),终于发现了问题。

在一个one to many关系时,一的那一方我们定义了:

1
2
@OneToMany(cascade = CascadeType.ALL, mappedBy = "parent", orphanRemoval = true)
private List<Children> childs = new ArrayList<>();

问题就出在orphanRemoval=true这里,当这个值为true时,hibernate会帮我们管理这个子集合,所有当childs为null或调用普通的set方法时,就会抛出上面这个异常。

1
2
3
4
public void setChilds(List<Children> childs){
// will throw exception like this
this.childs = childs;
}

这是因为我们声明了一个新的childs代替了原本的childs,导致parent实体与原本的childs之间的关系被破坏了,但是原本的childs还存在于这一个session中,所以当我们调用save的时候,原本的childs与parent之间关系被破坏,hibernate不认识了原本的childs,就会throw上面的异常。

当知道了为什么之后,事情就好办了,我们需要对set方法进行改造:

1
2
3
4
public void setChilds(List<Children> childs){
this.childs.clear();
this.childs.addAll(childs);
}

这样childs和parent之间的关系没有被破坏,当然save的时候hibernate也就认识了这个childs啦。

评论

Java深度拷贝:CloneUtils

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/**
* FUNCTION
*
* @author zili
* @create 2017-12-23 17:17
* @since 1.8
*/
public class CloneUtils {

public static <T extends Serializable> T deepClone(T obj) {
T cloneObj = null;

try {
// 写入字节流
ByteArrayOutputStream out = new ByteArrayOutputStream();
ObjectOutputStream obs = new ObjectOutputStream(out);
obs.writeObject(obj);
obs.close();

// 分配内存, 写入原始对象, 生成新对象
ByteArrayInputStream ios = new ByteArrayInputStream(out.toByteArray());
ObjectInputStream ois = new ObjectInputStream(ios);
// 返回生成的新对象
cloneObj = (T) ois.readObject();
ois.close();
} catch (Exception e) {
e.printStackTrace();
}

return cloneObj;
}

}
评论

Spring Cloud - Feign

Ribbon

Spring CLoud Ribbon是基于HTTP和TCP的负载均衡工具,在微服务开发我们经常使用的Feign也是使用的它。它是以Netflix Ribbon为基础开发的,
Ribbon是通过在客户端配置ribbonServerList来实现请求和负载均衡的。当与Spring Cloud Eureka联合使用时,ribbonServerList会被DiscoveryEnabledNIWSServerList重写,它会将获取服务端列表和确定服务端是否启动的职责交给eureka。

准备工作:
1、启动Eureka Server
2、启动两个provider-user(服务提供者)

image

在服务提供者中只简单了提供了一个加法功能:

provider 1:

1
2
3
4
@GetMapping("/add")
public String addCompute(@RequestParam Integer a, @RequestParam Integer b){
return "我是第一个服务提供者, 答案是: " + (a + b);
}

provider 2:

1
2
3
4
@GetMapping("/add")
public String addCompute(@RequestParam Integer a, @RequestParam Integer b){
return "我是第二个服务提供者, 答案是: " + (a + b);
}

使用Ribbon实现负载均衡

构建一个Spring boot项目:

添加依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.3.5.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>

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

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Brixton.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

在项目主类,添加@EnableDiscoveryClient来启用服务发现能力。创建RestTemplate的Bean,添加@LoadBalanced注解启用负载均衡功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* TODO
* Created by jinzili on 09/06/2017.
*/
@SpringBootApplication
@EnableDiscoveryClient
public class ConsumerMovieApplication {

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

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

创建ConsumerController,直接使用RestTemplate调用provider:

1
2
3
4
5
6
7
8
@Autowired
private RestTemplate restTemplate;

@GetMapping("/add")
public void computeAdd(){
String addResult = this.restTemplate.getForObject("http://MIRCROSERVICE-PROVIDER-USER/add?a=2&b=3", String.class);
System.out.println(addResult);
}

application.yml配置eureka注册中心:

1
2
3
4
5
6
7
8
9
10
11
12
13
server:
port: 8010
debug: true
spring:
application:
name: microservice-consumer
eureka:
instance:
prefer-ip-address: true
client:
service-url:
# defaultZone: http://peer1:peer1@peer1:8761/eureka/, http://peer2:peer2@peer2:8762/eureka/
defaultZone: http://localhost:8761/eureka/

访问两次http://localhost:8010/add,查看日志
image

我们可以看到两个provider分别被调用了一次,所以通过这些简单的配置和代码已经提供了一个客户端负责均衡的spring boot项目。

但是在实际项目中一般不会直接使用Ribbon,因为我们将URL硬编码到了Controller里,而且可能散落在项目的各个地方,这种方式实在不怎么优雅。所以在实际项目中我们都使用声明式的客户端–Feign。

Feign

Feign在RestTemplate的基础上对其进行封装,并且Spring Cloud为Feign增加了对Spring MVC注解的支持,整合了Ribbon和Eureka来实现负载均衡的。

创建一个spring boot 项目:

添加依赖:
在上文的依赖中只需在添加上对Feign的依赖:

1
2
3
4
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-feign</artifactId>
</dependency>

在项目主类上加上@EnableFeignClients开启Feign Client功能:

1
2
3
4
5
6
7
8
9
10
11
12
/**
* TODO
* Created by jinzili on 09/06/2017.
*/
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class ConsumerMovieFeignApplication {
public static void main(String[] args) {
SpringApplication.run(ConsumerMovieFeignApplication.class, args);
}
}

创建Feign Client:

1
2
3
4
5
6
7
8
9
/**
* TODO
* Created by jinzili on 10/06/2017.
*/
@FeignClient(name = "microservice-provider-user")
public interface UserFeignClient {
@GetMapping("/add")
String add(@RequestParam("a") Integer a, @RequestParam("b") Integer b);
}

创建controller:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* TODO
* Created by jinzili on 09/06/2017.
*/
@RestController
public class MovieController {
@Autowired
private UserFeignClient userFeignClient;

@GetMapping("/add")
public void add(){
String result = this.userFeignClient.add(2, 3);
System.out.println(result);
}
}

访问两次http://localhost:8011/add,查看日志
image

我们发现得到了与使用RestTemplate同样的效果,但是大大提高了代码的优雅性,我们可以像调用本地方法一样调用provider,并且同一模块的provider聚合到了一起。

有需要注意的几点:

  • 我们在Feign Client中使用@RequestParam @PathVariable 注解时必须要要指定其value,比如@RequestParam(“userName”),@PathVariable(“id”),这是因为Feign在处理这些注解时和Spring MVC不一样,Feign不会通过反射获取默认的key值。
  • 当使用GET方法和@RequestBody注解共同存在时,Feign会将请求自动转成POST方法,这一点也是需要特别注意的一点。
评论

quartz的misfire策略

quartz框架中提供了一系列misfire策略,是指任务因为某种原因错过了执行,可以根据不同任务需求定制不同的策略。

without repeating (不重复的任务)

这是一个在2017-11-30 11:25:00执行的任务,距离我写此篇文章的时候已经过去了十分钟左右,所以一定会出发misfire策略

1
2
3
4
5
SimpleTrigger simpleTrigger = TriggerBuilder.newTrigger()
.withIdentity("helloworld-trigger-name-1", "helloworld-trigger-group-1")
.withSchedule(SimpleScheduleBuilder.simpleSchedule())
.startAt(new Date(1512012300000L))
.build();

withMisfireHandlingInstructionFireNow

错过之后马上执行

withMisfireHandlingInstructionNextWithRemainingCount

错过之后忽略

repeating fixed number of times (多次执行)

这是一个在2017-11-30 12:00:00执行的任务,共执行5次,每次间隔1个小时的任务

1
2
3
4
5
6
7
SimpleTrigger simpleTrigger = TriggerBuilder.newTrigger()
.withIdentity("helloworld-trigger-name-1", "helloworld-trigger-group-1")
.withSchedule(SimpleScheduleBuilder.simpleSchedule()
.withRepeatCount(5)
.withIntervalInHours(1))
.startAt(1512014400000L)
.build();

withMisfireHandlingInstructionFireNow

错过之后,恢复正常时马上执行,总次数还是5次,假如在12:15:00分时执行了misfire,则以后每次执行都会在15分时执行,会执行到16:15:00。

withMisfireHandlingInstructionNowWithExistingCount

如果13:00:00错过执行,13:15:00恢复执行,则将清空已执行次数,总的执行次数为6,会执行到17:15:00。

withMisfireHandlingInstructionNextWithRemainingCount

错过之后忽略,do nothing, 总次数还是5次

reapting forever (重复执行)

会在2017-11-30 12:00:00开始第一次执行,并每隔1个小时重复执行下去

1
2
3
4
5
6
7
SimpleTrigger simpleTrigger = TriggerBuilder.newTrigger()
.withIdentity("helloworld-trigger-name-1", "helloworld-trigger-group-1")
.withSchedule(SimpleScheduleBuilder.simpleSchedule()
.repeatForever()
.withIntervalInHours(1))
.startAt(1512014400000L)
.build();

withMisfireHandlingInstructionFireNow

每次错过后,在下一个失效节点执行

withMisfireHandlingInstructionNextWithRemainingCount

每次错过后,在下个定义的时间点执行

Cron triggers (cron表达式执行)

这是一个会在每天下午两点到五点每隔一小时执行的任务

1
2
3
4
CronTrigger cronTrigger = TriggerBuilder.newTrigger()
.withIdentity("helloworld-trigger-name-" + i, "helloworld-trigger-group-1")
.withSchedule(CronScheduleBuilder.cronSchedule("00 00 14-17 * * ?"))
.build();

withMisfireHandlingInstructionIgnoreMisfires

所有错过的任务马上执行

withMisfireHandlingInstructionDoNothing

所有错过的任务忽略

withMisfireHandlingInstructionFireAndProceed

合并错过的任务,如14点和15点的任务错过,只会执行一次misfire,下次整点继续执行。

评论

Kubernetes - 专治疑难杂症

  • 使用kubectl logs 出现 failed to create fsnotify watcher: too many open files
    这是因为系统默认的fs.inotify.max_user_instances=128太小,重新设置此值:
1
sudo sysctl fs.inotify.max_user_instances=8192
评论

Java - synchronized

当我们在使用synchronized时,通常有下面几种方式:

synchronized(this){}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class SynchronizedTest {

public static class RunnableThread implements Runnable{

@Override
public void run() {
synchronized (this){
System.out.println(this);
for(int i = 0; i < 50; i++){
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}


@Test
public void synchronizedThisTest(){
RunnableThread r1 = new RunnableThread();
// RunnableThread r2 = new RunnableThread();
Thread t1 = new Thread(r1, "t1");
Thread t2 = new Thread(r1, "t2");
t1.start();
t2.start();
}

}
1
2
3
4
5
6
com.jzl.javabase.SynchronizedTest$RunnableThread@4c5986b6
t1:0
...
com.jzl.javabase.SynchronizedTest$RunnableThread@4c5986b6
t2:0
...

此时我们可以看到syschronized(this)中this指的是同一对象, 这段代码块同一时刻只能有同一线程运行。

再来看这种形式:

1
2
3
4
RunnableThread r1 = new RunnableThread();
RunnableThread r2 = new RunnableThread();
Thread t1 = new Thread(r1, "t1");
Thread t2 = new Thread(r2, "t2");
1
2
3
4
com.jzl.javabase.SynchronizedTest$RunnableThread@4c5986b6
com.jzl.javabase.SynchronizedTest$RunnableThread@22419e86
t2:0
...

此时this指的不是同一个对象, 所以这种情况syschronized代码块并不能保证同一时刻只有同一线程运行

再来看看这种情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public static class CommonClass{

public void lockMethod() {
synchronized (this){
for(int i = 0; i < 5; i++){
System.out.println(Thread.currentThread().getName() + ":" + i);
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

public void notLockMethod() {
for(int i = 0; i < 5; i++){
System.out.println(Thread.currentThread().getName() + ":" + i);
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

}

@Test
public void test2() throws InterruptedException {
CommonClass cc = new CommonClass();
Thread t1 = new Thread(() -> {cc.lockMethod();}, "t1");
Thread t2 = new Thread(() -> {cc.notLockMethod();}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
}

运行结果:

1
2
3
4
5
t1:0
t2:0
t2:1
t1:1
...

很显然,当一个线程访问这个对象的synchronized(this)方法时, 其他线程仍然可以访问这个对象的非synchronized(this)方法。

public synchronized void lockMethod()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public static class LockClass{

public synchronized void lockMethod(){
for(int i = 0; i < 5; i ++){
System.out.println(Thread.currentThread().getName() + ":" + i);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

}

@Test
public void lockClassTest() throws InterruptedException {
LockClass l1 = new LockClass();
// LockClass l2 = new LockClass();
Thread t1 = new Thread(() -> {l1.lockMethod();}, "t1");
Thread t2 = new Thread(() -> {l1.lockMethod();}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
}

@Test
public void lockClassTest() throws InterruptedException {
LockClass l1 = new LockClass(); LockClass l2 = new LockClass();
Thread t1 = new Thread(() -> {l1.lockMethod();}, "t1");
Thread t2 = new Thread(() -> {l2.lockMethod();}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
}

通过对比运行结果发现, 这种方式与synchronized(this)方式相同。第二种虽然方法是同步的, 但是锁住的却不是一个对象, 所以不能保证同一时刻只有一个线程运行此代码块。
如果要求不同对象间也只能有一个线程运行某个方法, 有两种方法可以实现:

  • synchronized(Object.class){// 同步代码块}
  • public synchronized static void lockMethod(){// 同步代码块}
  • public void lockMethod2(SomeObject so){synchronized(so){// 同步代码块}}

对于第一种方法, 通常定一个静态byte数组实现:

1
2
3
4
5
6
private static final byte[] mutex = new byte[0];
...
synchronzied(mutex){// 同步代码块}
...
// 或者是
synchronzied(SomeObject.class){// 同步代码块}

注:使用byte数组创建起来比其他对象更加经济–生成零长度的byte数组只需要三行操作码,而new Object()需要7行。

总结

在使用synchronized锁时,我们要注意锁住的对象到底是什么,然后在假设同时有多个线程同时进入代码块可能会发生的情况。这样我们就能设计更安全的多线程程序。

评论

软件开发心得记录

  • 线上fix bug时,不要立即发布,在急的bug在发布前也一定要经过回归测试,否则很有可能解决一个bug导致了另外一个bug
    Add 2017-11-21 11:20:23
  • 在代码中不要相信来自外部的数据, 关键数据一定要check
    Add 2017-11-22 20:19:30
  • 设计表的时候每一个实体表都要加上CREATE_DATE和UPDATE_DATE
    Add 2017-11-28 19:13:42
  • 数据库里的数据尽量保持完整,任何数据不要丢掉
    Add 2017-12-04 16:13:46
  • 在频繁操作数据库时,用只有SELECT权限的账户
    ADD 2017-12-29 19:56:56
  • 对代码要有责任感,乃至有强迫症
    ADD 2018-01-04 17:27:58
评论

Spring Cloud - Eureka

Eureka

在Spring Cloud体系中,eureka承担着服务注册与发现的任务,在整个微服务架构中起着整合作用。eureka的一些概念:

服务提供者

provider,指一个将自身的功能接口暴露出来的应用。

服务消费者

consumer,指需要借助provider提供的功能完成自身业务的应用。

服务注册

当provider应用启动时,会将自身的元数据,比如IP地址,端口,应用名等注册到eureka server中。

服务续约

provider每搁固定的时间段(默认30秒),向eureka server汇报一次自身状况,如果eureka server在固定的时间段(默认90秒)内没有收到某一provider的服务续约的请求,将会把这个provider实例从服务列表中剔除。

获取注册信息

consumer应用会定时从eureka server获取服务注册列表信息以供自身进行远程调用,consumer应用默认每30秒会更新一次注册列表信息,默认eureka server和consumer应用使用压缩的json进行交互。

服务下线

provider应用关闭时,会向eureka server发送下线请求,eureka server收到请求后会将此provider实例信息从服务注册列表中剔除。

Eureka Server

添加eureka server依赖

1
2
3
4
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-eureka-server</artifactId>
</dependency>

application.yml 配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
server:
  port: 8761 # 服务端口

spring:
  application:
    name: eurekaServer # 服务名
eureka:
  environment: devlopment # eureka server环境
  instance:
    lease-renewal-interval-in-seconds: 10 # 发送心跳的间隔时间
    lease-expiration-duration-in-seconds: 30 # 若这个时间段没收到心跳则移除该instance
    prefer-ip-address: true # 实例名称显示IP配置

  client:
    serviceUrl:
      defaultZone: http://localhost:8761/eureka/ # eureka server的地址
    register-with-eureka: true # 此服务是否注册到eureka server上
  server:
    enable-self-preservation: false # 关闭自我保护模式
    eviction-interval-timer-in-ms: 4000 # 扫描失效服务间隔(单位ms, 默认是60 * 1000)
    wait-time-in-ms-when-sync-empty: 0

debug: true # debug 模式

加入@EnableEurekaServer注解

1
2
3
4
5
6
7
@SpringBootApplication
@EnableEurekaServer
public class EurekaApplication {
    public static void main(String[] args) {
        SpringApplication.run(EurekaApplication.class, args);
    }
}

只需这简单的配置,就已经搭建了一台eureka server应用。

Eureka Client

添加eureka依赖

1
2
3
4
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>

application.yml 配置

1
2
3
4
5
6
7
8
9
server:
  port: 8080
debug: true
eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka/
  instance:
    prefer-ip-address: true   #将自己的IP注册到eureka server上,如不配置则表示注册微服务所在操作系统的hostname到eureka server

加入@EnableDiscoveryClient注解

1
2
3
4
5
6
7
8
9
10
11
/**
 * TODO
 * Created by jinzili on 09/06/2017.
 */
@SpringBootApplication
@EnableDiscoveryClient
public class ProviderUserApplication {
    public static void main(String[] args) {
        SpringApplication.run(ProviderUserApplication.class, args);
    }
}

现在我们已经搭建好了一台Eureka Server 和一台provider,访问http://localhost:8761看看效果!
image

perfect!

我们的两个服务已经注册到了Eureka Server上,上图的红字是因为我们在Eureka Server服务的application.yml配置了eureka.server.enable-self-preservation: false 关闭了自我保护模式,推荐在开发中我们使用此配置,可以及时刷新服务的最新状态,但是切记在生产环境使用要开启自我保护模式。

评论

2017-10-19:路灯下有感

月下旧人归,

人生几多回。

拨开云雾里,

心中自有醉。

image

评论