0%

Guava库处理驼峰

Guava提供了一个类用于字符串规范的转换

CaseFomat:

枚举常量 说明
LOWER_HYPHEN 连字符的变量命名规范如lower-hyphen
LOWER_UNDERSCORE c++变量命名规范如lower_underscore
LOWER_CAMEL java变量命名规范如lowerCamel
UPPER_CAMEL java和c++类的命名规范如UpperCamel
UPPER_UNDERSCORE java和c++常量的命名规范如UPPER_UNDERSCORE

maven依赖:

1
2
3
4
5
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>21.0</version>
</dependency>

测试:

1
2
3
4
5
6
7
8
9
10
11
@Test
public void test() {
System.out.println(CaseFormat.LOWER_HYPHEN.to(CaseFormat.LOWER_CAMEL, "test-data"));//testData
System.out.println(CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, "test_data"));//testData
System.out.println(CaseFormat.UPPER_UNDERSCORE.to(CaseFormat.UPPER_CAMEL, "test_data"));//TestData

System.out.println(CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, "testdata"));//testdata
System.out.println(CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, "TestData"));//test_data
System.out.println(CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_HYPHEN, "testData"));//test-data

}

Lambda 可序列化

在想要动态获取lambda运行时候的属性就必须使用可序列化的labmda表达式,就要借助序列化操作将运行时的运行方法给获取出来。

就比如说,有下面这一段代码:

1
2
3
4
5
6
7

@Test
public void resolve() {
LambdaObj lambdaObj = new LambdaObj();
LambdaObj2 lambdaObj2 = new LambdaObj2();
FiledUtils.copyProperties(lambdaObj, lambdaObj2, LambdaObj::getName, LambdaObj::getSex);
}

我在代码运行的时候传递了LambdaObj的getName和getSex两个lambda表达式进行,但是我并不需要执行getName的方法而是需要获取name的这个字段,就像:

1
FiledUtils.copyProperties(lambdaObj, lambdaObj2, "name","sex")

只是借助了lambda达到不用手动写死字符串的操作。

通过序列化的方式能够实现这个操作,JDK中提供了SerializedLambda类来表达lambdda的序列化形式,这个类存储了lambda表达式存储的信息,比如接口方法的标识、实现方法的元素等。

在编译器编译时,如果lambda接口实现了序列化接口,则会自动生成一个writeReplace方法返回一个SerializedLambda实例。

1
2
3
4
5
6
7
8
9
10
11
12
private static SerializedLambda resolve(SFunction lambda) {
SerializedLambda sl = null;
try {
Method writeReplace = lambda.getClass()
.getDeclaredMethod("writeReplace");
writeReplace.setAccessible(true);
sl = (SerializedLambda) writeReplace.invoke(lambda);
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
e.printStackTrace();
}
return sl;
}

在运行时debug发现能看见implMethodName就是我们在使用lambda表达式的时候传入的方法。

lambda接口必须实现序列化标示,我发现JDK自带的接口并没有支持可序列化的。因此需要自己去继承并实现一下。我选择直接继承Function的

1
2
3
4
5
6
7
8
9
10
public interface SFunction<T, R> extends Function<T, R>, Serializable {


}

public static <T, O> Optional<O> copyProperties(T source,
O target,
SFunction<T, ?>... cl) {
return copyProperties(source, target, null, null, false, columnsToString(cl));
}

Java响应式编程

什么是响应式编程

响应式是通过异步的数据流来进行编程,在某种程度下这并不是什么很新的概念。就比如说事件中心或者一个特殊事件就是一个典型的异步事件流,你可以订阅这个事件并且做一些额外的事情。响应式就是一种编程的概念,你能够创建任意的数据,不仅仅局限于类似于点击事件这种数据上。数据流无处不再,任何东西都可以是数据流,例如用户输入、配置、缓存、数据结构。就比如说,Twitter提醒就像和点击事件一样的数据流。你能够监听这个数据流,并作出改变。

响应式编程的发起是由微软创建了在.Net系统下创建了一个响应式的扩展库。随后,RxJava也在JVM同样实现了响应式编程。随着时间的推移,一个标准化的JVM响应式的定义规范就被沉淀了下来,规范定义了一些接口和一些响应式编程库的规则。这些接口已经被整合到Java9当中了。

目前比较流行的响应式框架有RxJava,Reactor。RxJava在移动端用的比较多一些,服务端更倾向于使用Reactor进行响应式开发。

Reactor框架

Reactor是运行在JVM上的完全非阻塞的响应式编程基础框架。它能够和Java8新的API结合起来使用,特别是CompletableFuture,Stream,Duration。它提供了可组合的异步序列API,Flux(针对多元素)、Mono(针对定的元素)。大部分实现了 Reactive Streams 规范。

Reacor同时提供非阻塞跨进程通讯框架,Reactor-Netty。适用于微服务框架,Reactor Netty提供了能够承载通讯压力的Http、TCP、UDP网络引擎,并支持数据的编码和解码操作。

不过Reactor只能够运行在Java 8或以上版本的jdk。

在传统的编程思想中,大多都是线程阻塞的,这大量浪费了系统的资源,现代的应用程序需要能够支持大量的用户同时请求,尽管现代的硬件性能不断提升,当仍然是一种瓶颈。有两种方式去提升程序的表现,并行化处理,使用更多的线程和硬件资源;在现有资源基础上寻求更有效率的编发。

通常,Java程序员使用阻塞的代码,一般情况下还是挺好的,直到出现了性能问题,这个使用就需要额外引入别的线程。但是这样容易出现竞争和并发问题。

目前JVM也有异步编程的操作类,比如Callbacks、Futures但是他们用起来代码不是那么易懂并且不是很容易维护,Reactor将会对这种JVM的弊端进行处理优化。

开始使用

maven

首先在maven统一版本里管理,然后将依赖添加到

1
2
3
4
5
6
7
8
9
10
11
<dependencyManagement> 
<dependencies>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-bom</artifactId>
<version>Bismuth-RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

1
2
3
4
5
6
7
8
9
10
11
12
<dependencies>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-core</artifactId>

</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

gradle

1
2
3
4
5
6
7
8
9
10
11
plugins {
id "io.spring.dependency-management" version "1.0.6.RELEASE"
}
dependencyManagement {
imports {
mavenBom "io.projectreactor:reactor-bom:Bismuth-RELEASE"
}
}
dependencies {
compile 'io.projectreactor:reactor-core'
}

Reactor核心功能

Rector提供了两种类型的操作符,Flux和Mono,Flux主要是针对0,N的元素,Mono主要是针对0,1个元素。

这两者主要是一种语意的差别,例如,一个Http请求只有一个请求结果,也不需要计数操作,因此使用Mono更加适合如使用Flux,因为Mono只提供了针对一项或者零个的运算符。

Flux是一个异步0-N的序列

Flux 是一个标准的Publisher,会发射出0-N的元素序列,可选择的可以由错误或者完成终止。在Reactive Stream规范中,有三种类型的元素回掉onNext,onComplete,onError。

使用Dozer处理类转换

什么是Dozer

dozer是用于将Java的一个对象递归映射成另一个Java对象的工具。通常来说,这些JavaBean都有各自特定的类型。

dozer提供了一个简单的属性的映射、复杂类型的映射、双向属性映射、内外属性映射也有递归映射。他们包含了集合下各个元素属性映射。

为什么要映射框架

映射框架被用于在分层体系中通过分装对特定数据对象的更改来创建抽象层,将分装好的对象传播到其他层级中去。
例如外部服务对象,领域对象DO,数据传输对象DTO。一个映射框架非常适合在一个对象转换到另一个对象的数据中使用。
对于分布式系统,又一个副作用就是需要在不同的系统之间传递对象,通常的,不能够把内部对象直接暴露到外部,不与许将外部对象直接引入到你当前的系统中去。

一直以来我们都是通过手动编码的方式在两个对象之间进行参数映射。大多数程序员都会开发一些常用的映射框架,并且话费无数的时间和数以千计的代码去处理这些关系映射。

一个通用的框架需要解决这些问题。Dozer是一个开源的映射框架,而且Dozer非常健壮,通用,灵活,可重复,可配置的。

数据对象是分层架构的一个重要组成部分。仔细的去确定每一层的边界,但是不要划分的太过,因为去维护这些对象会产生维护和性能的成本。

并行对象层级结构

有不同的原因解释为何需要支持并行对象层级结构,例如:

  • 与外部代码集成
  • 满足序列化要求
  • 集成框架
  • 划分体系结构层级

在某些情况下,当你无法直接控制代码的时候,保护你的代码频繁的改变对象层级结构说带来的侵害。因此,Dozer搭建起了一个桥梁用于连接应用和外部程序。
映射是采用反射的方式去执行的,并不会迫害你原有的API。例如如果当一个对象从Number变成String的时候,代码仍然会工作,这种问题会自动被修复。

一些框架强制要求可序列化的约束,他们不允许通过网络发送任何Java类对象。一个流行的谷歌框架GWT框架,他们强制要求开发者只能够发送只能够编译成能够序列化的JS对象。

在复杂的企业级开发中,划分成多个层次结构是非常有意义的。每个东西都会有其自己的抽象等级。一个典型的例子就是展示层,领域成和持久层。每一个层级都有
一组JavaBean标示这一层特定的数据。所有的数据都需要上传到上一个层级这种方式是不必要的,例如一些领域对象和最终的展示层对象会有所区别。

开始使用

如果是使用Maven, 在项目中引入下面的依赖:

1
2
3
4
5
<dependency>
<groupId>com.github.dozermapper</groupId>
<artifactId>dozer-core</artifactId>
<version>6.4.0</version>
</dependency>

简单映射

我们假设有两种数据类,并且他们的属性名称相同

1
2
Mapper mapper = DozerBeanMapperBuilder.buildDefault();
DestinationObject destObject = mapper.map(sourceObject, DestinationObject.class);

在执行演示程序之后,结果将会创建一个新的目标对象并且新的对象有了和原对象相同的属性。如果任何的映射属性有不同的数据类型,Dozer引擎将会自动处理数据的转化。
你已经完成了第一个Dozer的映射,后面的部分将去展现如何使用xml进行对象转换。

Dozer操作包含了两个默认的模型,显示的和隐示的。

注: 在实际的应用中不建议每次去创建一个新的mapper去转换对像,可以使用重复的替代。

通过XML指定特定的转换

如果两种不同的数据类型,并且他们的类型名字也是不同的,你这需要新增转换规则新增到自定义的一个xml文件中。
Dozer运行中使用可以使用这个映射文件。

从原字段数据转换到目标字段数据,Dozer会自动的运行类型转换。Dozer映射引擎是双向的,因此,如果想去映射目标对象到原对象,你无需再次新建另一个xml文件。

注: 字段如果有相同的名称,这无需在xml文件中特殊指定。Dozer会自动处理相同名称之间的映射关系

1
2
3
4
5
6
7
8
<mapping>
<class-a>yourpackage.yourSourceClassName</class-a>
<class-b>yourpackage.yourDestinationClassName</class-b>
<field>
<a>yourSourceFieldName</a>
<b>yourDestinationFieldName</b>
</field>
</mapping>

完整的Dozer映射文件如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?xml version="1.0" encoding="UTF-8"?>
<mappings xmlns="http://dozermapper.github.io/schema/bean-mapping"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://dozermapper.github.io/schema/bean-mapping http://dozermapper.github.io/schema/bean-mapping.xsd">
<configuration>
<stop-on-errors>true</stop-on-errors>
<date-format>MM/dd/yyyy HH:mm</date-format>
<wildcard>true</wildcard>
</configuration>
<mapping>
<class-a>yourpackage.yourSourceClassName</class-a>
<class-b>yourpackage.yourDestinationClassName</class-b>
<field>
<A>yourSourceFieldName</A>
<B>yourDestinationFieldName</B>
</field>
</mapping>
</mappings>

Dozer和框架集成

Dozer可以不依赖于任何依赖注入框架。然而Dozer能够支持特定的一些现成包装,如Spring。

通过XML映射

这个部分将会介绍如何使用xml配置映射关系。如果有两种不同类型的数据对象,并且他们的字段不同,你将要手动添加一个类型转换文件。
从原字段数据转换到目标字段数据,Dozer会自动的运行类型转换。Dozer映射引擎是双向的,因此,如果想去映射目标对象到原对象,你无需再次新建另一个xml文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?xml version="1.0" encoding="UTF-8"?>
<mappings xmlns="http://dozermapper.github.io/schema/bean-mapping"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://dozermapper.github.io/schema/bean-mapping http://dozermapper.github.io/schema/bean-mapping.xsd">
<mapping>
<class-a>com.github.dozermapper.core.vo.TestObject</class-a>
<class-b>com.github.dozermapper.core.vo.TestObjectPrime</class-b>
<field>
<a>one</a>
<b>onePrime</b>
</field>
</mapping>
<mapping wildcard="false">
<class-a>com.github.dozermapper.core.vo.TestObjectFoo</class-a>
<class-b>com.github.dozermapper.core.vo.TestObjectFooPrime</class-b>
<field>
<a>oneFoo</a>
<b>oneFooPrime</b>
</field>
</mapping>
</mappings>

一个映射元素可能对应多个映射元素,每个类的映射声明和每个属性的映射声明都有可能有这种情况。
通用属性wildcard如果设置为true则说明这是默认的转换规则。这意味着它将自动尝试这两个对象的中每个属性。当这个属性被设置为false的时候,它只会转换指定的字段。

如何加载xml的文件

Dozer将会搜索整个资源目录去寻找特定的文件。通常的做法是将配置文件打包在应用程序中。另一方面,你也可以从外部文件加载文件,可以在resource中加上file:c:\somedozermapping.xml

自从5.4.0之后,也支持从输入流中加载xml文件

使用注解

使用Dozer不好的一个地方是吃啊用XML进行配置。Dozer开始于大约五年之前XML最流行的那个年代,XML在当时是一个显而易见的选择。
在Java5之后给我们带来了注解并且这是新的一种行业公认的领域配置方法。DSL提供了映射的API,在5.3.2之后Dozer也同样支持使用注解配置。

使用注解的一个显而易见的原因是在映射代码中可以避免重复的属性和方法名。注解可以放在自身的属性上从而减少代码量。然而在某种情景下应该避免甚至不去使用Dozer。

例如:

  • 你需要映射的类不在你的控制下,可能是jar包中的
  • 映射规则十分复杂并且需要很多的配置

在第一种情况下,第三方的DTO你不可能加上注解。第二种则是需要写很多注解代码,或者某些相同的实体名隔离开来。过度注解的bean可能在阅读理解上造成困难。

使用方式

注:Dozer的注解目前是一个实现性的东西,还没有复杂转换的用例。然而在这是比使用XML和API方式更加容易实施。

下面的用例非常简单。在属性的get方法上面直接加上@Mapping注解。如果Dozer发现了它的双向映射,这意味着一个注释将会创建两种转换类型。
类型转换是自动的,全局的转换会很好的解析他们,注解只工作在定义了通配符的属性上面,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class SourceBean {

private Long id;

private String name;

@Mapping("binaryData")
private String data;

@Mapping("pk")
public Long getId() {
return this.id;
}

public String getName() {
return this.name;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class TargetBean {

private String pk;

private String name;

private String binaryData;

public void setPk(String pk) {
this.pk = pk;
}

public void setName(String name) {
this.name = name;
}
}

三个字段将会被转换。name将走默认的转换,id会被转换成pk,data将会把数据变成binaryData,不要担心私有属性,它也会被自动转换。

当前Dozer只提供了一个注解,但是下一个release版本会添加一个新的进去。

使用API

基于XML的配置方法是非常稳定的,他被用于很多真实的生产项目,然而它也有诸多限制。

  • 第一点也是最重要的一点就是它无法动态的生成,所有的XML映射配置必须在Dozer启动之前就弄好,而且之后还不能修改。又一个很棘手的地方,当你生成并且把映射规则放到你当前模板引擎的文件系统中,Dozer不支持这种操作。
  • 第二个问题是你不得不在XML中填写多次相同的类名字。这导致的大量的复制粘贴编程,这个可以在xml中使用特定表达式处理,但是仍然没有解决所有的问题。
  • 当你删除或者重命名这些被引用的类,但不是所有的IDE能够支持这种重构操作。自动填充的功能也不是所有的IDE都支持。

使用API进行映射:

API映射目的是去解决所有提出的问题。为了保持向后兼容,API映射同时也可以和XML结合在一起使用。事实上,某些配置部门只能够使用XML,比如全局模块配置。

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
import com.github.dozermapper.core.classmap.RelationshipType;
import com.github.dozermapper.core.loader.api.BeanMappingBuilder;
import com.github.dozermapper.core.loader.api.FieldsMappingOptions;
import com.github.dozermapper.core.loader.api.TypeMappingOptions;

import static com.github.dozermapper.core.loader.api.FieldsMappingOptions.collectionStrategy;
import static com.github.dozermapper.core.loader.api.FieldsMappingOptions.copyByReference;
import static com.github.dozermapper.core.loader.api.FieldsMappingOptions.customConverter;
import static com.github.dozermapper.core.loader.api.FieldsMappingOptions.customConverterId;
import static com.github.dozermapper.core.loader.api.FieldsMappingOptions.hintA;
import static com.github.dozermapper.core.loader.api.FieldsMappingOptions.hintB;
import static com.github.dozermapper.core.loader.api.FieldsMappingOptions.useMapId;
import static com.github.dozermapper.core.loader.api.TypeMappingOptions.mapId;
import static com.github.dozermapper.core.loader.api.TypeMappingOptions.mapNull;

public class MyClass {

public void create() {
BeanMappingBuilder builder = new BeanMappingBuilder() {
protected void configure() {
mapping(Bean.class, Bean.class,
TypeMappingOptions.oneWay(),
mapId("A"),
mapNull(true)
)
.exclude("excluded")
.fields("src", "dest",
copyByReference(),
collectionStrategy(true, RelationshipType.NON_CUMULATIVE),
hintA(String.class),
hintB(Integer.class),
FieldsMappingOptions.oneWay(),
useMapId("A"),
customConverterId("id")
)
.fields("src", "dest",
customConverter("com.github.dozermapper.core.CustomConverter")
);
}
};
}
}
1
2
3
Mapper mapper = DozerBeanMapperBuilder.create()
.withMappingBuilder(builder)
.build();

配置

通过XML配置

XML文件是Dozer的最主要的配置方式。可以有五种不同的配置范围,全局、每个类、单个类、每个字段、特定字段。

全局配置

配置块被用于配置全局的设置。全局的设置是可选的,配合自定义的转换器使用。

Dozer支持多个文件映射的功能。每个映射文件都有他们独有的配置区域。配置文件可以从存储中的已存在的映射文件集成。隐式映射将会集成默认的转换配置。

1
2
3
4
5
6
7
8
9
10
11
12
<configuration>
<date-format>MM/dd/yyyy HH:mm</date-format>
<stop-on-errors>true</stop-on-errors>
<wildcard>true</wildcard>
<custom-converters>
<!-- these are always bi-directional -->
<converter type="com.github.dozermapper.core.converters.TestCustomConverter">
<class-a>com.github.dozermapper.core.vo.TestCustomConverterObject</class-a>
<class-b>another.type.to.Associate</class-b>
</converter>
</custom-converters>
</configuration>

所有全局的设置都有其独特的XML标记。

全局配置可以被单个映射配置所覆盖隐式。这里,配置属性可以使用name=”value”的方式设置属性。他们会影响这两个类之间的操作。

1
2
3
4
5
6
7
8
<mapping wildcard="false" date-format="MM/dd/yyyy HH:mm">
<class-a>com.github.dozermapper.core.vo.SpringBean</class-a>
<class-b>com.github.dozermapper.core.vo.SpringBeanPrime</class-b>
<field>
<a>anAttributeToMap</a>
<b>anAttributeToMapPrime</b>
</field>
</mapping>

一个独立的类等级,一些时候,你可能想去只设置两个类之间的关系。他们可以使用class-a和class-b标签

1
2
3
4
5
6
7
8
<mapping>
<class-a is-accessible="true">com.github.dozermapper.core.vo.SpringBean</class-a>
<class-b>com.github.dozermapper.core.vo.SpringBeanPrime</class-b>
<field>
<a>anAttributeToMap</a>
<b>anAttributeToMapPrime</b>
</field>
</mapping>

每个属性的映射,同样的你可以改变两个属性之间的映射行为

1
2
3
4
5
6
7
8
<mapping>
<class-a>com.github.dozermapper.core.vo.SpringBean</class-a>
<class-b>com.github.dozermapper.core.vo.SpringBeanPrime</class-b>
<field remove-orphans="false">
<a>anAttributeToMap</a>
<b>anAttributeToMapPrime</b>
</field>
</mapping>

单个的属性,一些设置可以被应用于单个属性

1
2
3
4
5
6
7
8
<mapping>
<class-a>com.github.dozermapper.core.vo.SpringBean</class-a>
<class-b>com.github.dozermapper.core.vo.SpringBeanPrime</class-b>
<field remove-orphans="false">
<a>anAttributeToMap</a>
<b>anAttributeToMapPrime</b>
</field>
</mapping>

默认情况下,如果Dozer在执行字段映射的时候出现错误,异常将会被抛出并且终止映射。尽管这种行为是推荐行为,但是Dozer仍然可以通过设置stop-on-errors
去忽略这种异常并且继续映射下一个属性。

你还可以指定导致Dozer停止运行的特殊的异常并且抛出,即使stop-on-errors设置成false也是如此。

还能够通过trim-strings属性在执行setter之前将字符串自动修整,就像执行String.trim()

如果是使用MapperBuilder去定义映射规则,则能够通过编码的方式定义类、和属性的映射规则。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Mapper mapper = DozerBeanMapperBuilder.create()
.withMappingBuilder(new BeanMappingBuilder() {
@Override
protected void configure() {
mapping(type(A.class).mapEmptyString(true),
type(B.class),
TypeMappingOptions.wildcardCaseInsensitive(true)
).fields(
field("fieldOfA").getMethod("getTheField"),
field("fieldOfB"),
FieldsMappingOptions.oneWay()
);
}
})
.build();

在分布式系统中,常常需要使用到缓存,通常缓存一般都是使用集群的方式进行部署,访问缓存的时候一般是对对象的HASH值对集群中机器个个数取余。

比如有三台redis节点,hash值对3取余之后只能是0,1,2三个数通过取余求出来的结果决定到哪台机器上取缓存。

普通的Hash算法

普通的hash算法无法解决redis节点增加和删除的问题。假如增加了一个节点,那么原本对象的hash值则需要对4进行取余,取余结果可能有0,1,2,3。

这个时候,原有的对象的取余则会多个4,但是机器4上却缺少这个对象的缓存。

代码实现:

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
/**
* 单位对象
*/
public class Obj {
public Obj(String name) {
this.name = name;
}

private String name;

@Override
public String toString() {
return "Obj{" +
"name='" + name + '\'' +
'}';
}

@Override
public int hashCode() {
return Math.abs(super.hashCode());
}
}

/**
* 服务器节点,用于存储Obj对象
*/
public class Node {
Map<Integer, Obj> node = new HashMap<>();
public Node(String address) {
this.address = address;
}
/**
* 服务器地址
*/
private String address;
public String getAddress() {
return address;
}
public Obj getObj(Obj obj) {
return node.get(obj.hashCode());
}
public void putObj(Obj obj) {
node.put(obj.hashCode(), obj);
}
/**
* 预防出现负值
*
*/
@Override
public int hashCode() {
return Math.abs(super.hashCode());
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Node node = (Node) o;
return address.equals(node.address);
}
}


/**
* hash 列表
* 用于存储服务器节点
*/
public class NodeArray {

List<Node> nodes = new ArrayList();
public void addNode(Node node) {
size++;
nodes.add(node);
}
public void removeNode(Node node) {
nodes.remove(node);
}
Obj get(Obj obj) {
int index = obj.hashCode() % nodes.size();
Node node = nodes.get(index);
System.out.print(node.getAddress() + " ");
return node.getObj(obj);
}
void put(Obj obj) {
int index = obj.hashCode() % nodes.size();
nodes.get(index).putObj(obj);
}

}

在正常不增加节点的情况下,执行结果是正常的,都能够寻找到对应的机器

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
    public static void main(String[] args) {
NodeArray nodeArray = new NodeArray();
// 所有节点
Node[] nodes = { new Node("192.168.0.1"),new Node("192.168.0.2"),new Node("192.168.0.3")};
for (Node node : nodes) {
nodeArray.addNode(node);
}
// 所有对象
Obj[] objs = {new Obj("1"),new Obj("2"),new Obj("3"), new Obj("4"),new Obj("5")};
for (Obj obj : objs) {
nodeArray.put(obj);
}
for (Obj obj : objs) {
System.out.println(nodeArray.get(obj));
}

// 强行增加节点
nodeArray.addNode(new Node("192.168.1.4"));
System.out.println("========== after =============");
for (Obj obj : objs) {
System.out.println(nodeArray.get(obj));
}

// 强行删除节点
nodeArray.addNode(new Node("192.168.1.4"));
System.out.println("========== after =============");
for (Obj obj : objs) {
System.out.println(nodeArray.get(obj));
}

}

执行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
192.168.0.1   Obj{name='1'}
192.168.0.3 Obj{name='2'}
192.168.0.2 Obj{name='3'}
192.168.0.2 Obj{name='4'}
192.168.0.2 Obj{name='5'}
========== after =============
192.168.1.4 null
192.168.0.1 null
192.168.0.3 null
192.168.0.3 null
192.168.0.3 null
========== after =============
192.168.0.1 Obj{name='1'}
192.168.0.3 Obj{name='2'}
192.168.0.2 Obj{name='3'}
192.168.0.2 Obj{name='4'}
192.168.0.2 Obj{name='5'}

从结果中可以看到,数据初始化完毕之后,第一次加载数据正常,新增一个节点之后就到别的机器上寻找缓存,删除新增的节点之后又恢复正常。

一致性Hash算法

一致性Hash算法在普通的Hash算法基础之上,修改了取余的方式。

由于机器会变化,索性就不对机器数取余,找个中立的值2的32次方。

将我们的缓存节点通过Hash的方式取余之后分布在炒鸡大的一个手动构造的圆环中。每个缓存数据根据自己的值到Hash环上顺时针寻找离自己最近的服务器。

这样即使中间少了一台服务器,也能够从这个服务器之后继续往后寻找一个新的可用的服务器节点。

我们将NodeArray改造一下:

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

public class NodeArray {
TreeMap<Integer, Node> nodes = new TreeMap<>();
public void addNode(Node node) {
nodes.put(node.hashCode(), node);
}
public void removeNode(Node node) {
nodes.remove(node.hashCode());
}
Obj get(Obj obj) {
Node node;
node = nodes.get(obj.hashCode());
if (node != null) {
System.out.print(node.getAddress() + " ");
return node.getObj(obj);
}
// 找到比给定 key 大的集合
SortedMap<Integer, Node> tailMap = nodes.tailMap(obj.hashCode());
// 找到最小的节点
int nodeHashcode = tailMap.isEmpty() ? nodes.firstKey() : tailMap.firstKey();
node = nodes.get(nodeHashcode);
System.out.print(node.getAddress() + " ");
return node.getObj(obj);
}
void put(Obj obj) {
int objHashcode = obj.hashCode();
Node node = nodes.get(objHashcode);
if (node != null) {
node.putObj(obj);
return;
}

// 找到比给定 key 大的集合
SortedMap<Integer, Node> tailMap = nodes.tailMap(objHashcode);
// 找到最小的节点
int nodeHashcode = tailMap.isEmpty() ? nodes.firstKey() : tailMap.firstKey();
nodes.get(nodeHashcode).putObj(obj);
}
}

运行结果为:

1
2
3
4
5
6
7
8
9
10
11
12

192.168.0.3 Obj{name='1'}
192.168.0.2 Obj{name='2'}
192.168.0.1 Obj{name='3'}
192.168.0.3 Obj{name='4'}
192.168.0.3 Obj{name='5'}
========== after =============
192.168.0.3 Obj{name='1'}
192.168.0.2 Obj{name='2'}
192.168.0.1 Obj{name='3'}
192.168.0.3 Obj{name='4'}
192.168.0.3 Obj{name='5'}

一致性Hash算法的问题

一致性Hash由于Hash之后可能定位到相近的地址段中导致负载不均的问题,因此我们需要引入虚拟节点来解决这个问题
其工作原理是:将一个物理节点拆分为多个虚拟节点,并且同一个物理节点的虚拟节点尽量均匀分布在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
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

public class NodeArray {
/**
* 虚拟节点
*/
SortedMap<Integer, Node> virtualNodes = new TreeMap<>();

/**
* 虚拟节点数
*/
final int VIRTUAL_NODES = 5;
public void addNode(Node node) {
for (int i = 0; i < VIRTUAL_NODES; i++) {
Node virtualNode = new Node(node.getAddress() + "&&VN" + i);
virtualNode.node = node.node;
int hash = HashUtils.getHash(virtualNode.getAddress());
System.out.println("虚拟节点[" + virtualNode + "]被添加, hash值为" + hash);
virtualNodes.put(hash, virtualNode);
}
}
public void removeNode(Node node) {
for (int i = 0; i < VIRTUAL_NODES; i++) {
Node virtualNode = new Node(node.getAddress() + "&&VN" + i);
int hash = HashUtils.getHash(virtualNode.getAddress());
System.out.println("虚拟节点[" + virtualNode + "]被删除, hash值为" + hash);
virtualNodes.remove(hash);
}
}
Obj get(Obj obj) {
Node node;
node = virtualNodes.get(obj.hashCode());
if (node != null) {
System.out.print(node.getAddress() + " ");
return node.getObj(obj);
}
// 找到比给定 key 大的集合
SortedMap<Integer, Node> tailMap = virtualNodes.tailMap(obj.hashCode());
// 找到最小的节点
int nodeHashcode = tailMap.isEmpty() ? virtualNodes.firstKey() : tailMap.firstKey();
node = virtualNodes.get(nodeHashcode);
System.out.print(node.getAddress() + " ");
return node.getObj(obj);
}
void put(Obj obj) {
int objHashcode = obj.hashCode();
Node node = virtualNodes.get(objHashcode);
if (node != null) {
node.putObj(obj);
return;
}

// 找到比给定 key 大的集合
SortedMap<Integer, Node> tailMap = virtualNodes.tailMap(objHashcode);
// 找到最小的节点
int nodeHashcode = tailMap.isEmpty() ? virtualNodes.firstKey() : tailMap.firstKey();
virtualNodes.get(nodeHashcode).putObj(obj);
}
}

执行结果:

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

虚拟节点[Node{address='192.168.0.1&&VN0'}]被添加, hash值为675967561
虚拟节点[Node{address='192.168.0.1&&VN1'}]被添加, hash值为281940379
虚拟节点[Node{address='192.168.0.1&&VN2'}]被添加, hash值为533000324
虚拟节点[Node{address='192.168.0.1&&VN3'}]被添加, hash值为662119032
虚拟节点[Node{address='192.168.0.1&&VN4'}]被添加, hash值为194906877
虚拟节点[Node{address='192.168.0.2&&VN0'}]被添加, hash值为99777120
虚拟节点[Node{address='192.168.0.2&&VN1'}]被添加, hash值为1792088293
虚拟节点[Node{address='192.168.0.2&&VN2'}]被添加, hash值为1807865840
虚拟节点[Node{address='192.168.0.2&&VN3'}]被添加, hash值为1517366624
虚拟节点[Node{address='192.168.0.2&&VN4'}]被添加, hash值为1408334810
虚拟节点[Node{address='192.168.0.3&&VN0'}]被添加, hash值为2003205714
虚拟节点[Node{address='192.168.0.3&&VN1'}]被添加, hash值为51298955
虚拟节点[Node{address='192.168.0.3&&VN2'}]被添加, hash值为1514128021
虚拟节点[Node{address='192.168.0.3&&VN3'}]被添加, hash值为963993841
虚拟节点[Node{address='192.168.0.3&&VN4'}]被添加, hash值为192869356
192.168.0.2&&VN4 Obj{name='1'}
192.168.0.2&&VN4 Obj{name='2'}
192.168.0.3&&VN3 Obj{name='3'}
192.168.0.2&&VN4 Obj{name='4'}
192.168.0.2&&VN4 Obj{name='5'}
虚拟节点[Node{address='192.168.1.4&&VN0'}]被添加, hash值为1646480332
虚拟节点[Node{address='192.168.1.4&&VN1'}]被添加, hash值为870978208
虚拟节点[Node{address='192.168.1.4&&VN2'}]被添加, hash值为1782522189
虚拟节点[Node{address='192.168.1.4&&VN3'}]被添加, hash值为859295554
虚拟节点[Node{address='192.168.1.4&&VN4'}]被添加, hash值为103010354
========== after =============
192.168.0.2&&VN4 Obj{name='1'}
192.168.0.2&&VN4 Obj{name='2'}
192.168.0.3&&VN3 Obj{name='3'}
192.168.0.2&&VN4 Obj{name='4'}
192.168.0.2&&VN4 Obj{name='5'}

JSON数据类型

从MySQL 5.7.8开始,MySQL支持JSON数据类型 ,可以高效访问JSON文档中的数据。与在字符串列中存储JSON格式字符串相比,数据类型具有以下优势:

  • 存储在JSON列中的JSON文档的自动验证 。错误的JSON数据无法插入
  • 优化的存储格式。可以直接用SQL语句查询JSON文档中的内容。

JSON存储要求

通常,JSON列的存储要求与LONGBLOB或 LONGTEXT列的存储要求大致相同; 也就是说,JSON文档占用的空间与存储在其中一种类型的列中的文档字符串表示形式大致相同。

但是,存储在JSON文档中的各个值的二进制编码(包括查找所需的元数据和字典)会产生开销。例如,存储在JSON文档中的字符串需要4到10个字节的额外存储空间,具体取决于字符串的长度以及存储它的对象或数组的大小。

此外,MySQL对存储在JSON列中的任何JSON文档的大小施加限制,使得它不能大于值的任何大小 max_allowed_packet。

JSON列不能有非NULL默认值。JSON列,如其他二进制类型的列,不直接索引; 相反,可以在生成的列上创建索引,该列从列中提取标量值JSON。

基本函数

创建JSON值

  • JSON_ARRAY([val[, val] …])
    计算(可能为空)值列表并返回包含这些值的JSON数组。

mysql> SELECT JSON_ARRAY(1, “abc”, NULL, TRUE, CURTIME());
+———————————————+
| JSON_ARRAY(1, “abc”, NULL, TRUE, CURTIME()) |
+———————————————+
| [1, “abc”, null, true, “11:30:24.000000”] |
+———————————————+

  • JSON_OBJECT([key, val[, key, val] …])

    计算键值对(可能为空)并返回包含这些对的JSON对象。如果任何键名称NULL或参数数量为奇数,则会发生错误。

mysql> SELECT JSON_OBJECT(‘id’, 87, ‘name’, ‘carrot’);
+—————————————–+
| JSON_OBJECT(‘id’, 87, ‘name’, ‘carrot’) |
+—————————————–+
| {“id”: 87, “name”: “carrot”} |
+—————————————–+

  • JSON_QUOTE(string)

通过用双引号字符包装并转义内部引号和其他字符,然后将结果作为utf8mb4字符串返回,将字符串引用为JSON值 。如果参数是NULL,则 返回NULL。

mysql> SELECT JSON_QUOTE(‘null’), JSON_QUOTE(‘“null”‘);
+——————–+———————-+
| JSON_QUOTE(‘null’) | JSON_QUOTE(‘“null”‘) |
+——————–+———————-+
| “null” | “\”null\”” |
+——————–+———————-+
mysql> SELECT JSON_QUOTE(‘[1, 2, 3]’);
+————————-+
| JSON_QUOTE(‘[1, 2, 3]’) |
+————————-+
| “[1, 2, 3]” |
+————————-+

在MySQL中,JSON值被写为字符串。
MySQL会去解析设置为JSON类型的字符串,如果插入的是错误的JSON,这样会导致插入失败

如以下示例所示:

  • JSON 如果值是有效的JSON值,则 尝试将值插入列成功,但如果不是,则尝试失败:

mysql> CREATE TABLE t1 (jdoc JSON);
Query OK, 0 rows affected (0.20 sec)
mysql> INSERT INTO t1 VALUES(‘{“key1”: “value1”, “key2”: “value2”}’);
Query OK, 1 row affected (0.01 sec)
mysql> INSERT INTO t1 VALUES(‘[1, 2,’);
ERROR 3140 (22032) at line 2: Invalid JSON text:
“Invalid value.” at position 6 in value (or column) ‘[1, 2,’.

  • JSON_TYPE()函数需要一个JSON参数并尝试将其解析为JSON值。如果值有效,则返回值的JSON类型,否则产生错误:

mysql> SELECT JSON_TYPE(‘[“a”, “b”, 1]’);
+—————————-+
| JSON_TYPE(‘[“a”, “b”, 1]’) |
+—————————-+
| ARRAY |
+—————————-+
mysql> SELECT JSON_TYPE(‘“hello”‘);
+———————-+
| JSON_TYPE(‘“hello”‘) |
+———————-+
| STRING |
+———————-+
mysql> SELECT JSON_TYPE(‘hello’);
ERROR 3146 (22032): Invalid data type for JSON data in argument 1
to function json_type; a JSON string or JSON type is required.

  • JSON_ARRAY()获取(可能为空)值列表并返回包含这些值的JSON数组:

mysql> SELECT JSON_ARRAY(‘a’, 1, NOW());
+—————————————-+
| JSON_ARRAY(‘a’, 1, NOW()) |
+—————————————-+
| [“a”, 1, “2015-07-27 09:43:47.000000”] |
+—————————————-+

  • JSON_OBJECT() 获取(可能为空)键值对列表并返回包含这些对的JSON对象:

mysql> SELECT JSON_OBJECT(‘key1’, 1, ‘key2’, ‘abc’);
+—————————————+
| JSON_OBJECT(‘key1’, 1, ‘key2’, ‘abc’) |
+—————————————+
| {“key1”: 1, “key2”: “abc”} |
+—————————————+

  • JSON_MERGE() 获取两个或多个JSON文档并返回组合结果:

mysql> SELECT JSON_MERGE(‘[“a”, 1]’, ‘{“key”: “value”}’);
+——————————————–+
| JSON_MERGE(‘[“a”, 1]’, ‘{“key”: “value”}’) |
+——————————————–+
| [“a”, 1, {“key”: “value”}] |
+——————————————–+

搜索和修改JSON值

路径表达式对于提取JSON文档的一部分或修改JSON文档的函数很有用,以指定该文档中的操作位置。
例如,以下查询从JSON文档中提取具有name键的成员的值:

mysql> SELECT JSON_EXTRACT(‘{“id”: 14, “name”: “Aztalan”}’,’$.name’);

+———————————————————+
| JSON_EXTRACT(‘{“id”: 14, “name”: “Aztalan”}’, ‘$.name’) |
+———————————————————+
| “Aztalan” |
+———————————————————+

路径语法使用前导$字符来表示正在选择的JSON文档, 后面跟着对应的选择器。

  • 点后面跟的是JSON对象中指定的键。
  • [N]附加到路径后面path后面,选择一个数组中位置N,数组位置是从零开始的整数。如果path不是数组,则[0]选取的整个对象

mysql> SELECT JSON_SET(‘“x”‘, ‘$[0]’, ‘a’);
+——————————+
| JSON_SET(‘“x”‘, ‘$[0]’, ‘a’) |
+——————————+
| “a” |
+——————————+
1 row in set (0.00 sec)
mysql> SELECT JSON_SET(‘{“id”: 14, “name”: “Aztalan”}’, ‘$[1]’, ‘ccc’);
+———————————————————-+
| JSON_SET(‘{“id”: 14, “name”: “Aztalan”}’, ‘$[1]’, ‘ccc’) |
+———————————————————-+
| [{“id”: 14, “name”: “Aztalan”}, “ccc”] |
+———————————————————-+
1 row in set (0.00 sec)

  • 路径可以包含*或 **通配符:

    • .[*] JSON对象中所有成员的值。
    • [*] JSON数组中所有元素的值。
    • prefix**suffix 计算所有以命名前缀开头并以命名后缀结尾的路径。
  • 文档中不存在的路径的计算结果为NULL。

让我们$用三个元素来引用这个JSON数组:

1
[3, {"a": [5, 6], "b": 10}, [99, 100]]
  • $[0]计算结果为3。
  • $[1]计算结果为{“a”: [5, 6], “b”: 10}。
  • $[2]计算结果为[99, 100]。
  • $[3]计算结果为NULL (它指的是第四个不存在的数组元素)。
  • $[1].a计算结果为[5, 6]。
  • $[1].a[1]计算结果为 6。
  • $[1].b计算结果为 10。
  • $[2][0]计算结果为 99。

如果路径表达式中的键名称不合法,则必须引号对键进行引用。

1
{"a fish": "shark", "a bird": "sparrow"}
  • $.”a fish”计算结果为 shark。
  • $.”a bird”计算结果为 sparrow。

计算多个路径数组:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

mysql> SELECT JSON_EXTRACT('{"a": 1, "b": 2, "c": [3, 4, 5]}', '$.*');
+---------------------------------------------------------+
| JSON_EXTRACT('{"a": 1, "b": 2, "c": [3, 4, 5]}', '$.*') |
+---------------------------------------------------------+
| [1, 2, [3, 4, 5]] |
+---------------------------------------------------------+
mysql> SELECT JSON_EXTRACT('{"a": 1, "b": 2, "c": [3, 4, 5]}', '$.c[*]');
+------------------------------------------------------------+
| JSON_EXTRACT('{"a": 1, "b": 2, "c": [3, 4, 5]}', '$.c[*]') |
+------------------------------------------------------------+
| [3, 4, 5] |
+------------------------------------------------------------+
mysql> SELECT JSON_EXTRACT('{"a": {"b": 1}, "c": {"b": 2}}', '$**.b');
+---------------------------------------------------------+
| JSON_EXTRACT('{"a": {"b": 1}, "c": {"b": 2}}', '$**.b') |
+---------------------------------------------------------+
| [1, 2] |
+---------------------------------------------------------+
  • JSON_CONTAINS(target, candidate[, path])

通过返回1或0来指示给定的candidate来标示JSON文档是否包含在target的JSON文档中,或者如果提供了path参数检查是否在目标内的特定路径中找到候选项。

如果target或candidate不是有效的JSON文档,或者如果path参数不是一个有效的路径表达式或包含一个 *或**通配符则会报错。

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

mysql> SET @j = '{"a": 1, "b": 2, "c": {"d": 4}}';
mysql> SET @j2 = '1';
mysql> SELECT JSON_CONTAINS(@j, @j2, '$.a');
+-------------------------------+
| JSON_CONTAINS(@j, @j2, '$.a') |
+-------------------------------+
| 1 |
+-------------------------------+
mysql> SELECT JSON_CONTAINS(@j, @j2, '$.b');
+-------------------------------+
| JSON_CONTAINS(@j, @j2, '$.b') |
+-------------------------------+
| 0 |
+-------------------------------+

mysql> SET @j2 = '{"d": 4}';
mysql> SELECT JSON_CONTAINS(@j, @j2, '$.a');
+-------------------------------+
| JSON_CONTAINS(@j, @j2, '$.a') |
+-------------------------------+
| 0 |
+-------------------------------+
mysql> SELECT JSON_CONTAINS(@j, @j2, '$.c');
+-------------------------------+
| JSON_CONTAINS(@j, @j2, '$.c') |
+-------------------------------+
| 1 |
+-------------------------------+
  • JSON_CONTAINS_PATH(json_doc, one_or_all, path[, path] …)

返回0或1以指示JSON文档是否包含给定路径或路径的数据。

  • ‘one’:如果文档中至少存在一个路径,则为1,否则为0。

  • ‘all’:如果文档中存在所有路径,则为1,否则为0。

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

mysql> SET @j = '{"a": 1, "b": 2, "c": {"d": 4}}';
mysql> SELECT JSON_CONTAINS_PATH(@j, 'one', '$.a', '$.e');
+---------------------------------------------+
| JSON_CONTAINS_PATH(@j, 'one', '$.a', '$.e') |
+---------------------------------------------+
| 1 |
+---------------------------------------------+
mysql> SELECT JSON_CONTAINS_PATH(@j, 'all', '$.a', '$.e');
+---------------------------------------------+
| JSON_CONTAINS_PATH(@j, 'all', '$.a', '$.e') |
+---------------------------------------------+
| 0 |
+---------------------------------------------+
mysql> SELECT JSON_CONTAINS_PATH(@j, 'one', '$.c.d');
+----------------------------------------+
| JSON_CONTAINS_PATH(@j, 'one', '$.c.d') |
+----------------------------------------+
| 1 |
+----------------------------------------+
mysql> SELECT JSON_CONTAINS_PATH(@j, 'one', '$.a.d');
+----------------------------------------+
| JSON_CONTAINS_PATH(@j, 'one', '$.a.d') |
+----------------------------------------+
| 0 |
+----------------------------------------+
  • JSON_EXTRACT(json_doc, path[, path] …)

返回JSON文档中的数据,该文档从path 参数匹配的文档部分中选择。

mysql> SELECT JSON_EXTRACT(‘[10, 20, [30, 40]]’, ‘$[1]’);
+——————————————–+
| JSON_EXTRACT(‘[10, 20, [30, 40]]’, ‘$[1]’) |
+——————————————–+
| 20 |
+——————————————–+
mysql> SELECT JSON_EXTRACT(‘[10, 20, [30, 40]]’, ‘$[1]’, ‘$[0]’);
+—————————————————-+
| JSON_EXTRACT(‘[10, 20, [30, 40]]’, ‘$[1]’, ‘$[0]’) |
+—————————————————-+
| [20, 10] |
+—————————————————-+
mysql> SELECT JSON_EXTRACT(‘[10, 20, [30, 40]]’, ‘$[2][]’);
+———————————————–+
| JSON_EXTRACT(‘[10, 20, [30, 40]]’, ‘$[2][
]’) |
+———————————————–+
| [30, 40] |
+———————————————–+

数据库事务管理

事务特性-ACID

ACID,是指数据库管理系统(DBMS)在写入或更新资料的过程中,为保证事务是正确可靠的,所必须具备的四个特性:原子性(atomicity,或称不可分割性)、一致性(consistency)、隔离性(isolation,又称独立性)、持久性(durability)。

  • Atomicity(原子性):一个事务中的所有操作,或者全部完成,或者全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被恢复(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。
  • Consistency(一致性):在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设约束、触发器、级联回滚等。实际上保证业务是否正确是要业务代码来最终保证的,数据库能做的非常有限。
  • Isolation(隔离性):数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括读未提交(Read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(Serializable)。
  • Durability(持久性):事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。

对应在软件中实际的实现方式:

  • 原子性: 支持未完成的数据修改回滚的机制,在java中一般使用@Transactional标示事务出现异常之后回滚的方式 -
  • 一致性: 力所能及的数据合法性检查,简单方式是利用数据库的字段约束进行数据一致性校验,但更多的是在代码中判断插入数据是否符合预期
  • 隔离性: 保证数据并发的修改的规则,一般加锁去处理并发修改的问题
  • 持久性: 使用基于持久化存储(磁盘、SSD)的方式对数据进行存储

事务隔离级别

隔离级别 脏读(Dirty Read) 不可重复读(NonRepeatable Read) 幻读(Phantom Read)
未提交读(Read uncommitted) 可能 可能 可能
已提交读(Read committed) 不可能 可能 可能
可重复读(Repeatable read) 不可能 不可能 可能
可串行化(Serializable ) 不可能 不可能 不可能
  • 未提交读(Read Uncommitted):允许脏读,也就是可能读取到其他会话中未提交事务修改的数据
  • 提交读(Read Committed):只能读取到已经提交的数据。Oracle等多数数据库默认都是该级别 (不重复读)。基于MVCC实现
  • 可重复读(Repeated Read):可重复读。在同一个事务内的查询都是事务开始时刻一致的,InnoDB默认级别。基于MVCC实现
  • 串行读(Serializable):完全串行化的读,每次读都需要获得表级共享锁,读写相互都会阻塞。读加共享锁,写加排他锁,读写互斥。

MVCC在MySQL的InnoDB中的实现

Multi-Version Concurrency Control 多版本并发控制,MVCC 是一种并发控制的方法,也就是一种乐观锁。

在InnoDB中,会在每行数据后添加两个额外的隐藏的值来实现MVCC,这两个值一个记录这行数据何时被创建,另外一个记录这行数据何时过期(或者被删除)。 在实际操作中,存储的并不是时间,而是事务的版本号,每开启一个新事务,事务的版本号就会递增。

在可重读Repeatable reads事务隔离级别下:

  • SELECT时,读取创建版本号<=当前事务版本号,删除版本号为空或>当前事务版本号。
  • INSERT时,保存当前事务版本号为行的创建版本号
  • DELETE时,保存当前事务版本号为行的删除版本号
  • UPDATE时,插入一条新纪录,保存当前事务版本号为行创建版本号,同时保存当前事务版本号到原来删除的行

消除幻读 Next-Key锁

Next-Key锁是行锁和GAP(间隙锁)的合并。

Gap锁会锁定一个范围,但不包括记录本身。GAP锁的目的,是为了防止同一事务的两次当前读,出现幻读的情况。

UPDATE和DELETE时,除了对唯一索引的唯一搜索外都会获取gap锁或next-key锁。即锁住其扫描的范围。
在UPDATE时候,会在这行的上下几行进行加锁,即使上下几行都没有修改任何数据,Innodb也会在这个区间加gap锁,而其它区间不会影响,事务C正常插入。

事务传播行为

事务传播行为用来描述由某一个事务传播行为修饰的方法被嵌套进另一个方法的时事务如何传播。

事务传播行为类型 说明
PROPAGATION_REQUIRED 如果当前没有事务,就新建一个事务,如果已经存在一个事务中,加入到这个事务中。这是最常见的选择。
PROPAGATION_SUPPORTS 支持当前事务,如果当前没有事务,就以非事务方式执行。
PROPAGATION_MANDATORY 使用当前的事务,如果当前没有事务,就抛出异常。
PROPAGATION_REQUIRES_NEW 新建事务,如果当前存在事务,把当前事务挂起。
PROPAGATION_NOT_SUPPORTED 以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
PROPAGATION_NEVER 以非事务方式执行,如果当前存在事务,则抛出异常。
PROPAGATION_NESTED 如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与PROPAGATION_REQUIRED类似的操作。

PAC

代理自动配置(英语:Proxy auto-config,简称PAC)是一种网页浏览器技术,用于定义浏览器该如何自动选择适当的代理服务器来访问一个网址。

一个PAC文件包含一个JavaScript形式的函数“FindProxyForURL(url, host)”。这个函数返回一个包含一个或多个访问规则的字符串。用户代理根据这些规则适用一个特定的代理器或者直接访问。当一个代理服务器无法响应的时候,多个访问规则提供了其他的后备访问方法。浏览器在访问其他页面以前,首先访问这个PAC文件。PAC文件中的URL可能是手工配置的,也可能是是通过网页的网络代理自动发现协议(WPAD)自动配置的。

PAC内置函数

PAC脚本提供了几个内置函数:

dnsDomainIs: 类似于 ==,但是对大小写不敏感

1
2
3
4
if (dnsDomainIs(host, "google.com") || 
dnsDomainIs(host, "www.google.com")) {
return "DIRECT";
}

shExpMatch:正则匹配

1
2
3
4
if (shExpMatch(host, "vpn.domain.com") ||
shExpMatch(url, "http://abcdomain.com/folder/*")) {
return "DIRECT";
}

isInNet:判断是否在网段内容,比如 10.1.0.0 这个网段,10.1.1.0 就在网段中

1
2
3
4

if (isInNet(dnsResolve(host), "172.16.0.0", "255.240.0.0")) {
return "DIRECT";
}

dnsResolve:通过 DNS 查询主机 ip

1
2
3
4
5
6
7

if (isInNet(dnsResolve(host), "10.0.0.0", "255.0.0.0") ||
isInNet(dnsResolve(host), "172.16.0.0", "255.240.0.0") ||
isInNet(dnsResolve(host), "192.168.0.0", "255.255.0.0") ||
isInNet(dnsResolve(host), "127.0.0.0", "255.255.255.0")) {
return "DIRECT";
}

myIpAddress:返回主机的 IP

1
2
3
if (isInNet(myIpAddress(), "10.10.1.0", "255.255.255.0")) {
return "PROXY 10.10.5.1:8080";
}

isResolvable:判断主机是否可访问

1
2
3
if (isResolvable(host)) {
return "PROXY proxy1.example.com:8080";
}

dnsDomainLevels:返回是几级域名,比如 dnsDomainLevels(barretlee.com) 返回的结果就是 1

1
2
3
4
5
if (dnsDomainLevels(host) > 0) {
return "PROXY proxy1.example.com:8080";
} else {
return "DIRECT";
}

PAC基本语法

FindProxyForURL最后返回的字符串就是目标地址采用的代理类型。

可支持的类型: SOCKS5 [host:port] SOCKS [host:port] PROXY [host:port] 和 DIRECT 直连几种类型

而且可以连接在一起写,以达到代理自动降级的目的:
比如return ‘SOCKS5 bac.com:80 ; SOCKS abc.com:80; DIRECT;’
如果bac.com无法访问会降级到访问abc.com,最后会采用直连的方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function FindProxyForURL(url, host) {
// our local URLs from the domains below example.com don't need a proxy:
if (shExpMatch(url,"*.example.com/*")) {return "DIRECT";}
if (shExpMatch(url, "*.example.com:*/*")) {return "DIRECT";}

// URLs within this network are accessed through
// port 8080 on fastproxy.example.com:
if (isInNet(host, "10.0.0.0", "255.255.248.0")) {
return "PROXY fastproxy.example.com:8080";
}

// All other requests go through port 8080 of proxy.example.com.
// should that fail to respond, go directly to the WWW:
return "PROXY proxy.example.com:8080; DIRECT";
}

什么是控制反转

在一般情况下,如果Class A 使用到了Class B的对象,那么就要在Class A中new出来一个Class B的对象。

1
2
3
4
5
6
7
8
9
10
11
class B{
public void run(){
System.out.println("----我是B----");
}
}
class A{
public void run(){
B b = new B();,
b.run();
}
}

对象A依赖于对象B,那么对象A在初始化或者运行到某一点的时候,自己必须主动去创建对象B或者使用已经创建的对象B。无论是创建还是使用对象B,控制权都在自己手上。

倘若粗心忘记了创建B的对象。。emmmm。。。。

这个时候就需要依赖注入框架来解决管理对象的问题,
采用依赖注入技术之后,A的代码只需要定义一个私有的B对象,不需要直接new来获得这个对象,而是通过相关的容器控制程序来将B对象在外部new出来并注入到A类里的引用中。
好莱坞有一条著名的原则:“不要给我们打电话,我们会给你打电话(don‘t call us, we‘ll call you)”。
就是所有的演员(对象)交由给演艺公司(spring)控制,导演需要的时候就从演艺公司把演员拿过来,不需要就把演员丢了。

1
2
3
4
5
6
7
8
class BeanFactory{
public static Object create(String beanName){
switch(beanName){
case "B":
return new B();
}
}
}

1
2
3
4
5
6
7
8
9
10
11
class B{
public void run(){
System.out.println("----我是B----");
}
}
class A{
private B b = BeanFactory.create("B");,
public void run(){
b.run();
}
}

什么是依赖注入

依赖注入是实现控制反转的一种方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
class B{
public void run(){
System.out.println("----我是B----");
}
}
class A{
A() {
b = new B();
}
public void run(){
b.run();
}
}

还是这段代码,如果现在要改变B的生成方式会变得十分的麻烦,因为要去修改A的构造函数。如果B是一个接口的话,更加难以操作,因为B的实现类可能有多种。
但是改成下面这样就能很容易实现创建B的操作。
像这种非自己主动初始化依赖,而通过外部来传入依赖的方式,我们就称为依赖注入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class B{
public void run(){
System.out.println("----我是B----");
}
}
class A{
private B b;
A(B b) {
this.b = b;
}
public void run(){
b.run();
}
}

Spring中的依赖注入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class B{
public void run(){
System.out.println("----我是B----");
}
}
class A{
private B b;
public void setFinder(B b) {
this.b = b;
}
public void run(){
b.run();
}
}
1
2
3
4
5
6
7
8
9
<beans>
<bean id="A" class="A">
<property name="b">
<ref local="B"/>
</property>
</bean>
<bean id="B" class="B">
</bean>
</beans>

这样通过xml文件就能够将B的实例注入到A中

总结

  1. 控制反转是一种在软件工程中解耦合的思想,调用类只依赖接口,而不依赖具体的实现类,减少了耦合。控制权交给了容器,在运行的时候才由容器决定将具体的实现动态的“注入”到调用类的对象中。
  2. 依赖注入是一种设计模式,可以作为控制反转的一种实现方式。依赖注入就是将实例变量传入到一个对象中去(Dependency injection means giving an object its instance variables)。
  3. 通过IoC框架,类A依赖类B的强耦合关系可以在运行时通过容器建立,也就是说把创建B实例的工作移交给容器,类A只管使用就可以。

Spring重写Cookie解析

SessionId是什么

SessionId是在访问网站期间为用户分配的特定的一串唯一编号。会话ID可以存储为cookie,表单字段或URL中。session在访问服务器的时候就被创建,之后会被客户端
保存在cookie当中,之后访问都会将session再次回传到服务器上用于标示请求所属。服务器接收到客户端的请求解析请求头中的cookie对应的sessionId,再由sessionId找到对应的session

解析

默认的sessionId的key字段是SESSION,默认的解析器是DefaultCookieSerializer,在一些情况下key可能是其他的一些值,这个时候就需要去重写DefaultCookieSerializer来达到自定义的session解析的目的。
多系统的多点登陆可以使用自定义字段来定义。

比如A、B、C 三个系统所属同一个域名,但是A、B、C可以使用不同的账户同时登陆,这个时候就能够使用自定义解析来达到这个目的。

通过APPID,来标示是从哪个系统来的,并且找出这个APPID对应独立的sessionId

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

@Bean
public DefaultCookieSerializer cookieWebSessionIdResolver() {

return new DefaultCookieSerializer() {
@Override
public List<String> readCookieValues(HttpServletRequest request) {
String appId = request.getHeader('APPID');
String name = "JSESSIONID-SSO-" + appId;
Cookie[] cookies = request.getCookies();
List<String> matchingCookieValues = new ArrayList<>();
if (cookies != null) {
for (Cookie cookie : cookies) {
if (name.equals(cookie.getName())) {
String sessionId = cookie.getValue();
if (sessionId == null) {
continue;
}
matchingCookieValues.add(sessionId);
}
}
}
return matchingCookieValues;
}
};
}