《微服务》九大特性笔记

服务组件化

组件,是一个可以独立更换和升级的单元。就像 PC 中的 CPU、内存、显卡、硬盘一样,独立且可以更换升级而不影响其他单元。

在“微服务”架构中,需要我们对服务进行组件化分解。服务,是一种进程外的组件,它通过 http 等通信协议进行协作,而不是传统组件以嵌入的方式协同工作。服务都独立开发、部署,可以有效的避免一个服务的修改引起整个系统的重新部署。

打一个不恰当的比喻,如果我们的 PC 组件以服务的方式构建,我们只维护主板和一些必要外设之后,计算能力通过一组外部服务实现,我们只需要告诉 PC 我们从哪个地址来获得计算能力,通过服务定义的计算接口来实现我们使用过程中的计算需求,从而实现 CPU 组件的服务化。这样我们原本复杂的 PC 服务得到了更轻量化的实现,我们甚至只需要更换服务地址就能升级我们 PC 的计算能力。

按业务组织团队

当我们开始决定如何划分“微服务”时,通常也意味着我们要开始对团队进行重新规划与组织。按以往的方式,我们往往会以技术的层面去划分多个不同的团队,比如:DBA 团队、运维团队、后端团队、前端团队、设计师团队等等。若我们继续按这种方式组织团队来实施“微服务”架构开发时,当有一个有问题需要更改,可能是一个非常简单的变动,比如:对人物描述增加一个字段,这就需要从数据存储开始考虑一直到设计和前端,虽然大家的修改都非常小,但这会引起跨团队的时间和预算审批。

在实施“微服务”架构时,需要采用不同的团队分割方法。由于每一个微服务都是针对特定业务的宽栈或是全栈实现,既要负责数据的持久化存储,又要负责用户的接口定义等各种跨专业领域的职能。因此,面对大型项目时候,对于微服务团队拆分更加建议按业务线的方式进行拆分,一方面可以有效减少服务内部修改所产生的内耗;另一方面,团队边界可以变得更为清晰。

做“产品”的态度

实施“微服务”架构的团队中,每个小团队都应该以做产品的方式,对其产品的整个生命周期负责。而不是以项目的模式,以完成开发与交付并将成果交接给维护者为最终目标。

开发团队通过了解服务在具体生产环境中的情况,可以增加他们对具体业务的理解,比如:很多时候一些业务中发生的特殊或异常情况,很可能产品经理都并不知晓,但细心的开发者很容易通过生产环境发现这些特殊的潜在问题或需求。

所以,我们需要用做“产品”的态度来对待每一个“微服务”,持续关注服务的运作情况,并不断地分析帮助用户来提升业务功能。

智能端点与哑管道

在单体应用中,组件间直接通过函数调用的方式进行交互协作。而在“微服务”架构中,服务由于不在一个进程中,组件间的通信模式发生了改变,若仅仅将原本在进程内的方法调用改成 RPC 方式的调用,会导致微服务之间产生繁琐的通信,使得系统表现更为糟糕,所以,我们需要更粗粒度的通信协议。

在“微服务”架构中,通常会使用这两个服务调用方式:

第一种,使用 HTTP 协议的 RESTful API 或轻量级的消息发送协议,来实现信息传递与服务调用的触发。
第二种,通过在轻量级消息总线上传递消息,类似 RabbitMQ 等一些提供可靠异步交换的结构。

在极度强调性能的情况下,有些团队会使用二进制的消息发送协议,例如:protobuf。即使是这样,这些系统仍然会呈现出“智能端点和哑管道”的特点,为了在易读性与高效性之间取得平衡。当然大多数 Web 应用或企业系统并不需要作出在这两者间做出选择,能够获得易读性就已经是一个极大的胜利了。
——Martin Fowler

去中心化治理

当我们采用集中化的架构治理方案时,通常在技术平台上都会做同一的标准,但是每一种技术平台都有其短板,这会导致在碰到短板时,不得不花费大力气去解决,并且可能还是因为其底层原因解决的不是很好。

在实施“微服务”架构时,通过采用轻量级的契约定义接口,使得我们对于服务本身的具体技术平台不再那么敏感,这样我们整个“微服务”架构的系统中的组件就能针对其不同的业务特点选择不同的技术平台,终于不会出现杀鸡用牛刀或是杀牛用指甲钳的尴尬处境了。

不是每一个问题都是钉子,不是每一个解决方案都是锤子

去中心化管理数据

我们在实施“微服务”架构时,都希望可以让每一个服务来管理其自有的数据库,这就是数据管理的去中心化。

在去中心化过程中,我们除了将原数据库中的存储内容拆分到新的同平台的其他数据库实例中之外(如:把原本存储在 MySQL 中的表拆分后,存储多几个不同的 MySQL 实例中),也可以针对一些具有特殊结构或业务特性的数据存储到一些其他技术的数据库实例中(如:把日志信息存储到 MongoDB 中、把用户登录信息存储到 Redis 中)。

虽然,数据管理的去中心化可以让数据管理更加细致化,通过采用更合适的技术来让数据存储和性能达到最优。但是,由于数据存储于不同的数据库实例中后,数据一致性也成为“微服务”架构中急需解决的问题之一。分布式事务的实现,本身难度就非常大,所以在“微服务”架构中,我们更强调在各服务之间进行“无事务”的调用,而对于数据一致性,只要求数据在最后的处理状态是一致的效果;若在过程中发现错误,通过补偿机制来进行处理,使得错误数据能够达到最终的一致性。

基础设施自动化

近年来云计算服务与容器化技术的不断成熟,运维基础设施的工作变得越来越不那么难了。但是,当我们实施“微服务”架构时,数据库、应用程序的个头虽然都变小了,但是因为拆分的原因,数量成倍的增长。这使得运维人员需要关注的内容也成倍的增长,并且操作性任务也会成倍的增长,这些问题若没有得到妥善的解决,必将成为运维人员的噩梦。

所以,在“微服务”架构中,请务必从一开始就构建起“持续交付”平台来支撑整个实施过程,该平台需要两大内容,不可或缺:

自动化测试:每次部署前的强心剂,尽可能的获得对正在运行软件的信心。
自动化部署:解放繁琐枯燥的重复操作以及对多环境的配置管理。

容错设计

在单体应用中,一般不存在单个组件故障而其他还在运行的情况,通常是一挂全挂。而在“微服务”架构中,由于服务都运行在独立的进程中,所以是存在部分服务出现故障,而其他服务都正常运行的情况,比如:当正常运作的服务 B 调用到故障服务 A 时,因故障服务 A 没有返回,线程挂起开始等待,直到超时才能释放,而此时若触发服务 B 调用服务 A 的请求来自服务 C,而服务 C 频繁调用服务 B 时,由于其依赖服务 A,大量线程被挂起等待,最后导致服务 A 也不能正常服务,这时就会出现故障的蔓延。

所以,在“微服务”架构中,快速的检测出故障源并尽可能的自动恢复服务是必须要被设计和考虑的。通常,我们都希望在每个服务中实现监控和日志记录的组件,比如:服务状态、断路器状态、吞吐量、网络延迟等关键数据的仪表盘等。

演进式设计

通过上面的几点特征,我们已经能够体会到,要实施一个完美的“微服务”架构,需要考虑的设计与成本并不小,对于没有足够经验的团队来说,甚至要比单体应用发付出更多的代价。

所以,很多情况下,架构师们都会以演进的方式进行系统的构建,在初期系统以单体系统的方式来设计和实施,一方面系统体量初期并不会很大,构建和维护成本都不高。另一方面,初期的核心业务在后期通常也不会发生巨大的改变。随着系统的发展或者业务的需要,架构师们会将一些经常变动或是有一定时间效应的内容进行“微服务”处理,并逐渐地将原来在单体系统中多变的模块逐步拆分出来,而稳定不太变化的就形成了一个核心“微服务”存在于整个架构之中。

分享到

ThreadLocal 的最佳实践

SimpleDateFormat 众所周知是线程不安全的,多线程中如何保证线程安全又同时兼顾性能问题呢?那就是使用 ThreadLocal 维护 SimpleDateFormat

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
39
40
41
42
43
44
45
public class SimpleDateFormatThreadTest {

static volatile AtomicInteger n = new AtomicInteger(-1);

static ThreadLocal<DateFormat> sdf ;

static {
sdf =new ThreadLocal<DateFormat>() {
@Override
protected DateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd");
}
};
}

public static void main(String[] args) throws ParseException, InterruptedException {

Set<String> dateSet = new ConcurrentHashSet<>();
Set<Integer> numberSet = new ConcurrentHashSet<>();

Date[] dates = new Date[1000];
for (int i = 0; i < 1000; i++) {
dates[i] = sdf.get().parse(i + 1000 + "-11-22");
}

ExecutorService executorService = Executors.newFixedThreadPool(10);
for(int i=0;i<1000;i++){
executorService.execute(new Runnable() {
@Override
public void run() {
int number = n.incrementAndGet();
String date = sdf.get().format(dates[number]);
numberSet.add(number);
dateSet.add(date);
System.out.println(number+" "+date);
}
});
}
executorService.shutdown();
Thread.sleep(5000);
System.out.println(dateSet.size());
System.out.println(numberSet.size());
}

}

实践证明 sdf 的 parse(String to Date)有严重的线程安全问题,format(Date to String)有轻微的线程安全问题,虽然不太明显,但还是会出现问题,这和内部的实现有关。

简单分析下使用 ThreadLocal 的好处,1000 次转换操作,10 个线程争抢执行,如果每次都去 new 一个 sdf,可见其效率之低,而使用 ThreadLocal,是对每个线程维护一个 sdf,所以最多就只会出现 10 个 sdf,真正项目中,由于操作系统线程分片执行,所以线程不会非常的多,使用 ThreadLocal 的好处也就立竿见影了。

分享到

Transactional 注解使用注意点

@Transactional 可以说是 spring 中最常用的注解之一了,通常情况下我们在需要对一个 service 方法添加事务时,加上这个注解,如果发生 unchecked exception,就会发生 rollback,最典型的例子如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Service
public class StudentService {
@Autowired
StudentDao studentDao;

@Transactional
public void innerSave(int i) {
Student student = new Student();
student.setName("test" + i);
studentDao.save(student);
//i=5 会出现异常
int a = 1 / (i - 5);
}
}

在调用 innerSave(5) 时会发运算异常,导致保存操作回滚,不在此赘述了。

新的需求:循环保存 10 个学生,发生异常时要求回滚。
我们理所当然的写出了下面的代码,在 StudentService.java 添加如下方法

1
2
3
4
5
6
7
8
9
public void outerLooper1() {
for (int i = 1; i <= 10; i++) {
try{
innerSave(i);
}catch (Exception e){
e.printStackTrace();
}
}
}

先考虑一下 test5 这个学生有没有保存呢?
结果:
这里写图片描述
依然出现了,考虑下问题出在哪儿了?

其实也好理解,spring 中 @Transactional 的事务开启 ,是基于接口 或者是类的代理被创建的。所以在同一个类中一个普通方法 outerLooper1() 调用另一个有事务的方法 innerSave(),事务是不会起作用的。要解决这个问题,一般我的做法是写一个帮助类,注入到当前类中,来完成事务操作。

1
2
3
4
5
6
7
8
@Autowired
UtilService utilService;

public void outerLooper2() {
for (int i = 1; i <= 10; i++) {
utilService.innerSave(i);
}
}

这里写图片描述
在 spring 中使用事务需要遵守一些规范和了解一些坑点,别想当然。列举一下一些注意点。

  • 在需要事务管理的地方加 @Transactional 注解。@Transactional 注解可以被应用于接口定义和接口方法、类定义和类的 public 方法上。

  • @Transactional 注解只能应用到 public 可见度的方法上。如果你在 protectedprivate 或者 package-visible 的方法上使用 @Transactional 注解,它也不会报错,但是这个被注解的方法将不会展示已配置的事务设置。

  • Spring 团队建议在具体的类(或类的方法)上使用 @Transactional 注解,而不要使用在类所要实现的任何接口上。在接口上使用 @Transactional 注解,只能当你设置了基于接口的代理时它才生效。因为注解是 不能继承的,这就意味着如果正在使用基于类的代理时,那么事务的设置将不能被基于类的代理所识别,而且对象也将不会被事务代理所包装。

  • @Transactional 的事务开启 ,或者是基于接口的或者是基于类的代理被创建。所以在同一个类中一个方法调用另一个方法有事务的方法,事务是不会起作用的。

  • 了解事务的隔离级别,各个数据库默认的隔离级别是不一样的,在 spring 中用的是 isolation = Isolation.READ_COMMITTED 来设置;了解事务的传播机制,当发生事务嵌套时,按照业务选择对应的传播机制,用 propagation= Propagation.REQUIRED 来设置。

分享到

简单了解 RPC 实现原理

时下很多企业应用更新换代到分布式,一篇文章了解什么是 RPC。
原作者梁飞,在此记录下他非常简洁的 rpc 实现思路。

查看更多

分享到

java trick--String.intern()

《深入理解 java 虚拟机》第二版中对 String.intern() 方法的讲解中所举的例子非常有意思

不了解 String.intern() 的朋友要理解他其实也很容易,它返回的是一个字符串在字符串常亮池中的引用。直接看下面的 demo

1
2
3
4
5
6
7
8
9
public class Main {
public static void main(String[] args) {
String str1 = new StringBuilder("计算机").append("软件").toString();
System.out.println(str1.intern() == str1);

String str2 = new StringBuilder("ja").append("va").toString();
System.out.println(str2.intern() == str2);
}
}

两者输出的结果如下:

1
2
true
false

我用的 jdk 版本为 Oracle JDK7u45。简单来说,就是一个很奇怪的现象,为什么 java 这个字符串在类加载之前就已经加载到常量池了?

我在知乎找到了具体的说明,如下:

1
2
3
4
5
6
7
8
9
10
11
package sun.misc;

import java.io.PrintStream;

public class Version {
private static final String launcher_name = "java";
private static final String java_version = "1.7.0_79";
private static final String java_runtime_name = "Java(TM) SE Runtime Environment";
private static final String java_runtime_version = "1.7.0_79-b15";
...
}

而 HotSpot JVM 的实现会在类加载时先调用:

1
2
3
4
5
6
7
8
9
public final class System{
...
private static void initializeSystemClass() {
...
sun.misc.Version.init();
...
}
...
}

原来是 sun.misc.Version 这个类在起作用。

分享到

java trick -- intergerCache

看一段代码:

1
2
3
4
5
6
7
public class Main {
public static void main(String[] args) {
Integer a=100,b=100,c=150,d=150;
System.out.println(a==b);
System.out.println(c==d);
}
}

这段代码会输出什么?

查看更多

分享到

java trick--system.out.println

多线程在使用 system.out.println 时要留一个有意思的地方

查看更多

分享到

Hello World

分享到

使用 JPA 实现乐观锁

乐观锁的概念就不再赘述了,不了解的朋友请自行百度谷歌之,今天主要说的是在项目中如何使用乐观锁,做成一个小 demo。

持久层使用 jpa 时,默认提供了一个注解 @Version 先看看源码怎么描述这个注解的

1
2
3
4
@Target({METHOD, FIELD})
@Retention(RUNTIME)
public @interface Version {
}

简单来说就是用一个 version 字段来充当乐观锁的作用。
先来设计实体类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* Created by xujingfeng on 2017/1/30.
*/
@Entity
@Table(name = "t_student")
public class Student {

@Id
@GenericGenerator(name = "PKUUID", strategy = "uuid2")
@GeneratedValue(generator = "PKUUID")
@Column(length = 36)
private String id;

@Version
private int version;

private String name;

//getter()...
//setter()...
}

查看更多

分享到

使用 zkclient 操作 zookeeper 的学习过程记录

前言

最近开发的分布式 (使用 motan) 项目中使用 zookeeper 作为服务中心来提供注册服务 (@MotanService) 和发现服务(@MotanRefer), 虽然 motan 这个 rpc 框架对服务模块进行了很好的封装,但是以防以后会出现定制化的需求,以及对服务更好的监控,所以有必要了解一下 zookeeper 的基本知识和使用方法。关于 zookeeper 的知识点,网上很多的博客都已经介绍的很详尽了,我写这篇的博客的用意其实也就是将一些零散的却很精妙的博客整理出来,方便以后查阅。短篇以 cp 的方式,长篇的以 url 的方式。

zookeeper 是什么?

ZooKeeper 是一个分布式的,开放源码的分布式应用程序协调服务,是 Google 的 Chubby 一个开源的实现,是 Hadoop 和 Hbase 的重要组件。它是一个为分布式应用提供一致性服务的软件,提供的功能包括:配置维护、域名服务、分布式同步、组服务等。
ZooKeeper 的目标就是封装好复杂易出错的关键服务,将简单易用的接口和性能高效、功能稳定的系统提供给用户。
ZooKeeper 包含一个简单的原语集,提供 Java 和 C 的接口。 ZooKeeper 代码版本中,提供了分布式独享锁、选举、队列的接口。

—- 百度百科

一开始看的云里雾里的,幸好我之前搞过一点 hadoop,对他的生态体系有所了解,这才大概知道他想说什么。提炼几个关键词,并且加入我后面学习的理解,总结一下就是 –

zookeeper 是一个组件,需要安装客户端和服务端,一般用于解决分布式开发下的一些问题。化抽象为具体,你可以把整个 zookeeper 理解成一个树形数据结构,也可以理解为一个文件系统的结构,每个叶子节点都会携带一些信息 (data),并且也可能会携带一些操作 (op)。分布式场景中,每一个客户端都可以访问到这些叶子节点,并且进行一些操作。我们所有使用 zookeeper 的场景几乎都是在 CRUD 某一个或者某些叶子节点,然后会触发对应的操作… 即 zookeeper 本身可以理解为一个 shareData。
—- 来自于博主的口胡

zookeeper 怎么学?

学一个新的中间件的最好方法是先在脑子里面有一个想法:我为什么要学他,是想解决什么问题,他大概是个什么东西,我觉得打开思路的最好方式是看几篇博客 (大多数情况你一开始看不懂,但是混个眼熟),然后看视频,这里我自己是了解过了 zookeeper 原生的 api 之后看了极客学院 的视频

查看更多

分享到