开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

深入学习Seata(一):什么是Seata

发表于 2019-09-03

微服务与分布式事务

分布式事务是随着服务拆分而产生的问题,至于为什么要做服务拆分以及什么是微服务,可以参考下这里
我们知道对于分布式场景而言,肯定是遵循CAP理论的,所以对于这种情况下的事务而言,跨多个服务的调用事务则成了一个令人头疼的点,而Seata则是一个用于解决分布式环境下事务的框架。

Seata历史介绍

Seata是阿里开发的一个用于微服务架构的高性能易使用的分布式事务框架。
Seata由TXC(Taobao Transaction Constructor,阿里于2014开始着手解决分布式事务的内部框架)-》GTX(Global Transaction Service,阿里于2016年将TXC发布于云服务并且改名为GTX)-》Fescar(阿里于2019将GTS开源并改名为Fescar)-》Seata(Simple Extensible Autonomous Transaction Architecture,阿里将蚂蚁金服框架DTX与Fescar结合并且改名为Seata)
目前Seata已经是github上一个大热的项目,年初开源,现在发布至0.8.0版本,已经有11000的star数,并且在快速的更新迭代。 相信未来会是一个普遍的分布式事务解决方案。

Seata架构

Seata目前的事务模式有AT,TCC,Saga三种模式,默认即是AT模式,AT本质上是2pc协议的一种实现,三种模式的不同后面文章再详细介绍。
这里我们简单介绍下Seata是如何解决分布式事务的

假设我们现在有一个商品购物的业务,对于后台系统而言有四个服务,Business(业务入口),Storage(库存服务),Order(订单服务),Account(用户服务),用户通过Business购买商品下单,在系统内部会经历以下流程:

下单

如图所示,Business通过rpc框架(dubbo,feign….)调用其他服务。Seata将上面整个调用链所产生的事务结合生成了一个全局事务:
image

如图所示,对于全局事务而言,它由各个分支事务结合而成,而分支事务则代表一个服务的本地事务。
对于每个服务来说,代表了两种角色:

全局事务

如图所示,对于每一个服务而言都有两种角色(TM,RM),在全局事务的过程中,它们会与TC进行通信协助完成整个事务,可以简单介绍下每个角色的作用:
TC*: Transaction Coordinator,事务协调器:监视每个全局事务的状态,负责全局事务的提交和回滚。
*TM
: Transaction Manager, 事务管理者:向TC发起,提交,回滚*全局事务的请求。
*RM
: Resource Manager, 资源管理器:服务向TC发起,提交,报告分支事务的请求,并且服务本地事务的回滚,提交。
image

Seata处理一个全局事务的流程如下:

  1. TM向TC请求发起一个全局事务,TC返回一个代表这个全局事务的XID。
  2. XID在rpc中传播给每一个调用链中的服务。
  3. 每个RM拿到XID后向TC发起一个分支事务,TC返回一个代表这个分支事务的XID。
  4. RM完成本地分支的业务,提交本地分支,并且报告给TC。
  5. 全局事务调用链处理完毕,TM根据有无异常向TC发起全局事务的提交或者回滚。

回滚:

  1. 假设某个RM本地事务失败。该RM自身驱动本地事务回滚,并且报告给TC。
  2. TM检测到了某个分支事务失败,向TC发起全局事务回滚。
  3. TC给每一个RM发送消息,通知它们全部回滚。
  4. TC将全局事务回滚的结果发送给TM。 全局事务结束。
    image

总结

  1. 社区活跃,短短几个月时间star数已经上W,目前已经更新0.8版本,到1.0版本可供线上环境使用。
  2. 灵活,对于seata的使用而言,使用非常简单,特别对于AT模式来说,几乎只要加一个注解就能实现分布式事务。
  3. 高性能,虽然对于使用2pc协议的一个最大诟病就是性能问题,多个库同时锁定造成性能的急剧下降。 而seata在这个基础上有较大的提升,特别对于tcc模式而言。 而目前AT模式只会
  4. 目前TC还不支持集群部署,一旦TC宕机,整个系统分布式事务全都无法处理

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

如何在测试中更好地使用mock

发表于 2019-09-02

注意:本文大部分内容为翻译 Bob 大叔的文章,原文链接可以在文章底部的参考文档处找到。

什么是 mock

mock 作为名词时表示 mock 对象,在维基百科的解释中如下:

在面向对象程序设计中,模拟对象(英语:mock object,也译作模仿对象)是以可控的方式模拟真实对象行为的假的对象。程序员通常创造模拟对象来测试其他对象的行为。

mock 作为动词时表示编写使用 mock 对象。

mock 多用于测试代码中,对于不容易构造或者不容易获取的对象,使用一个虚拟的对象来方便测试。

mock 的分类

为了使用示例说明各个mock 种类的区别与联系,文章使用 go 语言作为示例,如下为示例的基础代码:

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
复制代码type Authorizer interface {
authorize(username, password string) bool
}

type System struct {
authorizer Authorizer
}

func NewSystem(authorizer Authorizer) *System {
system = new(System)
system.authorizer = authorizer
return system
}

func (s *System) loginCount() int {
// skip
return 0
}

func (s *System) login(username, password string) error {
if s.authorizer.authorize(username, password) {
return nil
}
return errors.New("username or password is not right")
}

dummy

当你不关心传入的参数被如何使用时,你就应该使用 dummy 类型的 mock,一般用于作为其他对象的初始化参数。示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码type DummyAuthorizer struct {}
func (d *DummyAuthorizer) authorize(username, password string) bool {
// return nil
return false
}

// Test
func TestSystem(t *testing.T) {
system := NewSystem(new(DummyAuthorizer))
got := system.loginCount()
want := 0
if got != want {
t.Errorf("got %d, want %d", got, want)
}
}

在上面的测试示例代码中,DummyAuthorizer 的作为只是为了初始化 System 对象的需要,后续测试中并没有使用该 DummyAuthorizer 对象。

注意:此处的 authorize 方法原文返回了 null ,由于 go 语言不允许为 bool 返回 nil ,因此此处返回了 false

stub

当你只关心方法的返回结果,并且需要特定返回值的时候,这时候你就可以使用 stub 类型的 mock 。比如我们需要测试系统中某些功能是否能正确处理用户登录和不登录的情况,而登录功能我们已经在其他地方经过测试,而且使用真实的登录功能调用又比较的麻烦,我们就可以直接返回已登录或者未登录状态来进行其他功能的验证。

1
2
3
4
5
6
7
8
9
10
11
复制代码type AcceptingAuthorizerStub struct {}

func (aas *AcceptingAuthorizerStub) authorize(username, password string) bool {
return true
}

type RefusingAuthorizerStub struct {}

func (ras *RefusingAuthorizerStub) authorize(username, password string) bool {
return false
}

spy

当你不只是只关心方法的返回结果,还需要检查方法是否真正的被调用了,方法的调用次数等,或者需要记录方法调用过程中的信息。这个时候你就应该使用 spy 类型的 mock ,调用结束后你需要自己检查方法是否被调用,检查调用过程中记录的其他信息。但是请注意,这将会使你的测试代码和被测试方法相耦合,测试需要知道被测试方法的内部实现细节。使用时需要谨慎一些,不要过渡使用,过渡使用可能导致测试过于脆弱。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
复制代码type AcceptingAuthorizerSpy struct {
authorizeWasCalled bool
}

func (aas *AcceptingAuthorizerSpy) authorize(username, password string) bool {
aas.authorizeWasCalled = true
return true
}

// Test
func TestSystem(t *testing.T) {
authorizer := new(AcceptingAuthorizerSpy)
system := NewSystem(authorizer)
got := system.login("will", "will")
if got != nil {
t.Errorf("login failed with error %v", got)
}

if authorizer.authorizeWasCalled != true {
t.Errorf("authorize was not called")
}
}

mock

mock 类型的 mock 可以算作是真正的 ”mock“ 。把 spy 类型的 mock 在测试代码中的断言语句移动到 mock 对象中,这使它更关注于测试行为。这种类型的 mock 对方法的返回值并不是那么的感兴趣,它更关心的是哪个方法被使用了什么参数在什么时间被调用了,调用的频率等。这种类型的 mock 使得编写 mock 相关的工具更加的简单,mock 工具可以帮助你在运行时创建 mock 对象。

1
2
3
4
5
6
7
8
9
10
11
12
复制代码type AcceptingAuthorizerVerificationMock struct {
authorizeWasCalled bool
}

func (aavm *AcceptingAuthorizerVerificationMock) authorize(username, password string) bool {
aavm.authorizeWasCalled = true
return true
}

func (aavm *AcceptingAuthorizerVerificationMock) verify() bool {
return aavm.authorizeWasCalled
}

fake

fake 类型的 mock 与其他类型的 mock 最大的区别是它包含了真实的业务逻辑。当以不同的数据调用时,你会得到不同的结果。随着业务逻辑的改变,它可能也会越来越复杂,最终你也需要为这种类型的 mock 编写单元测试,甚至最后它可能成为了一个真实的业务系统。如果不是必须,请不要使用 fake 类型的 mock 。

1
2
3
4
5
6
7
8
复制代码type AcceptingAuthorizerFake struct {}

func (aas *AcceptingAuthorizerFake) authorize(username, password string) bool {
if username == "will" {
return true
}
return false
}

总结

mock 是 spy 的一种类型,spy 又是 stub 的一种类型,而 stub 又是 dummy 的一种类型,但是 fake 与其他所有 mock 类型不同,fake 包含了真实的业务逻辑,而其他类型的 mock 都不包含真实的业务逻辑。

根据 Bob 大叔的实践来看,他使用最多的是 spy 和 stub 类型的 mock ,并且他不会经常使用 mock 工具,很少使用 dummy 类型的 mock ,只有在使用 mock 工具时才会使用 mock 类型的 mock 。现在的编程 IDE 中,只需要你定义好接口,IDE 就可以帮你轻松的实现他们,你只需要简单的修改就可以实现 spy 和 stub 类型的 mock ,因此 Bob 大叔很少使用 mock 工具。

mock 的使用时机

mock 对象是一个强大的工具,但是 mock 对象也有两面性,如果使用不正确也可能会带来强大的破坏力。

完全不使用 mock

如果我们完全不使用 mock ,直接使用真实的对象进行测试,这会带来什么问题呢?

  • 测试将会运行缓慢。我们使用真实的数据库,真实的上游服务,由于这些都需要通过网络来进行通信,这会将比程序内部的函数调用慢上几个数量级。当我们修改一行简单的代码,进行测试时,可能需要等待数分钟,数小时,甚至可能要几天才能把测试运行结束。
  • 代码的测试覆盖率可能会降低很多。一些错误和异常在没有使用 mock 的情况下可能根本无法进行测试,例如网络协议的异常。一些危险的测试用例,比如删除文件、删除数据库表很难进行安全的测试。
  • 测试变得异常的脆弱。与测试无关的其他问题可能会导致测试失败,例如由于机器负载导致的网络时延问题,数据库表的结构不正确,配置文件被错误修改等问题。

在完全不使用 mock 对象的情况下,我们的测试会变得缓慢、不完整、脆弱。

过度使用 mock

如果过度使用 mock 对象,所有的测试都使用 mock 对象,这会带来什么问题呢?

  • 测试将会运行缓慢。一些 mock 工具强依赖反射机制,因此会使得测试变慢。
  • mock 所有类之间的交互,会导致你必须创建返回其他 mock 类的 mock 类,你可能需要 mock 整个交互链路上所有的类,这将会导致你的测试异常的复杂,并且所有交互链路上的 mock 类可能都耦合在了一起,当其中一个修改时,可能会导致整个测试失败。
  • 暴露本不需要暴露的接口。由于需要 mock 每一个类之间的交互,就需要为每一个类之间的交互创建接口,这将会导致你需要创建出许多只用于 mock 对象的接口,这是一种过度抽象和可怕的设计损坏。

过度使用 mock 对象,将会使用测试变得缓慢、脆弱、复杂,并且有可能损坏你的软件设计。

mock 的使用建议

在架构的重要边界使用 mock ,不要在边界内部使用 mock

例如可以在数据库、web服务器等所有第三方服务的边界处使用 mock 。可以参考如下的整洁架构图:

可以在最外环的边界处使用 mock 隔离外部依赖,方便测试,这样做可以得到如下的好处:

  • 测试运行速度快。
  • 测试不会因为外部依赖的错误而失败。
  • 更容易的模拟测试外部依赖的所有异常情况。
  • 横跨边界的有限状态机的每条路径都可以被测试。
  • mock 不在需要相互耦合依赖,代码会更整洁。

另一个比较大的好处是它强迫你思考找出软件的重要边界,并且为它们定义接口,这使得你的软件不会强耦合依赖于边界外的组件。因此你可以独立开发部署边界两边的组件。像这样去分离架构关注点是一个很好的软件设计原则。

使用你自己的 mock

mock 工具有它们自己的领域语言,在使用它们之前你必须先学习它。通过前面的 mock 类型介绍,我们已经知道用的最多的 mock 是 stub 和 spy 类型,而由于现在的 IDE 可以很方便的生成这些 mock 代码,我们只需要稍作修改就可以直接使用,所以综合来看,我们一般情况下是不需要使用 mock 工具的。

由于你自己写 mock 时不会使用反射,这将会让你的测试代码运行速度更快。如果你决定使用 mock 工具,请尽量少的使用它。

总结

mock 对象既不能完全不使用,也不能过度使用。我们应该在软件的重要边界处使用 mock ,要尽量少的使用 mock 工具,使用 mock 工具时不要过度依赖它,我们应该尽量使用轻量级的 stub 和 spy 的 mock 类型,并且我们应该自己手写这些简单的 mock 类型。如果你这样做了,你会发现你的测试运行速度更快,更稳定,并且还会有更高的测试覆盖率,你的软件架构设计也会越来越好。

参考文档

  • blog.cleancoder.com/uncle-bob/2…
  • blog.cleancoder.com/uncle-bob/2…

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Flutter完整开发实战详解(十七、 实用技巧与填坑二)

发表于 2019-09-02

作为系列文章的第十七篇,本篇再一次带来 Flutter 开发过程中的实用技巧,让你继续弯道超车,全篇均为个人的日常干货总结,以实用填坑为主,让你少走弯路狂飙车。

文章汇总地址:

Flutter 完整实战实战系列文章专栏

Flutter 番外的世界系列文章专栏


1、Package get git 失败


Flutter 项目在引用第三库时,一般都是直接引用 pub 上的第三方插件,但是有时候我们为了安全和私密,会选择使用 git 引用,如:

1
2
3
4
复制代码  photo_view:
git:
url: https://github.com/CarSmallGuo/photo_view.git
ref: master

这时候在执行 flutter packages get 过程中,如果出现失败后,再次执行 flutter packages get 可能会遇到如下图所示的问题:

)
而 flutter packages get 提示 git 失败的原因,主要是:

在下载包的过程中出现问题,下次再拉包的时候,在 .pub_cache 内的 git 目录下会检测到已经存在目录,但是可能是空目录等等,导致 flutter packages get 的时候异常。

所以你需要清除掉 .pub_cache 内的 git 的异常目录,然后最好清除掉项目下的 pubspec.lock ,之后重新执行 flutter packages get 。

win 一般是在 C:\Users\xxxxx\AppData\Roaming\Pub\Cache 路径下有 git 目录。

mac 目录在 ~/.pub-cache 。

2、TextEditingController

image.png

如上代码所示,红线部分表示,如果 controller 为空,就赋值一个 TextEditingController ,这样的写法会导致如下图所示问题:

弹出键盘时输入成功后,收起键盘时输入的内容消失了! 这是因为键盘的弹出和收起都会触发页面 build ,而在 controller 为 null 时,每次赋值的 TextEditingController 会导致 TextField 的 TextEditingValue 重置。

image.png

如上图所示,因为当 TextField 的 controller 不为空时,update 时是不会执行 value 的拷贝,所以为了避免这类问题,如下图所示, 需要先在全局构建 TextEditingController 再赋值,如果 controller 为空直接给 null 即可,避免 build 时每次重构 TextEditingController 。

3、Scrollable

如上图所示,在之前第七篇的时候分析过,滑动列表内一般都会有 Scrollable 的存在,而 Scrollable 恰好是一个 InheritedWidget ,这就给我们在 children 中调用 Scrollable 相关方法提供了便利。

如下代码所依,通过 Scrollable.of(context) 我们可以更解耦的在 ListView/GridView 的 children 对其进行控制。

1
2
3
4
5
6
7
8
9
10
11
复制代码ScrollableState state = Scrollable.of(context)

///获取 _scrollable 内 viewport 的 renderObject
RenderObject renderObject = state.context.findRenderObject();
///监听位置更新
state.position.addListener((){});
///通知位置更新
state.position.notifyListeners();
///滚动到指定位置
state.position.jumpTo(1000);
····

4、图片高斯模糊

在 Flutter 中,提供了 BackdropFilter 和 ImageFilter 实现了高斯模糊的支持,如下代码所示,可以快速实现上图的高斯模糊效果。

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
复制代码class BlurDemoPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: new Container(
child: Stack(
children: <Widget>[
Positioned(
top: 0,
bottom: 0,
left: 0,
right: 0,
child: new Image.asset(
"static/gsy_cat.png",
fit: BoxFit.cover,
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
)),
new Center(
child: new Container(
width: 200,
height: 200,
child: ClipRRect(
borderRadius: BorderRadius.circular(15.0),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 8.0, sigmaY: 8.0),
child: new Row(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
new Icon(Icons.ac_unit),
new Text("哇!!")
],
)))))
],
)));
}
}

5、滚动到指定位置

因为目前 Flutter 并没有直接提供滚动到指定 Item 的方法,在每个 Item 大小不一的情况下,折中利用如图下所示代码,可以快速实现滚动到指定 Item 的效果:

上图为部分代码,完整代码可见 scroll_to_index_demo_page2.dart ,这里主要是给每个 item 都赋予了一个 GlobalKey , 利用 findRenderObject 找到所需 item 的 RenderBox ,然后使用 localToGlobal 获取 item 在 ViewPort 这个 ancestor 中的偏移量进行滚动:

当然还有另外一种实现方式,具体可见 scroll_to_index_demo_page.dart

6、findRenderObject

在 Flutter 中是存在 容器 Widget 和 渲染Widget 的区别的,一般情况下:

  • Text、Sliver 、ListTile 等都是属于渲染 Widget ,其内部主要是 RenderObjectElement 。
  • StatelessWidget / StatefulWidget 等属于容器 Widget ,其内部使用的是 ComponentElement , ComponentElement 本身是不存在 RenderObject 的。

结合前面篇章我们说过 BuildContext 的实现就是 Element,所以 context.findRenderObject() 这个操作其实就是 Element 的 findRenderObject() 。

那么如上图所示,findRenderObject 的实现最终就是获取 renderObject,在 Element 中 renderObject 的获取逻辑就很清晰了,在遇到 ComponentElement 时,执行的是 element.visitChildren(visit); , 递归直到找到 RenderObjectElement 。

所以如下代码所示,print("${globalKey.currentContext.findRenderObject()}"); 最终输出了 SizedBox 的 RenderObject 。

7、行间距

在 Flutter 中,是没有直接设置 Text 行间距的方法的, Text 显示的效果是如下图所示的逻辑组成:

那么我们应该如何处理行间距呢?如下图所示,通过设置 StrutStyle 的 leading , 然后利用 Transform 做计算翻方向位置偏移,因为 leading 是上下均衡的,所以计算后就可以得到我们所需要的行间距大小。 (虽然无法保证一定 100%像素准确,你是否还知道其他方法?)

这里额外提一点,可以通过父节点使用 DefaultTextStyle 来实现局部样式的共享哦。

8、Builder

在 Flutter 中存在 Builder 这样一个 Widget,看源码发现它其实就是 StatelessWidget 的简单封装,那为什么还需要它的存在呢?

如下图所示,相信一些 Flutter 开发者在使用 Scaffold.of(context).showSnackBar(snackbar) 时,可能 遇到过如下错误,这是因为传入的 context 属于错误节点导致的,因为此处传入的 context 并不能找到页面所在的 Scaffold 节点。

所以这时候 Builder 的作用就体现了,如下所示,通过 builder 方法返回赋予的 context ,在向上查找 Scaffold 的时候,就可以顺利找到父节点的 Scaffold 了,这也一定程度上体现了 ComponentElement 的作用之一。

9、快速实现动画切换效果

要实现如上图所示动画效果,在 Flutter 中提供了 AnimatedSwitcher 封装简易实现。

如下图所示,通过嵌套 AnimatedSwitcher ,指定 transitionBuilder 动画效果,然后在数据改变时,同时改变需要执行动画的 key 值,即可达到动画切换的效果。

10、多语言显示异常

在官方的 github.com/flutter/flu… issue 中可以发现,Flutter 在韩语/日语 与中文同时显示,会导致 iOS 下出现文字渲染异常的问题 ,如下图所示,左边为异常情况。

改问题解决方案暂时有两种:

  • 增加字体 ttf ,全局指定改字体显示。
  • 修改主题下所有 TextTheme 的 fontFamilyFallback :
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
复制代码getThemeData() {
var themeData = ThemeData(
primarySwatch: primarySwatch
);

var result = themeData.copyWith(
textTheme: confirmTextTheme(themeData.textTheme),
accentTextTheme: confirmTextTheme(themeData.accentTextTheme),
primaryTextTheme: confirmTextTheme(themeData.primaryTextTheme),
);
return result;
}
/// 处理 ios 上,同页面出现韩文和简体中文,导致的显示字体异常
confirmTextTheme(TextTheme textTheme) {
getCopyTextStyle(TextStyle textStyle) {
return textStyle.copyWith(fontFamilyFallback: ["PingFang SC", "Heiti SC"]);
}

return textTheme.copyWith(
display4: getCopyTextStyle(textTheme.display4),
display3: getCopyTextStyle(textTheme.display3),
display2: getCopyTextStyle(textTheme.display2),
display1: getCopyTextStyle(textTheme.display1),
headline: getCopyTextStyle(textTheme.headline),
title: getCopyTextStyle(textTheme.title),
subhead: getCopyTextStyle(textTheme.subhead),
body2: getCopyTextStyle(textTheme.body2),
body1: getCopyTextStyle(textTheme.body1),
caption: getCopyTextStyle(textTheme.caption),
button: getCopyTextStyle(textTheme.button),
subtitle: getCopyTextStyle(textTheme.subtitle),
overline: getCopyTextStyle(textTheme.overline),
);
}

ps :通过WidgetsBinding.instance.window.locale; 可以获取到手机平台本身的当前语言情况,不需要 context ,也不是你设置后的 Locale 。

11、长按输入框导致异常的情况

如果项目存在多语言和主题切换的场景,可能会遇到长按输入框导致异常的场景,目前可推荐两种解放方法:

  • 1、可以给你的自定义 ThemeData 强制指定固定一个平台,但是该方式会导致平台复制粘贴弹出框没有了平台特性:
1
2
复制代码 ///防止输入框长按崩溃问题
platform: TargetPlatform.android
  • 2、增加一个自定义的 LocalizationsDelegate , 实现多语言环境下的自定义支持:
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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
复制代码
class FallbackCupertinoLocalisationsDelegate
extends LocalizationsDelegate<CupertinoLocalizations> {
const FallbackCupertinoLocalisationsDelegate();

@override
bool isSupported(Locale locale) => true;

@override
Future<CupertinoLocalizations> load(Locale locale) => loadCupertinoLocalizations(locale);

@override
bool shouldReload(FallbackCupertinoLocalisationsDelegate old) => false;
}

class CustomZhCupertinoLocalizations extends DefaultCupertinoLocalizations {
const CustomZhCupertinoLocalizations();

@override
String datePickerMinuteSemanticsLabel(int minute) {
if (minute == 1) return '1 分钟';
return minute.toString() + ' 分钟';
}

@override
String get anteMeridiemAbbreviation => '上午';

@override
String get postMeridiemAbbreviation => '下午';

@override
String get alertDialogLabel => '警告';

@override
String timerPickerHourLabel(int hour) => '小时';

@override
String timerPickerMinuteLabel(int minute) => '分';

@override
String timerPickerSecond(int second) => '秒';

@override
String get cutButtonLabel => '裁剪';

@override
String get copyButtonLabel => '复制';

@override
String get pasteButtonLabel => '粘贴';

@override
String get selectAllButtonLabel => '全选';
}

class CustomTCCupertinoLocalizations extends DefaultCupertinoLocalizations {
const CustomTCCupertinoLocalizations();

@override
String datePickerMinuteSemanticsLabel(int minute) {
if (minute == 1) return '1 分鐘';
return minute.toString() + ' 分鐘';
}

@override
String get anteMeridiemAbbreviation => '上午';

@override
String get postMeridiemAbbreviation => '下午';

@override
String get alertDialogLabel => '警告';

@override
String timerPickerHourLabel(int hour) => '小时';

@override
String timerPickerMinuteLabel(int minute) => '分';

@override
String timerPickerSecond(int second) => '秒';

@override
String get cutButtonLabel => '裁剪';

@override
String get copyButtonLabel => '復制';

@override
String get pasteButtonLabel => '粘貼';

@override
String get selectAllButtonLabel => '全選';
}

Future<CupertinoLocalizations> loadCupertinoLocalizations(Locale locale) {
CupertinoLocalizations localizations;
if (locale.languageCode == "zh") {
switch (locale.countryCode) {
case 'HK':
case 'TW':
localizations = CustomTCCupertinoLocalizations();
break;
default:
localizations = CustomZhCupertinoLocalizations();
}
} else {
localizations = DefaultCupertinoLocalizations();
}
return SynchronousFuture<CupertinoLocalizations>(localizations);
}

自此,第十七篇终于结束了!(///▽///)

资源推荐

  • Github : github.com/CarGuo
  • 开源 Flutter 完整项目:github.com/CarGuo/GSYG…
  • 开源 Flutter 多案例学习型项目: github.com/CarGuo/GSYF…
  • 开源 Fluttre 实战电子书项目:github.com/CarGuo/GSYF…
  • 开源 React Native 项目:github.com/CarGuo/GSYG…

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

从零开始的高并发(七)--- RPC的介绍,协议及框架

发表于 2019-08-31

前言

前情概要

上一篇中我们有简单提到master选举与代码实现,官网的一些模块划分和它们对于以往文章主题的帮助。毕竟不能只吃官网给出来的东西,所以还是要靠自己多找资料吧。

关于master选举的代码中,重点其实也就是最后通过一个线程去执行这个master的选举,我们在master的选举中要不停地监听我们的master,在master被抢到了以后,需要对这个线程,也就是负责抢master的线程进行阻塞操作,可是如果这时候主线程阻塞了,整个程序就不跑了,所以我们就使用了一个子线程去帮我们去抢master,此时就算抢不到master且阻塞了该线程,也能保证整个程序可以继续往下执行。

浏览官网的顺序很大程度上帮助了我们如何去了解这个框架,保证基础扎实的前提下,对于扩展类的知识也需要花挺多心思,比如paxos这些。对于这种知识官网就算无法提供一个最完美的学习方案,但是也给我们指示了方向,而且其实在很多情况下,我们并不是理解能力薄弱,而是苦于东西太多而无从下手。在知道了存在这些东西的时候去查阅一下很多大佬们的分享,相信一定不难学会。

以往链接

从零开始的高并发(一)— Zookeeper的基础概念

从零开始的高并发(二)— Zookeeper实现分布式锁

从零开始的高并发(三)— Zookeeper集群的搭建和leader选举

从零开始的高并发(四)— Zookeeper的分布式队列

从零开始的高并发(五)— Zookeeper的配置中心应用

从零开始的高并发(六)— Zookeeper的Master选举及官网小览

内容一:RPC理论

1.什么是RPC

① RPC简介

RPC(Remote Procedure Call Protocol)-远程过程调用协议。通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。它假定某种传输协议的存在,如TCP,UDP,为通信程序之间携带信息数据。在OSI网络通信模型中,RPC跨越了传输层和应用层,因分布式,微服务等而兴起

其实简单点来理解,就是比如有一个应用1,通过网络调用了应用2的服务,而不用关心应用2的协议。这就是最简单的示例。

② 为什么是远程过程调用协议?

这里的过程指的是业务处理,计算任务,更直白一点,就是程序,就像是在调用本地方法一样

与本地调用的区别可以用异地恋这个场景来理解,本地调用即是,大家同居了,都住在一起,而远程调用就像异地恋,想要见面就要不男坐车过来要不女坐车过来,而且也说异地恋一般没有好结果。远程调用中间需要网络,所以我们就可以明白

1
2
3
复制代码因为是通过网络通信的,所以
响应会慢上几个数量级
不像本地如此可靠

③ RPC的C/S模式

RPC采用client-server结构,通过request-response消息模式实现,如果服务器的程序进行了更新,那客户端方面也需要同步更新才能正常使用

④ RPC的三个过程

1
2
3
4
5
复制代码1.通讯协议:类似于在中国需要联系美国的朋友一样,我们会有多种联系他的方式,比如打电话,比如自己去美国等
2.寻址:我们需要知道怎么联系,比如打电话我们需要知道电话号码,自己去需要知道美国在哪儿这样的,
也就是,必须要知道一个具体的地址。
3.数据序列化:序列化的作用也很简单,比如我们已经拨通了这个外国人的电话,或者说已经去到他面前了
可我们说的是中文,他说英语,大家互相都听不懂大家说话,这就没法沟通了,所以序列化其实就是一个翻译作用

把这三个过程都做好之后RPC才能正常工作

⑤ 我们为什么要使用RPC

微服务,分布式系统架构的兴起,为了实现服务可重用或者说系统间的交互调用

⑥ RPC与其他协议的区别

RPC和RMI的区别,RMI远程方法调用是OOP领域中RPC的一种具体实现。也就是RPC是父,RMI是子,且RMI仅能存在Java中调用

WebService,restful接口调用其实都是RPC,只是它们的消息组织方式和消息协议不同而已

⑥ 与MQ的使用场景对比:

RPC就像打电话,需要一个迅速的回应(接通或者不接通),而MQ就像发微信QQ的消息,发过去了,不急着等回

1
2
3
4
5
6
7
8
9
复制代码架构上它们的差异是:MQ有一个中间节点Queue作为消息存储点

RPC是同步调用,对于要等待返回结果的实例,RPC处理的比较自然,由于等待结果时Consumer会有线程消耗,
如果以异步的方式使用,可以避免Consumer的线程消耗,
但它不能做到像MQ一样暂存请求,压力会堆积在服务Provider那

而MQ会把请求的压力暂缓,让处理者根据自己的节奏处理,但是由于消息是暂存在了它的队列中,
所以这个系统的可靠性会受到这个队列的影响,它是异步单向,发送消息设计为不需要等待消息处理的完成,
所以如果有需要设计为同步返回的功能的话,MQ就会变得不好使用

所以如果是非常着急需要等待返回结果的,又或者说是希望使用方便简单,RPC就比较好用,它的使用基于接口,类似于本地调用,异步编程会相对复杂,比如gRPC。而不希望发送端受限于处理端的速度时,就使用MQ。随着业务增长,也会出现处理端的处理速度不够,从而进行同步调用到异步消息的改造

2.RPC的流程及其协议

① RPC的流程

1
2
3
4
5
6
复制代码1.客户端处理过程中调用client stub(就像调用本地方法一样),传入参数
2.Client stub将参数编组为消息,然后通过系统调用向服务端发送消息
3.客户端本地操作系统将消息从客户端机器发送到服务端机器
4.服务端操作系统将接收到的数据包传递给client stub
5.server stub解组消息为参数
6.server stub再调用服务端的过程,过程执行结果以反方向的相同步骤响应给客户端

stub:分布式计算中的存根是一段代码,它转换在远程过程调用期间Client和server之间传递的参数

② 整个流程中需要注意处理的问题

1
2
3
4
5
复制代码1.client stub和server stub的开发问题
2.参数如何编组为消息,以及如何解组
3.消息如何发送
4.过程结果如何表示,异常情况如何处理
5.如何实现安全的访问控制

③ 核心概念术语

client和server,calls调用,replies响应,services,programs,procedures,version,Marshalling和unmarshalling编组和解组

关于services,programs,procedures,version

一个网络服务由一个或者多个远程程序集构成,而一个远程程序实现一个或多个远程过程。过程与过程参数,结果在程序协议说明书中定义说明,而为兼容程序协议变更,一个服务端可能支持多个版本的远程程序

④ RPC协议

RPC调用过程中需要将参数编组为消息进行发送,接收方需要解组消息为参数,过程处理结果同样需要经过编组解组。消息由哪些部分构成及消息的表示形式就构成了消息协议。RPC调用过程中采用的消息协议成为RPC协议

RPC是规定要做的事,RPC协议规定请求响应消息的格式,在TCP之上我们可以选用或自定义消息协议来完成我们的RPC交互,此时我们可以选用http或者https这种通用的标准协议,也可以根据自身的需要定义自己的消息协议(较多)

RPC框架

RPC框架封装好了参数编组解组,底层网络通信的RPC程序开发框架,让我们可以直接在其基础之上只需要专注于过程代码编写

Java领域的比较常见的RPC框架:webService,Apache的CXF,Axis2,Java自带的jax-ws,微服务常见的dubbo,spring cloud,Apache Thrift,ICE,google的GRPC等

① RPC框架的服务暴露

远程提供者需要以某种形式提供服务调用相关的信息,包括但不限于服务接口定义,数据结构,或者中间态的服务定义文件,例如Thrift的IDL文件,webService的WSDL文件,服务的调用者需要通过一定的途径或者远程服务调用的相关信息,其实简单点说,就是需要告诉别人怎么调用服务

② RPC框架的远程代理对象

代理处理技术:服务调用者用的服务实际是远程服务的本地代理,其实就是通过动态代理来实现

Java里至少提供了两种方式来提供动态代码生成,一种是jdk动态代理,另一种是字节码生成,动态代理相比字节码生成使用起来更方便,但动态代理方式在性能上比字节码要差,而字节码生成在代码可读性上要差很多,所以我们一般都是牺牲一些性能来获得代码可读性和可维护性的提高

③ RPC框架的通信

RPC框架通信和具体的协议是无关的,它可基于HTTP或TCP协议,webService就是基于http协议的RPC,具有更好的跨平台性,但性能却不如基于TCP协议的RPC

NIO其实不一定会比BIO性能要高,NIO只是为了支持高并发而已,特点就是非阻塞的,适合小数据量高并发的场景,大数据包情况下,BIO会更为合适

④ RPC框架的序列化

两个方面会直接影响RPC的性能,一是传输方式,二是序列化

Finally

这一篇全是理论,我们简单过了一遍RPC是什么,三个过程,为什么我们需要它,它的特性和适用场景,RPC的流程及协议定义还有它的框架的一些小知识。也是为了方便下一篇上代码的时候会比较方便阐述

下一篇:从零开始的高并发(八)— RPC框架的简单实现

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

从0到1理解数据库事务(上):并发问题与隔离级别 一、重新理

发表于 2019-08-29

最近准备写一篇关于Spanner事务的分享,所以先分享一些基础知识,涉及ACID、隔离级别、MVCC、锁,由于太长,只好拆分成上下两篇:

  • 上:并发问题与隔离级别
    主要讲事务所要解决的问题、思路,先理解为什么需要事务以及事务并发控制中面临的问题。
  • 下:隔离级别实现——MVCC与锁
    隔离性是为了更好地做到并发控制,事务的并发表现会对业务有直接影响,所以这篇会详细讲如何实现隔离,主要是讲两种主流技术方案——MVCC与锁,理解了MVCC与锁,就可以举一反三地看各种数据库并发控制方案,并理解每种实现能解决的问题以及需要开发者自己注意的并发问题,以更好支撑业务开发。

文章开始前先给一个小思考,考虑一个情况:
像下面这样实现User提现100元,是否一定不会出问题?

  1. Start Transaction
  2. SELECT balance FROM users WHERE user_name=x; (此次读取在Transaction中)
  3. 在代码中判断balance是否大于等于100
  4. 如果小于100元,End Transaction并且返回余额不足提现失败
  5. 如果大于等于100元,则 UPDATE users SET balance = balance - 100 WHERE user_name=x; 然后Commit Transaction,返回提现成功

如果你已经很理解数据库事务了,一定知道什么情况有问题,以及为什么出现这个问题,这篇文章对你太入门,不用继续看。如果不太清楚,那希望你看完上、下两篇就非常理解了,否则就是我写得太烂。

一、重新理解 ACID

1. 数据操作中面临的问题

技术中的所有方案必定是为了解决特定问题,先理解问题再看方案,学起来更简单、理解更深入,所以先从数据库面临的问题说起。
首先,要理解为什么数据库会有事务的需求,先理解数据库要解决的根本问题不是存储,存储问题已经被文件系统解决了,数据库的目的是如何帮助开发者更可靠、更快速、更便利地使用存储,更好地帮助开发者完成业务,业务中一个高频需求是:有一批连续的操作,这一批操作要么全部成功,要么就可以像没有发生过一样,不要由于部分未成功而导致脏数据产生。如果没有事务,我们处理用户下单的业务场景,就要超级多的代码去handle各种错误、清理各种脏数据、避免可能的bug,比如下单成功却由于数据库宕机导致没有扣款。为了提高开发效率、降低开发成本,就需要数据库能提供一种保证:将一组操作看作一个单元,这一单元可以全部成功,在部分失败的情况下,可以完全回滚,就像没有发生,这一组操作称为事务(Transaction)。
但是仅仅做到上面那一点是不够的,因为这一个简单需求,其实引入了另一个问题,请注意重点——“一组操作”,事务中可能存在着多个独立操作,他们组合为一组操作,理解多线程编程的同学一定会马上想到,这就会出现经典的并发问题,多个事务间如果不进行并发控制,就会产生各种意外结果,这不是使用者想要的。

总结一下,数据操作中面临的问题:

  1. 如何将一组操作看作一个整体,要么全部成功,要么全部回滚。
  2. 如何在满足上一条需求的情况下,能够对它进行并发控制,保证不要出现意外结果。

2. 我们需要什么:ACID

ACID 是为了解决上述问题所总结出,为保证事务是正确可靠的,所必须具备的四个特性:

1. 原子性(Atomicity)
事务中的原子性是一个常常被大家误解的特性,因为这个原子性的意思和我们通常语境下的原子性不太一样,大多数时候原子性是指一条不可再分割、不会被中断影响的指令,比如读取一个内存地址的值、将值写回内存地址、redis的SETNX(set if not exists),这些操作都符合我们常说的原子性。
可是事务中的原子性,并不是指事务具有不被中断影响的特点,它仅仅是指,事务中的所有操作应该被看作不可分割的一组指令,任何一个指令不能独立存在,要么全部成功执行,要么全部不发生(也就是回滚)。
还有很多同学对这里所说的“成功执行”有误解,成功执行是指数据库层面的,而不是业务层面的,举个例子,客户购买商品A,可是在购买时,商家刚好下架了商品,那么此时执行 update products set price=100 where product_id=A and status=销售中 ,由于product的status已经变成“下架”,导致被更新的行数为0,这个算成功执行吗?算!数据库不报错、不宕机、正常运行就是成功,更新行数为0是数据库的正常返回结果,这在业务上是失败,在数据库层面是成功,这种情况数据库不会执行回滚,需要程序员判断更新行数,如果为0,手动回滚。
如果数据库由于硬件或者系统问题发生宕机、报错,这样才算是指令执行失败,此时数据库会重试或者直接回滚,然后将错误返回给开发者。
原子性不止为开发者保证了事务的可靠性(不会因为数据库出错而产生脏数据),还能让开发者手动回滚,提供了业务的便利性。

2. 一致性(Consistency)
这个名词也是相当令人困惑,与数据库主从复制中所说的“一致性”不同,主从复制的一致性是指多个副本间是否完成同步、数据相同,而这里的一致性是指事务是否产生非预期中间状态或结果。比如脏读和不可重复读,产生了非预期中间状态,脏写与丢失修改则产生了非预期结果。一致性实际上是由后面的隔离性去进一步保证的,隔离性达到要求,则可以满足一致性。也就是说,隔离不足会导致事务不满足一致性要求,所以务必理解各个隔离级别,才能少写Bug。

3. 隔离性(Isolation)
简单来说,隔离性就是多个事务互不影响,感觉不到对方存在,这个特性就是为了做并发控制。在多线程编程中,如果大家都读写同一块数据,那么久可能出现最终数据不一致,也就是每条线程都可能被别的线程影响了。按理说,最严格的隔离性实现就是完全感知不到其他并发事务的存在,多个并发事务无论如何调度,结果都与串行执行一样。为了达到串行效果,目前采用的方式一般是两阶段加锁(Two Phase Locking),但是读写都加锁效率非常低,读写之间只能排队执行,有时候为了效率,原则是可以妥协的,于是隔离性并不严格,它被分为了多种级别,从高到低分别为:

  • ⬇️可串行化(Serializable)
  • ⬇️可重复读(Read Repeatable)
  • ⬇️已提交读(Read Committed)
  • ⬇️未提交读(Read Uncommitted)

每一个级别都只是指导标准,每个数据库对其的实现都有差异,有的数据库在Read Committed级别时,就已经实现了Read Repeatable的效果,有的数据库干脆不提供Read Uncommitted级别。
在隔离级别为Serializable时,就会感觉到事务像一个完完全全的原子操作,不被任何中断、并发所影响。
很多开发者理解的事务可能就在Serializable级别,大家误以为事务都是可串行化的,其实并不是,大多数的数据库默认隔离级别都不是可串行化,大多数在Read Repeatable或者Read Committed,要是按照可串行化的思维去编程,却用着低于可串行化的隔离级别,就很容易写出导致数据在业务层面不一致的代码,所以开发者一定要理解各个隔离级别及其原理,更好地支撑业务开发,下面会仔细地讲隔离级别及其实现。

4. 持久性(Duration)
这是ACID中最好理解的,即事务成功提交后,对数据的修改永久的,即使系统发生故障,也不会丢失,这里所说的故障,也只是一般错误比如宕机、系统Bug、断电,如果是硬盘损毁,那就没办法,数据一定会丢失。

二、并发问题与隔离级别

在讨论各个隔离级别的实现之前,先看一下在事务并发执行时,隔离不足会导致的问题。

脏写(Dirty Write)

还未提交的事务写了另一个未提交事务所写过的数据,称为脏写,比如:
两个并发执行的事务A、B,A写了x,在A还未提交前,B也写了x,然后A提交,此时虽然B还没有提交,但是A也会发现自己写的x不见了。

脏写

很多地方用“覆盖”去形容脏写,但是我觉得不太适合,因为覆盖暗示了一种先后链条,某个事务写了数据,在昨天就提交了,今天有事务来写同一个数据,可以称之为覆盖,昨天的数据成为历史,但这不是脏写,所以更适合的形容可能是“擦除”,事务发现自己的提交被别人擦除,好像不存在。
脏写是事务一定不允许发生的,所以不管是哪个隔离级别都一定不允许脏写。

脏读(Dirty Read)

由于事务的可回滚特性,因此commit前的任何读写,都有被撤销的可能,假如某个事物读取了还未commit事务的写数据,后来对方回滚了,那么读到的就是脏数据,因为它已经不存在了。

脏读

避免脏读可以采用加锁或者快照读的解决方案。在**已提交读(Read Committed)**级别就可以避免脏读,因为读到的一定是已经Commit的数据。在业务开发中,虽然有未提交读(Read Uncommitted),但是几乎是没有人会用的,读到脏数据一般对业务是很大的伤害,所以有的数据库干脆都不支持未提交读,比如PostgreSQL。

不可重复读(Non-Repeatable Read)

事务A读取一个值,但是没有对它进行任何修改,另一个并发事务B修改了这个值并且提交了,事务A再去读,发现已经不是自己第一次读到的值了,是B修改后的值,就是不可重复读。
简单来说就是第一次读的值,啥都没做,下次读它也有可能发生变化。

不可重复读

一般数据库使用MVCC,在事务的第一条语句开始时生成Read View,事务之后的所有读取,都是基于同一个Read View,以此避免不可重复读问题。

幻读(Phantom)

与不可重复读非常类似,事务A查询一个范围的值,另一个并发事务B往这个范围中插入了数据并提交,然后事务A再查询相同范围,发现多了一条记录,或者某条记录被别的事务删除,事务A发现少了一条记录。

幻读

幻读容易与不可重复读混淆,区别它们只需要记住不可重复读面向的是“同一条记录”,而幻读面向的是“同一个范围”。
MVCC虽然使用快照的方式解决了不可重复读,但是还是不能避免幻读,幻读需要通过范围锁解决,可能大家会觉得很奇怪,为什么快照读无法避免幻读,这个会在下一篇文章中详细讲。

SQL标准中有对于各个隔离级别所允许出现的问题作出规定:

SQL标准

除了以上4个问题外,下面还有3个问题,更偏向业务层面,不过也是由于隔离不足引起的:

读偏差(Read Skew)

Skew可以理解为不一致,因此读偏差可以理解为读结果违反业务一致性,比如X、Y两个账户余额都为50,他们总和为100,事务A读X余额为50,然后事务B从X转账50到Y然后提交,事务A在B提交后读Y发现余额为100,那么它们总和变成了150,此时违反业务一致性。

Read Skew

写偏差(Write Skew)

写偏差可以理解为事务commit之前写前提被破坏,导致写入了违反业务一致性的数据,网上有个很好的简称为写前提困境,也就是读出某些数据,作为另一些写入的前提条件,但是在提交前,读入的数据就已被别的事务修改并提交,这个事务并不知道,然后commit了自己的另一些写入,写前提在commit前就被修改,导致写入结果违反业务一致性。
写偏差发生在写前提与写入目标不相同的情境下。
这是业务开发中最容易出错地方,如果开发者不太理解隔离级别,也不知道目前使用的是哪个隔离级别,很可能写出有写偏差的代码,造成业务不一致。
举个例子:
信用卡系统对不同等级的会员有积分加成,3级会员则每次都3倍积分,同时,会有定时任务检查当积分不满足要求时,就会降级。
首先,会员进行了刷卡消费,此时要计算积分,开启了事务A,读到会员等级为3,与此同时定时任务也开始了,读到会员积分为2800,已经不满足3000分应该降级为2级,然后将会员等级降级为2并且commit,由于事务A读到的等级为3,它还是按照3倍积分为会员增加了积分,会员赚了,多亏那个程序员不理解他使用的事务隔离级别,出现了业务不一致。

Write Skew

丢失更新(Lost Updates)

由于未提交事务之间看不到对方的修改,因此都以一个旧前提去更新同一个数据,导致最后的提交结果是错误值。
假设有支付宝账户X,余额100元,事务A、B同时向X分别充值10元、20元,最后结果应该为130元,但是由于丢失更新,最后是110元。

丢失更新

丢失更新与写偏差很相似,都是由于写前提被改变,他们区别是,丢失更新是在同一个数据的最终不一致,而写偏差的冲突不在同一个数据,是在不同数据中的最终不一致。
这一篇讲到的所有问题都会在下一篇讲隔离级别实现中得到解决,理解隔离级别实现,有助于选择合适的隔离级别,或者在代码层面有意识地避免隔离级别不足所带来的问题。

参考资料

  • 《MySQL 是怎样运行的:从根儿上理解 MySQL》
  • 《数据库事务处理的艺术:事务管理与并发控制》
  • 《数据库事务、隔离级别和锁》(博客)
  • 《SQL隔离级别》(博客)
  • 《开发者都应该了解的数据库隔离级别》(博客)
  • 《A beginner’s guide to read and write skew phenomena》(博客)

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Linux系统搭建Java环境【JDK、Tomcat、MyS

发表于 2019-08-29

前言:所有项目在完成开发后都会部署上线的,一般都是用Linux系统作为服务器的,很少使用Windows Server(大多数项目的开发都是在Windows桌面系统完成的),一般有专门负责上线的人员。作为一个开发人员当然也能玩转Linux,毕竟Linux是很多IT人员的喜好。
  下面就使用纯净的Linux系统完成Java Web项目的部署。

  1. 安装一个VMware workstation
  2. 在虚拟机上面安装CentOS系统 (这里使用的CentOS6.7 64位)
  3. 安装Java开发环境JDK 1.8
  4. Tomcat8.5 其他服务器也可以
  5. 数据库安装MySQL5.7 (Oracle、MS Server也可以)
    在这里插入图片描述

一、搭建Linux环境

  1.下载并安装一个VMware workstation, 这个是虚拟机的平台(自行度娘下载~),虚拟机是在后面要在里面搭建Linux系统。

  2.下载一个centos安装包,linux版本有多种,比如说redhat、ubuntu、deepin、BT3,个人比较习惯使用centos,这里下载一个 CentOS-6.4-i386-bin-DVD1.iso,6.4版本的。

  3.新建一个虚拟机,并把这个安装包导入进去,记住期间步骤有一步是需要建一个用户和设置密码,这里的用户是linux环境的普通账户,但是密码是根账户root和这个普通用户共用的密码(这节的教程也在我的博客上一节有详细的步骤)

  4.安装完成后,是图形化界面,一般我们要用到的是命令行界面,所以这里可以用快捷键 CTRL+ALT+F2,就可以切换到命令界面了。(命令行界面有几种方式可以进入,这边gqzdev推荐的是==Xshell5== 比较方便使用。需要的可以留言!)

XShell5 Xftp5工具(其他工具可以!)

在这里插入图片描述

XShell 5

  5.用命令ifconfig查看IP 地址,然后可以考虑用Xshell来连接虚拟机,这样操作命令会比较方便,不用频繁的切换出来或者切换出去。

  上面的安装及配置有问题的可以自行百度,网上资源很多。主要是搭建好Linux系统环境…..

二、JDK安装

  1. 首先下载一个JDK版本,我这里下载的是==jdk-8u221-linux-x64.gz== 这个版本。(JDK1.8目前用得比较多!)
    也可以自行到JDK官网下载 www.oracle.com/technetwork…
  2. 用xftp上传到linux环境中去。上传的路径为: /usr/java

在这里插入图片描述

将刚才下载好的 压缩包解压,得到一个jdk1.8.0_221的文件夹。使用tar -xzvf命令解压
然后用Xftp上传至linux环境中去。有两种方式
第一种,通过功能栏红框内的“向右传输” 传过去
第二种,直接拖动文件夹,拖过去。
非常重要的一点,linux环境中的路径 是 /usr/java…..在这里插入图片描述

JDK传过去之后,现在就开始着手配置JDK环境了
3. 配置JDK环境,需要给予这个文件夹最高的权限,为了后续的方便,这边直接赋予最高权限
首先,通过XFTP打开Xshell(打开方式上一节有讲到)
1)切换到”/“目录下 cd ../..
2)切换到local路径下 cd /usr/java
3)赋予JDK文件最高权限 chmod 777 -R jdk1.8.0_221
4. 配置JDK环境变量:
1)切换到”/“目录下 cd ../..
2)切换到etc路径下 cd etc
3)编辑profile文件 vim profile
4)按下键盘的 insert 键,进入编辑模式
5)配置JDK的环境变量,在profile中输入如下内容(空白位置填入即可,添加在最后):建议配置变量参考下面的配置方式
export JAVA_HOME=/usr/java/jdk1.8.0_221 【特别说明:这个就是你的jdk的安装路径!!!不要弄错了!要以你自己的路径为准!】
6) 按住键盘的ESC键,然后输入 :wq,就保存了你刚刚设置的环境变量
7)让你刚刚设置的环境变量生效 source profile
8)如何查看你的JDK是否配置完成呢?输入命令 java -version,看到下面的对应安装JDK版本的截图,就说明你配置成功了!

在etc下面的profile里面最后面配置export…..

1
2
3
复制代码export JAVA_HOME=/usr/java/jdk1.8.0_221
export CLASSPATH=$CLASSPATH:$JAVA_HOME/lib:$JAVA_HOME/jre/lib
export PATH=$JAVA_HOME/bin:$JAVA_HOME/jre/bin:$PATH:$HOME/bin

在这里插入图片描述

在这里插入图片描述

通过java -version检查是否安装成功! 到此,JDK的配置就算完成了
在这里插入图片描述

三、服务器安装

Tomcat8.5下载 tomcat.apache.org/download-80…

在这里插入图片描述

下面来介绍下Tomcat的配置

  1. 首先下载 一个tomcat版本,我这里用的是apache-tomcat-6.0.37版本apache-tomcat-6.0.37.tar.gz是对应的压缩包。
  2. 可以用XFTP 直接把已经解压的apache-tomcat-6.0.37上传到 /usr/local路径,上传完毕,然后这里就需要对环境变量进行配置,然后后面的tomcat才会顺利启动
  3. 配置tomcat环境变量:
    1)切换到”/“目录下 cd ../..
    2)换到etc路径下 cd etc
    3)编辑profile文件 vi profile
    4)按下键盘的 i 键,进入编辑模式
    5)配置tomcat的环境变量,在profile中输入如下内容(配置JDK环境变量后面添加多这行即可):建议配置变量参考下面红色字体的配置方式
    export CATALINA_HOME=/usr/local/apache-tomcat-6.0.37 【特别注意:这里就是就是配置你的tomcat的安装路径!不要弄错了!】,如下图所示:
    7)编辑完毕后,按住键盘的ESC键,然后输入 :wq,就保存了你刚刚设置的tomcat环境变量
    8)让你刚刚设置的环境变量生效 source profile

注意:上面的JDK配置及tomcat路径的配置可能存在点问题 ,建议启用下面的配置方式

配置JDK环境变量及配置tomcat路径:
1)首先回到home路径,即顶级目录,命令: cd ~
2)然后打开并编辑环境变量的文件,输入命令: vi .bashrc
3)进入环境变量编辑环境,加入如下环境变量:

1
2
3
4
复制代码export JAVA_HOME=/usr/local/jdk1.6.0_45
export CLASSPATH=$CLASSPATH:$JAVA_HOME/lib:$JAVA_HOME/jre/lib
export PATH=$JAVA_HOME/bin:$JAVA_HOME/jre/bin:$PATH:$HOMR/bin
export CATALINA_HOME=/usr/local/apache-tomcat-6.0.37

4)编辑完毕,就保存内容,命令: 先按ESC,然后输入 ”:wq“就可以保存了。
5)配置完毕后要让配置生效,用命令:source ~/.bashrc   

4.可以启动一下tomcat,看是否配置成功了:

1)切换到”/“目录下 cd ../..
2 ) 切换到启动命令所在的bin路径:cd /usr/local/apache-tomcat-6.0.37/bin
3 ) 输入tomcat 启动命令 ./startup.sh,如果遇到下面的提示,就说明你对bin文件里面的命令操作权限不够,就需要赋予权限:
4 ) 返回到bin的上级目录 cd ..
5 ) 赋予 bin文件的最高权限 chmod 777 -R bin
6 ) 切换到bin路径下 cd bin
7 ) 然后再次执行tomcat启动命令: ./startup.sh,出现如下截图,则表明启动成功。
8 ) 一般默认的端口则是8080,所以直接在你的PC端电脑的浏览器输入 你的ip地址加上端口号,即可以访问到tomcat的首页了。 http://【你的linux服务器IP地址】:8080
PS:ip地址 是你Linux服务器的ip地址,如何获取?—>在Linux中输入ifconfig 就可以获取到了!
10) 但是任何配置都不会这么轻松就配置成功的,会出现防火墙关闭或者端口被占用的问题,这里我们可以切换到 tomcat下的bin 目录,执行下面这个命令,查看tomcat日志: ./catalina.sh run, 一般日志格式如下:
注:如果访问不了,可以尝试关闭防火墙,在Linux下输入命令: service iptables stop,然后再访问就可以了!
11)在浏览器输入IP地址加端口号,如果看到tomcat 的首页,则表明成功了,如下所示:

在这里插入图片描述

四、数据库

安装开源数据库MySQL

  1. 选择yum安装
  2. 通过tar.gz压缩包进行安装

4.1 yum安装

  校验当系统是否安装mysql:

1
复制代码rpm -qa | grep mysql

  卸载mysql:

1
2
复制代码// 强力删除模式,如果使用上面命令删除时,提示有依赖的其它文件,则用该命令可以对其进行强力删除
rpm -e --nodeps

  三行命令:

1
2
3
复制代码yum install mysql
yum install mysql-server
yum install mysql-devel

  也可以一行:

1
复制代码yum install -y mysql mysql-server mysql-devel

在这里插入图片描述

4.2 通过tar.gz压缩包进行安装

操作系统:Centos6.4 64位
工具:Xftp5、Xshell5
安装软件:mysql5.7
说明:使用官方编译好的二进制文件进行安装

在这里插入图片描述

  1. 移动安装文件到指定目录

在这里插入图片描述

  1. 检查是否安装Mysql
1
复制代码rpm -qa | grep mysql
  1. 解压
1
复制代码tar -xvzf mysql-5.7.22-linux-glibc2.12-x86_64.tar.gz
1
2
复制代码#重命名mysql57
[root@VM_0_17_centos mysql]# mv mysql-5.7.27-linux-glibc2.12-x86_64 mysql57

在这里插入图片描述

进行到这里说明:
 将mysql的压缩包解压到/usr/mysql/目录下面,并且rm重命名为mysql57
  此时数据库的文件目录为 ==/usr/mysql/mysql57==

  1. 设置mysql目录访问权限,用户组
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
复制代码#将mysql目录访问权限赋为myql用户
[root@VM_0_17_centos mysql57]# chown -R mysql /usr/mysql/mysql57

#改变mysql目录的用户组属于mysql组
[root@VM_0_17_centos mysql57]# chgrp -R mysql /usr/mysql/mysql57

#查看mysql目录下所有的目录及文件夹所属组合用户
[root@VM_0_17_centos mysql57]# cd /usr/mysql/mysql57

[root@VM_0_17_centos mysql57]# ll
total 56
drwxr-xr-x 2 mysql mysql 4096 Aug 11 21:24 bin
-rw-r--r-- 1 mysql mysql 17987 Dec 28 2017 COPYING
drwxr-xr-x 2 mysql mysql 4096 Aug 11 21:40 data
drwxr-xr-x 2 mysql mysql 4096 Aug 11 21:24 docs
drwxr-xr-x 3 mysql mysql 4096 Aug 11 21:23 include
drwxr-xr-x 5 mysql mysql 4096 Aug 11 21:24 lib
drwxr-xr-x 4 mysql mysql 4096 Aug 11 21:23 man
-rw-r--r-- 1 mysql mysql 2478 Dec 28 2017 README
drwxr-xr-x 28 mysql mysql 4096 Aug 11 21:24 share
drwxr-xr-x 2 mysql mysql 4096 Aug 11 21:24 support-files

权限被修改

  1. ==配置mysql==(重点部分)

创建以下文件,设置访问权限,用于mysql配置中

第一步:==创建文件/tmp/mysql.sock==。并设置权限

创建文件

1
2
3
4
5
6
7
8
9
10
11
12
复制代码
[root@VM_0_17_centos mysql57]# mkdir tmp
[root@VM_0_17_centos mysql57]# cd tmp

[root@VM_0_17_centos tmp]# ll
total 0

[root@VM_0_17_centos tmp]# touch mysql.sock

[root@VM_0_17_centos tmp]# ll
total 0
-rw-r--r-- 1 root root 0 Aug 11 21:59 mysql.sock

设置权限

1
2
3
复制代码[root@VM_0_17_centos tmp]# chown -R mysql:mysql /usr/mysql/mysql57/tmp/mysql.sock

[root@VM_0_17_centos tmp]# chmod 755 /usr/mysql/mysql57/tmp/mysql.sock

第二步:==创建/log/mysqld.log==。并设置权限

1
2
3
4
5
6
7
8
9
复制代码[root@VM_0_17_centos mysql57]# mkdir log
[root@VM_0_17_centos mysql57]# cd log

[root@VM_0_17_centos log]# ll
total 0

[root@VM_0_17_centos log]# touch mysqld.log
[root@VM_0_17_centos log]# chown -R mysql:mysql /usr/mysql/mysql57/log/mysqld.log
[root@VM_0_17_centos log]# chmod 755 /usr/mysql/mysql57/log/mysqld.log

如果出错,说明路径没有写全,要写绝对路径

第三步:==创建/tmp/mysqld.pid==。并设置权限

1
2
3
4
复制代码[root@VM_0_17_centos log]# cd ../tmp
[root@VM_0_17_centos tmp]# touch mysqld.pid
[root@VM_0_17_centos tmp]# chown -R mysql:mysql /usr/local/mysql57/tmp/mysqld.pid
[root@VM_0_17_centos tmp]# chmod 755 /usr/local/mysql57/tmp/mysqld.pid
  1. ==初始化mysql==
1
复制代码[root@VM_0_17_centos mysql57]# bin/mysqld --initialize --user=mysql --basedir=/usr/mysql/mysql57/ --datadir=/usr/local/mysql57/data/

可能会报错。报错信息

1
复制代码 bin/mysqld: error while loading shared libraries: libnuma.so.1: cannot open shared object file: No such file or directory

  解决方法:
原因:
yum安装的是libnuma.so.1,但安装时默认安装的是32的,而db2需要的是64位的

  • 1.如果已经安装了libnuma.so.1,先yum remove libnuma.so.1
1
复制代码[root@VM_0_17_centos mysql57]# yum remove libnuma.so.1
  • 2.安装依赖包 yum -y install numactl.x86_64
1
复制代码[root@VM_0_17_centos mysql57]# yum -y install numactl.x86_64
  • 3.安装完成后重新,执行
1
复制代码[root@VM_0_17_centos mysql57]# bin/mysqld --initialize --user=mysql --basedir=/usr/mysql/mysql57/ --datadir=/usr/local/mysql57/data/

  ==安装成功== 临时密码

在这里插入图片描述

 配置SSL参数(在mysql目录下)

1
复制代码[root@VM_0_17_centos mysql57]# bin/mysql_ssl_rsa_setup --datadir=/usr/mysql/mysql57/data/

 由于mysql-5.7.23版本my.cnf不在/support-files下,故我们==创建my.cnf==文件。

1
2
复制代码[root@VM_0_17_centos mysql57]# touch /etc/my.cnf
[root@VM_0_17_centos mysql57]# vim /etc/my.cnf

  复制如下内容(每个人安装路径可能不一样,==需要修改==):

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码[mysqld]
character_set_server=utf8
init_connect='SET NAMES utf8'
basedir=/usr/local/mysql57
datadir=/usr/local/mysql57/data
port = 3306
socket=/tmp/mysql.sock
log-error=/usr/local/mysql57/log/mysqld.log
pid-file=/usr/local/mysql57/tmp/mysqld.pid
#表名不区分大小写
lower_case_table_names = 1
max_connections=5000
sql_mode=STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION

配置完成

  1. 启动mysql
  • 方式一
1
复制代码[root@VM_0_17_centos mysql57]# bin/mysqld_safe --user=mysql &

查看mysql的运行情况

1
复制代码[root@VM_0_17_centos mysql57]# ps -ef |grep mysql

第一我没有成功,因为我有个地方出问题了。我在方式二配置成功。

  • 方式二
    配置mysql自动启动(可根据需要配置)
1
2
复制代码[root@VM_0_17_centos mysql57]# cp support-files/mysql.server /etc/init.d/mysql
[root@VM_0_17_centos mysql57]# vim /etc/init.d/mysql

添加配置(i 进入编辑;esc–> :wq保存退出)

若配置了mysql自启动方式则可以使用服务方式启动mysql

1
2
3
4
5
6
7
8
9
10
11
复制代码#查看mysql状态
/etc/init.d/mysql status 或者 service mysql status
#启动mysql
/etc/init.d/mysql start 或者 service mysql start
#停止mysql
/etc/init.d/mysql stop 或者 service mysql stop
#重新启动mysql
/etc/init.d/mysql restart 或者 service mysql restart
查看mysql服务说明启动成功
ps -ef|grep mysql
启动mysql
1
复制代码[root@VM_0_17_centos tmp]# service mysql start

Starting MySQL. ERROR! The server quit without updating PID file (/usr/local/mysql57/tmp/mysqld.pid).
==报错了,上面说没有/usr/local/mysql57/tmp/mysqld.pid。==

  解决方案:

a) 创建文件/usr/local/mysql57/tmp/mysqld.pid

b) 修改权限

修改存放mysqld.pid文件目录的权限

1
2
3
复制代码chown -R mysql /usr/local/mysql57/tmp
chgrp -R mysql /usr/local/mysql57/tmp
chmod 777 /usr/local/mysql57/tmp

重新启动成功(如果还不行,就是中间某个步骤写错了。或者直接把mysql目录权限赋为777)

1
2
复制代码[root@VM_0_17_centos tmp]# /etc/init.d/mysql start
Starting MySQL. SUCCESS!
  1. 配置mysql到环境变量
1
复制代码[root@VM_0_17_centos tmp]# vim /etc/profile

在这里插入图片描述

1
2
复制代码export MYSQL57_HOME=/usr/mysql/mysql57/bin 
export PATH=$PATH:${MYSQ57_HOME}

这个地方是冒号 ( :)

设置环境变量立即生效

1
复制代码[root@VM_0_17_centos tmp]# source /etc/profile
  1. mysql忘记密码

切换到mysql下的bin目录

1
2
3
4
5
复制代码[root@localhost bin]# ./mysql -u root -p
Enter password:

ERROR 1045 (28000): Access denied for user 'root'@'localhost' (using password: YES)
ERROR 1045 (28000): Access denied for user 'root'@'localhost' (using password: YES)

密码错误。。而且之前安装的密码也忘记了

  解决方案:

  • 第一步:跳过MySQL的密码认证过程

(注:windows下修改的是my.ini)

1
复制代码[root@VM_0_17_centos bin]# vim /etc/my.cnf

在[mysqld]后面任意一行添加“==skip-grant-tables== ”用来跳过密码验证的过程,如下图所示:

在这里插入图片描述

保存并退出(esc–> :wq)

  • 第二步:重启mysql
1
2
3
复制代码[root@VM_0_17_centos bin]# /etc/init.d/mysql restart
Shutting down MySQL.. SUCCESS!
Starting MySQL. SUCCESS!
  • 第三步:登录mysql

进入mysql/bin目录,启动mysql

1
复制代码[root@VM_0_17_centos bin]# ./mysql

启动成功

  • 第四步:使用sql语句修改密码
1
2
3
4
复制代码mysql> use mysql;
mysql> update user set authentication_string=password(" 你的新密码 ") where user="root";
mysql> flush privileges;
mysql> quit
  • 第五步:重新编辑my.cnf

去掉[mysqld]后面的“skip-grant-tables”

1
复制代码[root@VM_0_17_centos bin]# vim /etc/my.cnf

在这里插入图片描述

重启mysql

1
2
3
复制代码[root@VM_0_17_centos bin]# /etc/init.d/mysql restart
##注意有的版本需要使用mysqld命令
[root@VM_0_17_centos bin]# /etc/init.d/mysqld restart
  1. 设置mysql远程登录

先本地登录mysql

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码[root@localhost bin]# ./mysql -uroot -p
Enter password:
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 2
Server version: 5.7.21

Copyright (c) 2000, 2018, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
  • 1.报错

突然报错

1
复制代码ERROR 1820 (HY000): You must reset your password using ALTER USER statement before executing this statement.
1
2
复制代码mysql> use mysql
ERROR 1820 (HY000): You must reset your password using ALTER USER statement before executing this statement.

==解决方案:需要重新修改一下密码==

1
2
3
复制代码mysql> alter user 'root'@'localhost' identified by '修改的密码';
mysql> flush privileges;
mysql> quit;
  • 2.继续配置
1
2
3
4
5
6
7
复制代码mysql> use mysql;
#改表法
mysql> update user set host='%' where user='root';
#授权法
mysql> GRANT ALL PRIVILEGES ON *.* TO 'myuser'@'%' IDENTIFIED BY 'mypwd' WITH GRANT OPTION;
mysql> FLUSH PRIVILEGES;
mysql> quit;
  • 3.重启mysql
1
2
3
复制代码[root@VM_0_17_centos bin]# service mysql restart;
##注意有的版本需要使用mysqld命令
[root@VM_0_17_centos bin]# /etc/init.d/mysqld restart
  • 4.设置防火墙

a)配置防火墙开启3306端口

1
2
3
4
5
6
7
8
9
10
复制代码[root@VM_0_17_centos bin]# /sbin/iptables -I INPUT -p tcp --dport 3306-j ACCEPT

[root@VM_0_17_centos bin]# /etc/rc.d/init.d/iptables save
iptables: Saving firewall rules to /etc/sysconfig/iptables: [ OK ]

[root@VM_0_17_centos bin]# /etc/rc.d/init.d/iptables restart
iptables: Setting chains to policy ACCEPT: filter [ OK ]
iptables: Flushing firewall rules: [ OK ]
iptables: Unloading modules: [ OK ]
iptables: Applying firewall rules: [ OK ]

b)临时关闭防火墙

1
复制代码[root@VM_0_17_centos bin]# service iptables stop

c)永久关闭防火墙

重启后永久生效

1
复制代码[root@VM_0_17_centos  bin]# chkconfig iptables off

使用Navicat工具连接即可

在这里插入图片描述

5.部署项目

下面是将项目打包部署到服务器上面了,有空的话我会接着写下面的内容!!

  • 通过打包成war文件部署

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

优化代码的几个小技巧

发表于 2019-08-29

前言

最近看了《重构-改善既有代码的设计》这本书,总结了优化代码的几个小技巧,给大家分享一下。

提炼函数(适当抽取小函数)

定义

提炼函数就是将一段代码放进一个独立函数中,并让函数名称解释该函数用途。

一个过于冗长的函数或者一段需要注释才能让人理解用途的代码,可以考虑把它切分成一个功能明确的函数单元,并定义清晰简短的函数名,这样会让代码变得更加优雅。

优化例子

提炼函数之前:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
复制代码    private String name;
private Vector<Order> orders = new Vector<Order>();

public void printOwing() {
//print banner
System.out.println("****************");
System.out.println("*****customer Owes *****");
System.out.println("****************");

//calculate totalAmount
Enumeration env = orders.elements();
double totalAmount = 0.0;
while (env.hasMoreElements()) {
Order order = (Order) env.nextElement();
totalAmount += order.getAmout();
}

//print details
System.out.println("name:" + name);
System.out.println("amount:" + totalAmount);
}

提炼函数之后:

以上那段代码,可以抽成print banner,calculate totalAmount,print details三个功能的单一函数,如下:

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
复制代码    private String name;
private Vector<Order> orders = new Vector<Order>();

public void printOwing() {

//print banner
printBanner();
//calculate totalAmount
double totalAmount = getTotalAmount();
//print details
printDetail(totalAmount);
}

void printBanner(){
System.out.println("****************");
System.out.println("*****customer Owes *****");
System.out.println("****************");
}

double getTotalAmount(){
Enumeration env = orders.elements();
double totalAmount = 0.0;
while (env.hasMoreElements()) {
Order order = (Order) env.nextElement();
totalAmount += order.getAmout();
}
return totalAmount;
}

void printDetail(double totalAmount){
System.out.println("name:" + name);
System.out.println("amount:" + totalAmount);
}

内联函数(适当去除多余函数)

定义

内联函数就是在函数调用点插入函数本体,然后移除该函数。

上一小节介绍了提炼函数代码优化方式,以简短清晰的小函数为荣。但是呢,小函数是不是越多越好呢?肯定不是啦,有时候你会遇到某些函数,其内部代码和函数名称同样清晰,这时候呢你可以考虑内联函数优化一下了。

优化例子

内联函数之前

1
2
3
4
5
6
复制代码    int getRating(){
return moreThanFiveDeliveries() ? 2 : 1;
}
boolean moreThanFiveDeliveries(){
return numberOfLateDeliveries >5;
}

内联函数之后

1
2
3
复制代码  int getRating(){
return numberOfLateDeliveries >5 ? 2 : 1;
}

内联临时变量(去除多余临时变量)

定义

内联临时变量将所有对该变量的引用动作,替换为对它赋值的那个表达式自身。

优化例子

内联临时变量之前

1
2
复制代码double basePice = anOrder.basePrice();
return basePice >888;

内联临时变量之后

1
复制代码 return anOrder.basePrice() >888;

引入解释性变量

定义

引入解释性变量 就是将该复杂表达式(或其中一部分)的结果放进一个临时变量,以此变量名称来解释表达式用途。

有些表达式可能非常复杂难于阅读,在这种情况下,临时变量可以帮助你将表达式分解为可读的形式。

在比较复杂的条件逻辑中,你可以用引入解释性变量将每个条件子句提炼出来,以一个良好命名的临时变量来解释对应条件子句的意义。

优化例子

引入解释性变量之前

1
2
3
4
5
复制代码if ((platform.toUpperCase().indexOf("mac") > -1) &&
(brower.toUpperCase().indexOf("ie") > -1) &&
wasInitializes() && resize > 0) {
......
}

引入解释性变量之后

1
2
3
4
5
6
7
复制代码final boolean isMacOS = platform.toUpperCase().indexOf("mac") > -1;
final boolean isIEBrowser = brower.toUpperCase().indexOf("ie") > -1;
final boolean wasResized = resize > 0;

if (isMacOS && isIEBrowser && wasInitializes() && wasResized) {
......
}

以字面常量取代魔法数

定义

创造一个常量,根据其意义为它命名,并将上述的字面数值替换为这个常量。

所谓魔法数是指拥有特殊意义,却又不能明确表现出这种意义的数字。如果你需要在不同的地点引用同一个逻辑数,每当该数字要修改时,会特别头疼,因为很可能会改漏。而字面常量取代魔法数可以解决这个头疼问题。

优化例子

以字面常量取代魔法数之前

1
2
3
复制代码double getDiscountPrice(double price){
return price * 0.88;
}

以字面常量取代魔法数之后

1
2
3
4
5
复制代码 static final double DISCOUNT_CONSTANT=0.88;

double getDiscountPrice(double price){
return price * DISCOUNT_CONSTANT;
}

用多态替代switch语句

定义

用多态替换switch语句 就是利用Java面向对象的多态特点,使用state模式来替换switch语句。

优化例子

用多态替换switch语句之前

1
2
3
4
5
6
7
8
复制代码 int getArea() {
switch (shape){
case SHAPE.CIRCLE:
return 3.14 * _r * _r; break;
case SHAPE.RECTANGEL;
return width *,heigth;
}
}

用多态替换switch语句之后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码 class Shape {
int getArea(){};
}

class Circle extends Shape {
int getArea() {
return 3.14 * r * r;
}
}

class Rectangel extends Shape {
int getArea() {
return width * heigth;
}
}

将过多的参数对象化

定义

将过多的参数对象化就是把涉及过多参数封装成一个对象传参。

一个方法有太多传参时,即难以阅读又难于维护。尤其针对dubbo远程调用这些方法,如果有过多参数,增加或者减少一个参数,都要修改接口,真的坑。如果把这些参数封装成一个对象,就很好维护了,不用修改接口。

优化例子

将过多的参数对象化之前:

1
复制代码public int register(String username,String password,Integer age,String phone);

将过多的参数对象化之后:

1
2
3
4
5
6
7
8
复制代码 public int register(RegisterForm from );

class RegisterForm{
private String username;
private String password;
private Integer age;
private String phone;
}

参考与感谢

  • 《重构-改善既有代码的设计》

个人公众号

  • 如果你是个爱学习的好孩子,可以关注我公众号,一起学习讨论。
  • 如果你觉得本文有哪些不正确的地方,可以评论,也可以关注我公众号,私聊我,大家一起学习进步哈。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

HashMap和Hashtable的详细区别

发表于 2019-08-27

HashMap和Hashtable的详细区别

一、简述:

1.安全性

Hashtable是线程安全,HashMap是非线程安全。HashMap的性能会高于Hashtable,我们平时使用时若无特殊需求建议使用HashMap,在多线程环境下若使用HashMap需要使用Collections.synchronizedMap()方法来获取一个线程安全的集合(Collections.synchronizedMap()实现原理是Collections定义了一个SynchronizedMap的内部类,这个类实现了Map接口,在调用方法时使用synchronized来保证线程同步

2.是否可以使用null作为key

HashMap可以使用null作为key,不过建议还是尽量避免这样使用。HashMap以null作为key时,总是存储在table数组的第一个节点上。而Hashtable则不允许null作为key

3.继承了什么,实现了什么

HashMap继承了AbstractMap,HashTable继承Dictionary抽象类,两者均实现Map接口

4.默认容量及如何扩容

HashMap的初始容量为16,Hashtable初始容量为11,两者的填充因子默认都是0.75。HashMap扩容时是当前容量翻倍即:capacity 2,Hashtable扩容时是容量翻倍+1即:capacity (2+1)

6.底层实现

HashMap和Hashtable的底层实现都是数组+链表结构实现

7.计算hash的方法不同

Hashtable计算hash是直接使用key的hashcode对table数组的长度直接进行取模
HashMap计算hash对key的hashcode进行了二次hash,以获得更好的散列值,然后对table数组长度取模

二、详述:

参考文章:www.cnblogs.com/xinzhao/p/5…

HashMap和HashTable有什么不同?在面试和被面试的过程中,我问过也被问过这个问题,也见过了不少回答,今天决定写一写自己心目中的理想答案。

代码版本

JDK每一版本都在改进。本文讨论的HashMap和HashTable基于JDK 1.7.0_67。源码见这里

1. 时间

HashTable产生于JDK 1.1,而HashMap产生于JDK 1.2。从时间的维度上来看,HashMap要比HashTable出现得晚一些。

2. 作者

以下是HashTable的作者:

1
2
3
4
5
6
复制代码
以下代码及注释来自java.util.HashTable

* @author Arthur van Hoff
* @author Josh Bloch
* @author Neal Gafter

以下是HashMap的作者:

1
2
3
4
5
6
7
复制代码
以下代码及注释来自java.util.HashMap

* @author Doug Lea
* @author Josh Bloch
* @author Arthur van Hoff
* @author Neal Gafter

可以看到HashMap的作者多了大神Doug Lea。不了解Doug Lea的,可以看这里。

3. 对外的接口(API)

HashMap和HashTable都是基于哈希表来实现键值映射的工具类。讨论他们的不同,我们首先来看一下他们暴露在外的API有什么不同。

3.1 Public Method

下面两张图,我画出了HashMap和HashTable的类继承体系,并列出了这两个类的可供外部调用的公开方法。

HashMap


HashTable

从图中可以看出,两个类的继承体系有些不同。虽然都实现了Map、Cloneable、Serializable三个接口。但是HashMap继承自抽象类AbstractMap,而HashTable继承自抽象类Dictionary。其中Dictionary类是一个已经被废弃的类,这一点我们可以从它代码的注释中看到:

1
2
3
4
5
复制代码
以下代码及注释来自java.util.Dictionary

* <strong>NOTE: This class is obsolete. New implementations should
* implement the Map interface, rather than extending this class.</strong>

同时我们看到HashTable比HashMap多了两个公开方法。一个是elements,这来自于抽象类Dictionary,鉴于该类已经废弃,所以这个方法也就没什么用处了。另一个多出来的方法是contains,这个多出来的方法也没什么用,因为它跟containsValue方法功能是一样的。代码为证:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
复制代码
以下代码及注释来自java.util.HashTable

public synchronized boolean contains(Object value) {
if (value == null) {
throw new NullPointerException();
}

Entry tab[] = table;
for (int i = tab.length ; i-- > 0 ;) {
for (Entry<K,V> e = tab[i] ; e != null ; e = e.next) {
if (e.value.equals(value)) {
return true;
}
}
}
return false;
}

public boolean containsValue(Object value) {
return contains(value);
}

所以从公开的方法上来看,这两个类提供的,是一样的功能。都提供键值映射的服务,可以增、删、查、改键值对,可以对建、值、键值对提供遍历视图。支持浅拷贝,支持序列化。

3.2 Null Key & Null Value

HashMap是支持null键和null值的,而HashTable在遇到null时,会抛出NullPointerException异常。这并不是因为HashTable有什么特殊的实现层面的原因导致不能支持null键和null值,这仅仅是因为HashMap在实现时对null做了特殊处理,将null的hashCode值定为了0,从而将其存放在哈希表的第0个bucket中。我们一put方法为例,看一看代码的细节:

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
复制代码
以下代码及注释来自java.util.HashTable

public synchronized V put(K key, V value) {

// 如果value为null,抛出NullPointerException
if (value == null) {
throw new NullPointerException();
}

// 如果key为null,在调用key.hashCode()时抛出NullPointerException

// ...
}


以下代码及注释来自java.util.HasMap

public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
// 当key为null时,调用putForNullKey特殊处理
if (key == null)
return putForNullKey(value);
// ...
}

private V putForNullKey(V value) {
// key为null时,放到table[0]也就是第0个bucket中
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(0, null, value, 0);
return null;
}

4. 实现原理

本节讨论HashMap和HashTable在数据结构和算法层面,有什么不同。

4.1 数据结构

HashMap和HashTable都使用哈希表来存储键值对。在数据结构上是基本相同的,都创建了一个继承自Map.Entry的私有的内部类Entry,每一个Entry对象表示存储在哈希表中的一个键值对。

Entry对象唯一表示一个键值对,有四个属性:

-K key 键对象
-V value 值对象
-int hash 键对象的hash值
-Entry entry 指向链表中下一个Entry对象,可为null,表示当前Entry对象在链表尾部

可以说,有多少个键值对,就有多少个Entry对象,那么在HashMap和HashTable中是怎么存储这些Entry对象,以方便我们快速查找和修改的呢?请看下图。

HashMapDataStructure

上图画出的是一个桶数量为8,存有5个键值对的HashMap/HashTable的内存布局情况。可以看到HashMap/HashTable内部创建有一个Entry类型的引用数组,用来表示哈希表,数组的长度,即是哈希桶的数量。而数组的每一个元素都是一个Entry引用,从Entry对象的属性里,也可以看出其是链表的节点,每一个Entry对象内部又含有另一个Entry对象的引用。

这样就可以得出结论,HashMap/HashTable内部用Entry数组实现哈希表,而对于映射到同一个哈希桶(数组的同一个位置)的键值对,使用Entry链表来存储(解决hash冲突)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码
以下代码及注释来自java.util.HashTable

/**
* The hash table data.
*/
private transient Entry<K,V>[] table;


以下代码及注释来自java.util.HashMap

/**
* The table, resized as necessary. Length MUST Always be a power of two.
*/
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;

从代码可以看到,对于哈希桶的内部表示,两个类的实现是一致的。

4.2 算法

上一小节已经说了用来表示哈希表的内部数据结构。HashMap/HashTable还需要有算法来将给定的键key,映射到确定的hash桶(数组位置)。需要有算法在哈希桶内的键值对多到一定程度时,扩充哈希表的大小(数组的大小)。本小节比较这两个类在算法层面有哪些不同。

初始容量大小和每次扩充容量大小的不同。先看代码:

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
复制代码
以下代码及注释来自java.util.HashTable

// 哈希表默认初始大小为11
public Hashtable() {
this(11, 0.75f);
}

protected void rehash() {
int oldCapacity = table.length;
Entry<K,V>[] oldMap = table;

// 每次扩容为原来的2n+1
int newCapacity = (oldCapacity << 1) + 1;
// ...
}


以下代码及注释来自java.util.HashMap

// 哈希表默认初始大小为2^4=16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

void addEntry(int hash, K key, V value, int bucketIndex) {
// 每次扩充为原来的2n
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length);
}

可以看到HashTable默认的初始大小为11,之后每次扩充为原来的2n+1。HashMap默认的初始化大小为16,之后每次扩充为原来的2倍。还有我没列出代码的一点,就是如果在创建时给定了初始化大小,那么HashTable会直接使用你给定的大小,而HashMap会将其扩充为2的幂次方大小。

也就是说HashTable会尽量使用素数、奇数。而HashMap则总是使用2的幂作为哈希表的大小。我们知道当哈希表的大小为素数时,简单的取模哈希的结果会更加均匀(具体证明,见这篇文章),所以单从这一点上看,HashTable的哈希表大小选择,似乎更高明些。但另一方面我们又知道,在取模计算时,如果模数是2的幂,那么我们可以直接使用位运算来得到结果,效率要大大高于做除法。所以从hash计算的效率上,又是HashMap更胜一筹。

所以,事实就是HashMap为了加快hash的速度,将哈希表的大小固定为了2的幂。当然这引入了哈希分布不均匀的问题,所以HashMap为解决这问题,又对hash算法做了一些改动。具体我们来看看,在获取了key对象的hashCode之后,HashTable和HashMap分别是怎样将他们hash到确定的哈希桶(Entry数组位置)中的。

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
复制代码
以下代码及注释来自java.util.HashTable

// hash 不能超过Integer.MAX_VALUE 所以要取其最小的31个bit
int hash = hash(key);
int index = (hash & 0x7FFFFFFF) % tab.length;

// 直接计算key.hashCode()
private int hash(Object k) {
// hashSeed will be zero if alternative hashing is disabled.
return hashSeed ^ k.hashCode();
}


以下代码及注释来自java.util.HashMap
int hash = hash(key);
int i = indexFor(hash, table.length);

// 在计算了key.hashCode()之后,做了一些位运算来减少哈希冲突
final int hash(Object k) {
int h = hashSeed;
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}

h ^= k.hashCode();

// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}

// 取模不再需要做除法
static int indexFor(int h, int length) {
// assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
return h & (length-1);
}

正如我们所言,HashMap由于使用了2的幂次方,所以在取模运算时不需要做除法,只需要位的与运算就可以了。但是由于引入的hash冲突加剧问题,HashMap在调用了对象的hashCode方法之后,又做了一些位运算在打散数据。关于这些位计算为什么可以打散数据的问题,本文不再展开了。感兴趣的可以看这里。

如果你有细心读代码,还可以发现一点,就是HashMap和HashTable在计算hash时都用到了一个叫hashSeed的变量。这是因为映射到同一个hash桶内的Entry对象,是以链表的形式存在的,而链表的查询效率比较低,所以HashMap/HashTable的效率对哈希冲突非常敏感,所以可以额外开启一个可选hash(hashSeed),从而减少哈希冲突。因为这是两个类相同的一点,所以本文不再展开了,感兴趣的看这里。事实上,这个优化在JDK 1.8中已经去掉了,因为JDK 1.8中,映射到同一个哈希桶(数组位置)的Entry对象,使用了红黑树来存储,从而大大加速了其查找效率。

5. 线程安全

我们说HashTable是同步的,HashMap不是,也就是说HashTable在多线程使用的情况下,不需要做额外的同步,而HashMap则不行。那么HashTable是怎么做到的呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
复制代码
以下代码及注释来自java.util.HashTable

public synchronized V get(Object key) {
Entry tab[] = table;
int hash = hash(key);
int index = (hash & 0x7FFFFFFF) % tab.length;
for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {
return e.value;
}
}
return null;
}

public Set<K> keySet() {
if (keySet == null)
keySet = Collections.synchronizedSet(new KeySet(), this);
return keySet;
}

可以看到,也比较简单,就是公开的方法比如get都使用了synchronized描述符。而遍历视图比如keySet都使用了Collections.synchronizedXXX进行了同步包装。

6. 代码风格

从我的品位来看,HashMap的代码要比HashTable整洁很多。下面这段HashTable的代码,我就觉着有点混乱,不太能接受这种代码复用的方式。

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
复制代码
以下代码及注释来自java.util.HashTable

/**
* A hashtable enumerator class. This class implements both the
* Enumeration and Iterator interfaces, but individual instances
* can be created with the Iterator methods disabled. This is necessary
* to avoid unintentionally increasing the capabilities granted a user
* by passing an Enumeration.
*/
private class Enumerator<T> implements Enumeration<T>, Iterator<T> {
Entry[] table = Hashtable.this.table;
int index = table.length;
Entry<K,V> entry = null;
Entry<K,V> lastReturned = null;
int type;

/**
* Indicates whether this Enumerator is serving as an Iterator
* or an Enumeration. (true -> Iterator).
*/
boolean iterator;

/**
* The modCount value that the iterator believes that the backing
* Hashtable should have. If this expectation is violated, the iterator
* has detected concurrent modification.
*/
protected int expectedModCount = modCount;

Enumerator(int type, boolean iterator) {
this.type = type;
this.iterator = iterator;
}

//...

}

7. HashTable已经被淘汰了,不要在代码中再使用它。

以下描述来自于HashTable的类注释:

If a thread-safe implementation is not needed, it is recommended to use HashMap in place of Hashtable. If a thread-safe highly-concurrent implementation is desired, then it is recommended to use java.util.concurrent.ConcurrentHashMap in place of Hashtable.

简单来说就是,如果你不需要线程安全,那么使用HashMap,如果需要线程安全,那么使用ConcurrentHashMap。HashTable已经被淘汰了,不要在新的代码中再使用它。

8. 持续优化

虽然HashMap和HashTable的公开接口应该不会改变,或者说改变不频繁。但每一版本的JDK,都会对HashMap和HashTable的内部实现做优化,比如上文曾提到的JDK 1.8的红黑树优化。所以,尽可能的使用新版本的JDK吧,除了那些炫酷的新功能,普通的API也会有性能上有提升。

为什么HashTable已经淘汰了,还要优化它?因为有老的代码还在使用它,所以优化了它之后,这些老的代码也能获得性能提升。

Reference

  • github.com/ZhaoX/jdk-1…
  • github.com/ZhaoX/jdk-1…

欢迎关注公众号:Java学习之道

个人博客网站:www.mmzsblog.cn

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Android - 一种新奇的冷启动速度优化思路(Fragm

发表于 2019-08-26

一、背景

明天是周二,正好是我们团队每周一次的技术分享,我会把前段时间花了几天在干其他活的同时,整的一套诡异的冷启动速度优化方案分享一下。

二、特意声明

我这边文章的内容不会涉及网上变地都是的常规的优化方案~ ,同时,平时工作的时候,工作内容杂且多,所以这个优化方案也不是特别成熟,仅供参考吧~

三、最常见的优化方案

  • 数据懒加载,比如Fragment用户不可见时不进行数据的获取
  • 优化布局层级,减少首次inflate layout的耗时
  • 将绝大部分sdk的初始化放线程池中运行
  • 能用ViewStub的就用ViewStub,按需加载layout
  • 一定要尽量避免启动过程中,出现的主线程去unpack一些全局配置的数据
  • 不仅仅是三方库可以放子线程进行,一些时效性要求没那么高的逻辑都可以放子线程

四、项目结构

在我们的Android项目中,应用过了闪屏之后会进入到主屏 - MainActivity,这个地方我吐槽很多次了,广告闪屏作为launcher真的不是特别靠谱,最好的方式应该是从MainActivity里面来启动AdActivity,甚至是不用Activity,采用一个全屏的AdView都可以。

先简单介绍一下我们项目中MainActivity涉及到的结构:

简单的画了个图,简直是。。画图界的耻辱。。。

大概看看意思就可以了,我在组内分享就是用的这个草图,急着下班,就不重新画了。。

当App冷启动的时候,肉眼可见的要初始化的东西太多了,本身Fragment就是一个相对重的东西。比Activity要轻量很多,但是比View又要重

我们首页大概是 4-5个tab,每个tab都是一个Fragment,且第一个tab内嵌了4个Fragment,我这一次的优化主要将目标瞄准了首页的 tab1 以及tab1内嵌的四个tab

五、极致的懒加载

5.1 极致的懒加载

平时见到的懒加载:

就是初始化fragment的时候,会连同我们写的网络请求一起执行,这样非常消耗性能,最理想的方式是,只有用户点开或滑动到当前fragment时,才进行请求网络的操作。因此,我们就产生了懒加载这样一个说法。

但是。。。。

由于我们首屏4个子Tab都是继承自一个基类BaseLoadListFragment,数据加载的逻辑非常的死,按照上述的改法,影响面太大。后续可能会徒增烦恼

5.2 懒加载方案

  1. 首屏加载时,只往ViewPager中塞入默认要展示的tab,剩余的tab用空的占位Fragment代替
  2. 当用户滑动到其他tab时,比如滑动到好友动态tab,就用FriendFragment把当前的EmptyPlaceholderFragment替换掉,然后adapter.notifyDataSetChanged
  3. 当四个Tab全部替换为数据tab时,清除掉EmptyFragment的引用,释放内存

说到这里,又不得不提一个老生常谈的一个坑,因为我们的首页是用的ViewPager + FragmentPagerAdapter来进行实现的。因此就出现了一个坑:

ViewPager + FragmentPagerAdapter组合使用,调用notifyDataSetChanged()方法无效,无法刷新Fragment列表

下面我会对这个问题进行一下详细的介绍

5.3 FragmentPagerAdapter与FragmentStatePagerAdapter

当我们要使用ViewPager来加载Fragment时,官方为我们提供了这两种Adapter,都是继承自PagerAdapter。

区别,上官方描述:

FragmentPagerAdapter

This version of the pager is best for use when there are a handful of typically more static fragments to be paged through, such as a set of tabs. The fragment of each page the user visits will be kept in memory, though its view hierarchy may be destroyed when not visible. This can result in using a significant amount of memory since fragment instances can hold on to an arbitrary amount of state. For larger sets of pages, consider [FragmentStatePagerAdapter](https://developer.android.com/reference/android/support/v4/app/FragmentStatePagerAdapter.html).

FragmentStatePagerAdapter

This version of the pager is more useful when there are a large number of pages, working more like a list view. When pages are not visible to the user, their entire fragment may be destroyed, only keeping the saved state of that fragment. This allows the pager to hold on to much less memory associated with each visited page as compared to[FragmentPagerAdapter](https://developer.android.com/reference/android/support/v4/app/FragmentPagerAdapter.html) at the cost of potentially more overhead when switching between pages

总结:

  • 使用FragmentStatePagerAdapter时,如果tab对于用户不可见了,Fragment就会被销毁,FragmentPagerAdapter则不会,使用FragmentPagerAdapter时,所有的tab上的Fragment都会hold在内存里
  • 当tab非常多时,推荐使用FragmentStatePagerAdapter
  • 当tab不多,且固定时,推荐用FragmentPagerAdapter

我们项目中就是使用的ViewPager+FragmentPagerAdapter。

5.4 FragmentPagerAdapter的刷新问题

正常情况,我们使用adapter时,想要刷新数据只需要:

  1. 更新dataSet
  2. 调用notifyDataSetChanged()

但是,这个在这个Adapter中是不适用的。因为(这一步没耐心的可以直接看后面的总结):

  1. 默认的PagerAdapter的destoryItem只会把Fragment detach掉,而不会remove
  2. 当再次调用instantiateItem的时候,之前detach掉的Fragment,又会从mFragmentManager中取出,又可以attach了

3,ViewPager的dataSetChanged代码如下:

4,且adapter的默认实现

简单总结一下:

1,ViewPager的dataSetChanged()中会去用adapter.getItemPosition来判断是否要移除当前Item(position = POSITION_NONE时remove)

2,PagerAdapter的getItemPosition默认实现为POSITION_UNCHANGED

上述两点导致ViewPager构建完成Adapter之后,不会有机会调用到Adapter的instantiateItem了。

再者,即使重写了getItemPosition方法,每次返回POSITION_NONE,还是不会替换掉Fragment,这是因为instantiateItem方法中,会根据getItemId()去从FragmetnManager中找到已经创建好的Fragment返回回去,而getItemId()的默认实现是return position。

5.5 FragmentPagerAdapter刷新的正确姿势

重写getItemId()和getItemPosition()

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
46
47
48
49
50
51
typescript复制代码 class TabsAdapter extends FragmentPagerAdapter {

private ArrayList<Fragment> mFragmentList;
private ArrayList<String> mPageTitleList;
private int mCount;

TabsAdapter(FragmentManager fm, ArrayList<Fragment> fragmentList, ArrayList<String> pageTitleList) {
super(fm);
mFragmentList = fragmentList;
mCount = fragmentList.size();
mPageTitleList = pageTitleList;
}

@Override
public Fragment getItem(int position) {
return mFragmentList.get(position);
}

@Override
public CharSequence getPageTitle(int position) {
return mPageTitleList.get(position);
}

@Override
public int getCount() {
return mCount;
}

@Override
public long getItemId(int position) {
//这个地方的重写非常关键,super中是返回position,
//如果不重写,还是会继续找到FragmentManager中缓存的Fragment
return mFragmentList.get(position).hashCode();
}

@Override
public int getItemPosition(@NonNull Object object) {
//不在数据集合里面的话,return POSITION_NONE,进行item的重建
int index = mFragmentList.indexOf(object);
if (index == -1) {
return POSITION_NONE;
} else {
return mFragmentList.indexOf(object);
}
}

void refreshFragments(ArrayList<Fragment> fragmentList) {
mFragmentList = fragmentList;
notifyDataSetChanged();
}
}

其他的相关代码:

(1)实现ViewPager.OnPageChangeListener,来监控ViewPager的滑动状态,才可以在滑动到下一个tab的时候进行Fragment替换的操作,其中mDefaultTab是我们通过接口返回的当前启动展示的tab序号

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
arduino复制代码    @Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
}

@Override
public void onPageSelected(int position) {
mCurrentSelectedTab = position;
}

@Override
public void onPageScrollStateChanged(int state) {
if (!hasReplacedAllEmptyFragments && mCurrentSelectedTab != mDefaultTab && state == 0) {
//当满足: 1. 没有全部替换完 2. 当前tab不是初始化的默认tab(默认tab不会用空的Fragment去替换) 3. 滑动结束了,即state = 0
replaceEmptyFragmentsIfNeed(mCurrentSelectedTab);
}
}

备注:

onPageScrollStateChanged接滑动的状态值。一共有三个取值:

0:什么都没做

1:开始滑动

2:滑动结束

一次引起页面切换的滑动,state的顺序分别是: 1 -> 2 -> 0

(2)进行Fragment的替换,这里因为我们的tab数量是可能根据全局config信息而改变的,所以这个地方写的稍微纠结了一些。

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
ini复制代码    /**
* 如果全部替换完了,直接return
* 替换过程:
* 1. 找到当前空的tab在mEmptyFragmentList 中的实际下标
*
* @param tabId 要替换的tab的tabId - (当前空的Fragment在adapter数据列表mFragmentList的下标)
*/
private void replaceEmptyFragmentsIfNeed(int tabId) {
if (hasReplacedAllEmptyFragments) {
return;
}
int tabRealIndex = mEmptyFragmentList.indexOf(mFragmentList.get(tabId)); //找到当前的空Fragment在 mEmptyFragmentList 是第几个
if (tabRealIndex > -1) {
if (Collections.replaceAll(mFragmentList, mEmptyFragmentList.get(tabRealIndex), mDataFragmentList.get(tabRealIndex))) {
mTabsAdapter.refreshFragments(mFragmentList); //将mFragmentList中的相应empty fragment替换完成之后刷新数据
boolean hasAllReplaced = true;
for (Fragment fragment : mFragmentList) {
if (fragment instanceof EmptyPlaceHolderFragment) {
hasAllReplaced = false;
break;
}
}
if (hasAllReplaced) {
mEmptyFragmentList.clear(); //全部替换完成的话,释放引用
}
hasReplacedAllEmptyFragments = hasAllReplaced;
}
}
}

六、神奇的的预加载(预加载View,而不是data)

Android在启动过程中可能涉及到的一些View的预加载方案:

  1. WebView提前创建好,因为webview创建的耗时较长,如果首屏有h5的页面,可以提前创建好。
  2. Application的onCreate时,就可以开始在子线程中进行后面要用到的Layout的inflate工作了,最先想到的应该是官方提供的AsyncLayoutInflater
  3. 填充View的数据的预加载,今天的内容不涉及这一项

6.1 需要预加载什么

直接看图,这个是首页四个子Tab Fragment的基类的layout,因为某些东西设计的不合理,导致层级是非常的深,直接导致了首页上的三个tab加上FeedMainFragment自身,光将这个View inflate出来的时间就非常长。因此我们考虑在子线程中提前inflate layout

6.2 修改AsyncLayoutInflater

官方提供了一个类,可以来进行异步的inflate,但是有两个缺点:

  1. 每次都要现场new一个出来
  2. 异步加载的view只能通过callback回调才能获得(死穴)

因此决定自己封装一个AsyncInflateManager,内部使用线程池,且对于inflate完成的View有一套缓存机制。而其中最核心的LayoutInflater则直接copy出来就好。

先看AsyncInflateManager的实现,这里我直接将代码copy进来,而不是截图了,这样你们如果想用其中部分东西,可以直接copy:

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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
typescript复制代码/**
* @author zoutao
* <p>
* 用来提供子线程inflate view的功能,避免某个view层级太深太复杂,主线程inflate会耗时很长,
* 实就是对 AsyncLayoutInflater进行了抽取和封装
*/
public class AsyncInflateManager {
private static AsyncInflateManager sInstance;
private ConcurrentHashMap<String, AsyncInflateItem> mInflateMap; //保存inflateKey以及InflateItem,里面包含所有要进行inflate的任务
private ConcurrentHashMap<String, CountDownLatch> mInflateLatchMap;
private ExecutorService mThreadPool; //用来进行inflate工作的线程池

private AsyncInflateManager() {
mThreadPool = new ThreadPoolExecutor(4, 4, 0, TimeUnit.MILLISECONDS, new LinkedBlockingDeque<Runnable>());
mInflateMap = new ConcurrentHashMap<>();
mInflateLatchMap = new ConcurrentHashMap<>();
}

public static AsyncInflateManager getInstance() {
单例
}

/**
* 用来获得异步inflate出来的view
*
* @param context
* @param layoutResId 需要拿的layoutId
* @param parent container
* @param inflateKey 每一个View会对应一个inflateKey,因为可能许多地方用的同一个 layout,但是需要inflate多个,用InflateKey进行区分
* @param inflater 外部传进来的inflater,外面如果有inflater,传进来,用来进行可能的SyncInflate,
* @return 最后inflate出来的view
*/
@UiThread
@NonNull
public View getInflatedView(Context context, int layoutResId, @Nullable ViewGroup parent, String inflateKey, @NonNull LayoutInflater inflater) {
if (!TextUtils.isEmpty(inflateKey) && mInflateMap.containsKey(inflateKey)) {
AsyncInflateItem item = mInflateMap.get(inflateKey);
CountDownLatch latch = mInflateLatchMap.get(inflateKey);
if (item != null) {
View resultView = item.inflatedView;
if (resultView != null) {
//拿到了view直接返回
removeInflateKey(inflateKey);
replaceContextForView(resultView, context);
return resultView;
}

if (item.isInflating() && latch != null) {
//没拿到view,但是在inflate中,等待返回
try {
latch.wait();
} catch (InterruptedException e) {
Log.e(TAG, e.getMessage(), e);
}
removeInflateKey(inflateKey);
if (resultView != null) {
replaceContextForView(resultView, context);
return resultView;
}

}
//如果还没开始inflate,则设置为false,UI线程进行inflate
item.setCancelled(true);
}
}
//拿异步inflate的View失败,UI线程inflate
return inflater.inflate(layoutResId, parent, false);
}

/**
* inflater初始化时是传进来的application,inflate出来的view的context没法用来startActivity,
* 因此用MutableContextWrapper进行包装,后续进行替换
*/
private void replaceContextForView(View inflatedView, Context context) {
if (inflatedView == null || context == null) {
return;
}
Context cxt = inflatedView.getContext();
if (cxt instanceof MutableContextWrapper) {
((MutableContextWrapper) cxt).setBaseContext(context);
}
}

@UiThread
private void asyncInflate(Context context, AsyncInflateItem item) {
if (item == null || item.layoutResId == 0 || mInflateMap.containsKey(item.inflateKey) || item.isCancelled() || item.isInflating()) {
return;
}
onAsyncInflateReady(item);
inflateWithThreadPool(context, item);
}

private void onAsyncInflateReady(AsyncInflateItem item) {
...
}

private void onAsyncInflateStart(AsyncInflateItem item) {
...
}

private void onAsyncInflateEnd(AsyncInflateItem item, boolean success) {
item.setInflating(false);
CountDownLatch latch = mInflateLatchMap.get(item.inflateKey);
if (latch != null) {
//释放锁
latch.countDown();
}
...
}

private void removeInflateKey(String inflateKey) {
...
}

private void inflateWithThreadPool(Context context, AsyncInflateItem item) {
mThreadPool.execute(new Runnable() {
@Override
public void run() {
if (!item.isInflating() && !item.isCancelled()) {
try {
onAsyncInflateStart(item);
item.inflatedView = new BasicInflater(context).inflate(item.layoutResId, item.parent, false);
onAsyncInflateEnd(item, true);
} catch (RuntimeException e) {
Log.e(TAG, "Failed to inflate resource in the background! Retrying on the UI thread", e);
onAsyncInflateEnd(item, false);
}
}
}
});
}

/**
* copy from AsyncLayoutInflater - actual inflater
*/
private static class BasicInflater extends LayoutInflater {
private static final String[] sClassPrefixList = new String[]{"android.widget.", "android.webkit.", "android.app."};

BasicInflater(Context context) {
super(context);
}

public LayoutInflater cloneInContext(Context newContext) {
return new BasicInflater(newContext);
}

protected View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException {
for (String prefix : sClassPrefixList) {
try {
View view = this.createView(name, prefix, attrs);
if (view != null) {
return view;
}
} catch (ClassNotFoundException ignored) {
}
}
return super.onCreateView(name, attrs);
}
}
}

这里我用一个AsyncInflateItem来管理一次要inflate的一个单位,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
arduino复制代码/**
* @author zoutao
*/
public class AsyncInflateItem {
String inflateKey;
int layoutResId;
ViewGroup parent;
OnInflateFinishedCallback callback;
View inflatedView;

private boolean cancelled;
private boolean inflating;

//还有一些set get方法
}

以及最后inflate的回调callback:

1
2
3
csharp复制代码public interface OnInflateFinishedCallback {
void onInflateFinished(AsyncInflateItem result);
}

经过这样的封装,外面可以直接在Application的onCreate中,开始异步的inflate view的任务。调用如下:

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
java复制代码AsyncInflateUtil.startTask();

/**
* @author zoutao
*/
public class AsyncInflateUtil {
public static void startTask() {
Context context = new MutableContextWrapper(CommonContext.getApplication());
AsyncInflateManager.getInstance().asyncInflateViews(context,
new AsyncInflateItem(InflateKey.TAB_1_CONTAINER_FRAGMENT, R.layout.fragment_main),
new AsyncInflateItem(InflateKey.SUB_TAB_1_FRAGMENT, R.layout.fragment_load_list),
new AsyncInflateItem(InflateKey.SUB_TAB_2_FRAGMENT, R.layout.fragment_load_list),
new AsyncInflateItem(InflateKey.SUB_TAB_3_FRAGMENT, R.layout.fragment_load_list),
new AsyncInflateItem(InflateKey.SUB_TAB_4_FRAGMENT, R.layout.fragment_load_list));

}

public class InflateKey {
public static final String TAB_1_CONTAINER_FRAGMENT = "tab1";
public static final String SUB_TAB_1_FRAGMENT = "sub1";
public static final String SUB_TAB_2_FRAGMENT = "sub2";
public static final String SUB_TAB_3_FRAGMENT = "sub3";
public static final String SUB_TAB_4_FRAGMENT = "sub4";
}
}

注意:这里会有一个坑。就是在Application的onCreate中,能拿到的Context只有Application,这样inflate的View,View持有的Context就是Application,这会导致一个问题。

如果用View.getContext()这个context去进行Activity的跳转就会。。抛异常

Calling startActivity() from outside of an Activity context requires the FLAG_ACTIVITY_NEW_TASK flag. Is this really what you want?

而如果想要传入Activity来创建LayoutInflater,时机又太晚。众所周知,Context是一个抽象类,实现它的包装类就是ContextWrapper,而Activity、Appcation等都是ContextWrapper的子类,然而,ContextWrapper还有一个神奇的子类,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
scala复制代码package android.content;

/**
* Special version of {@link ContextWrapper} that allows the base context to
* be modified after it is initially set.
*/
public class MutableContextWrapper extends ContextWrapper {
public MutableContextWrapper(Context base) {
super(base);
}

/**
* Change the base context for this ContextWrapper. All calls will then be
* delegated to the base context. Unlike ContextWrapper, the base context
* can be changed even after one is already set.
*
* @param base The new base context for this wrapper.
*/
public void setBaseContext(Context base) {
mBase = base;
}
}

6.3 装饰器模式

可以看到Android上Context的设计采用了装饰器模式,装饰器模式极大程度的提高了灵活性。这个例子对我最大的感受就是,当官方没有提供MutableContextWrapper这个类时,其实我们自己也完全可以通过同样的方式去进行实现。思维一定要灵活~

七、总结

常见的启动速度优化的方案有:

  • 数据懒加载,比如Fragment用户不可见时不进行数据的获取
  • 优化布局层级,减少首次inflate layout的耗时
  • 将绝大部分sdk的初始化放线程池中运行
  • 能用ViewStub的就用ViewStub,按需加载layout
  • 一定要尽量避免启动过程中,出现的主线程去unpack一些全局配置的数据
  • 不仅仅是三方库可以放子线程进行,一些时效性要求没那么高的逻辑都可以放子线程

这些都可以在网上找到大量的文章以及各个大佬的实现方案。

首先,优化的大方向肯定先定好:

  1. 懒加载
  2. 预加载

懒加载:

  • 首屏加载时,只往ViewPager中塞入默认要展示的tab,剩余的tab用空的占位Fragment代替
  • 当用户滑动到其他tab时,比如滑动到好友动态tab,就用FriendFragment把当前的EmptyPlaceholderFragment替换掉,然后adapter.notifyDataSetChanged
  • 当四个Tab全部替换为数据tab时,清除掉EmptyFragment的引用,释放内存

预加载:

  • Application onCreate方法中,针对后续所有的Fragment,在子线程中将Layout先给inflate出来
  • 针对inflate完成的View加入一套缓存的存取机制,以及等待机制
  • 如果正在inflate,则进行阻塞等待
  • 如果已经inflate完成了,取出view,并释放缓存对于View的引用
  • 如果还没有开始Inflate,则在UI线程直接进行inflate

这些方案不一定非常好使,所以仅供参考~

从ContextWrapper、MutableContextWrapper类的设计中学到了 ↓

写代码的时候,首先要进行设计,选用最合适的设计模式,这样后续赚到的远远大于写一个文档、想一个设计所耗费的时间和脑力成本

我的简书 邹啊涛涛涛的简书

我的CSDN 邹啊涛涛涛的CSDN

我的掘金 邹啊涛涛涛的掘金

你可能感兴趣

Android QUIC 实践 - 基于 OKHttp 扩展出 Cronet 拦截器 - 掘金 (juejin.cn)

Android启动优化实践 - 秒开率从17%提升至75% - 掘金 (juejin.cn)

如何科学的进行Android包体积优化 - 掘金 (juejin.cn)

Android稳定性:Looper兜底框架实现线上容灾(二) - 掘金 (juejin.cn)

基于 Booster ASM API的配置化 hook 方案封装 - 掘金 (juejin.cn)

记 AndroidStudio Tracer工具导致的编译失败 - 掘金 (juejin.cn)

Android 启动优化案例-WebView非预期初始化排查 - 掘金 (juejin.cn)

chromium-net - 跟随 Cronet 的脚步探索大致流程(1) - 掘金 (juejin.cn)

Android稳定性:可远程配置化的Looper兜底框架 - 掘金 (juejin.cn)

一类有趣的无限缓存OOM现象 - 掘金 (juejin.cn)

Android - 一种新奇的冷启动速度优化思路(Fragment极度懒加载 + Layout子线程预加载) - 掘金 (juejin.cn)

Android - 彻底消灭OOM的实战经验分享(千分之1.5 -> 万分之0.2) - 掘金 (juejin.cn)

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

使用kubernetes一年之后的思考

发表于 2019-08-25

本文首发于我的blog

从去年10月份第一次接触kubernetes,到年初系统学习,然后到上个月接手来运维kubernetes集群,也算是对kubernetes有一些了解了。在学习一个技术的时候,对技术的使用场景和发展趋势应该有自己的看法,这样才知道如何结合团队情况和公司的发展合理的采用一个技术。所以这里我宏观上谈一下我对kubernetes技术的一些思考。

kubernetes背景和诞生目的

kubernetes诞生的背景是因为Docker和Paas,对于稍微复杂的业务是不能直接用Docker的,因为Docker提供的能力实在有限,复杂的业务上云一般需要一些平台层面的能力,也就是Paas。kubernetes就是这个背景诞生的,击败了Mesos等竞争对手,成为容器编排领域事实上的标准。

所以按照技术的诞生背景来说,kubernetes的目的就是要做Paas,所以要玩好kubernetes必须对Paas有个大局观,下图是左耳朵耗子梳理的Paas结构图,我觉得挺全面了:

Paas

基于kubernetes构建Paas的原因在于:

  1. kubernetes本身提供了一些核心的能力比如服务发现,statefulset,负载均衡,服务状态检查并且自动重启等等;
  2. kubernetes提供了一些插件化的机制(CRD,动态准入控制等等)使得DIY,或者增强kubernetes的能力变得简单;
  3. kubernetes社区非常活跃,诞生了非常多的高质量项目,比如Prometheus,Rook等等;

因为这几个原因,基于kubernetes构建Paas变得更加简单。

入手难度

kubernetes入手确实有一些难度,尤其是运维kubernetes集群,不仅需要懂kubernetes的知识,还需要懂公有云的使用。基于此,很多公有云厂商推出了kubernetes托管服务,不仅降低了kubernetes运维成本,也更好的和已有的服务结合起来(比如阿里云的kubernetes托管服务,不再需要自己搭建ELK了,可以使用阿里云的日志系统)。

但是使用托管服务的时候不要觉得用了托管服务了自己的学习成本就降低了,对于kubernetes运维人员还是需要了解kubernetes的各个组件,了解云供应商各种产品使用。只有这样,才知道如何规划集群的网络,容量,存储等等,才知道业务如何改造才能上kubernetes。

业务如何上kubernetes

这里提三点。

  1. 服务需要设置合理的存活探针和就绪探针,这样kubernetes才知道什么时候杀掉Pod启动一个新的,什么时候把流量交给Pod。
  2. 设置合理的资源request和limit,一般request设置一个应用平均的资源利用值,limit设置一个稍高一些的值。
  3. 在线业务和离线业务混部需要额外注意,防止离线业务占用太多node资源导致node资源紧张从而杀掉在线业务。如果做不到这一点就不要做混部,利用nodeselector和toleraions来把在线和离线业务的pod分别部署到对应的node上去。

收益

这大半年用kubernetes的收益是:

  • 资源高度弹性。离线业务来的时候申请spot实例,计算完毕之后就会自动回收spot实例,所以离线计算消耗的计算资源价格非常低;
  • 更加敏捷。不用找运维申请物理机了,直接部署pod就好了,如果集群的资源不够,会自动扩容,完全不用担心计算资源不够的问题;
  • 能力复用,尤其是监控的能力,把metric全部导入Prometheus,借助Prometheus+Grafana,整个监控体系非常简单;
  • 提高资源利用率,尤其是可以做到业务混部;

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…859860861…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%