springboot包结构

使用springboot打包插件

如果用springboot插件进行打包以后,包结构会发生变化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<mainClass>com.tencent.xxx.Application</mainClass>
</configuration>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>

springboot打包的jar包用tar -xvf解压以后,大致结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
+-META-INF
| +-MANIFEST.MF
| +-maven
| +-pom.xml
| +-pom.properties
+-org
| +-springframework
| +-boot
| +-loader
| +-<spring boot loader classes>
+-BOOT-INF
+-classes
| +-com
| +-tencent
| +-xxx.class
| +-其他src/main/resource路径下的文件
+-lib
+-依赖的jar包

可以看出主要分为三部分:
META-INF文件夹: 元数据信息;
org文件夹: springboot框架相关的class和依赖;
BOOT-INF: 我们写的代码、resource以及引入的相关依赖。

其中比较重要的是元数据信息中的MANIFEST.MF:

1
2
3
4
5
6
7
8
9
10
Manifest-Version: 1.0
Archiver-Version: Plexus Archiver
Built-By: mengqifeng
Start-Class: com.tencent.xxx.Application
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Spring-Boot-Version: 2.2.0.M4
Created-By: Apache Maven 3.5.3
Build-Jdk: 1.8.0_161
Main-Class: org.springframework.boot.loader.JarLauncher

这里可以看出springboot的入口类是org.springframework.boot.loader.JarLauncher
先启动它这个类(main-class),然后反射调用我们的类(start-class)。

此外由于这里看出lib文件夹的目录是/BOOT-INF/class/lib,我们可以手动在pom文件中修改resource文件的打包路径,对准这个目录放进去就可以作为库文件依赖了:

1
2
3
4
5
6
7
<resource>
<directory>src\main\resources\lib</directory>
<targetPath>/BOOT-INF/class/lib</targetPath>
<includes>
<include>**/*.jar</include>
</includes>
</resource>

默认的maven包结构

如果把xxx.jar.original解压开的话,能得到springbootrepackage以前的包结构。
此时只有两部分,元数据信息和我们写的代码(字节码和资源文件),没有依赖库:

1
2
3
4
5
6
+-META-INF
| +-MANIFEST.MF
| +-maven
| +-pom.xml
| +-pom.properties
+-com(我们写的代码)以及其他src/main/resource路径下的文件

元数据信息中的MANIFEST.MF内容也少一些:

1
2
3
4
5
Manifest-Version: 1.0
Archiver-Version: Plexus Archiver
Built-By: mengqifeng
Created-By: Apache Maven 3.5.3
Build-Jdk: 1.8.0_161

JNI总结

what: JNI是啥

JNI(Java Native Interface)是java访问native方法的接口规范。
所谓native方法一般是c/c++代码。(也可以是汇编)
java实现了一个JNI框架来让java和其他语言互调,java方法可以调JNI接口声明了的native方法,native方法也可以创建、使用java对象。
JNI接口规范主要按照c语言,不像c++一样改写方法名。
因此实际编码中需要用extern c来维持方法名的纯净。

编译方法:
c++: print(int)=>print_int;
c: print.
所以我们需要c这种风格的。(不支持重载)

why: 为啥要使用JNI

使用的场景包括:

  1. 有些现成的代码是c/c++的,需要在java中调用; (比如一些平台相关的、SIMD操作、或其他java中没有的库)
  2. c/c++版本的代码也许有巨大的性能优势。

HOW: JNI如何工作

如何使用JNI

两种方法: 静态注册和动态加载。

静态注册

假设我们要在java中调用c的方法,大致分为6个步骤:

  1. 在java中声明一个native方法,但是不实现;
  2. 编译java字节码,生成class文件;(javac命令)
  3. 用class文件生成.h的文件头;(javah命令)
  4. 创建.c文件实现.h文件头中声明的方法;
  5. 编译.c.h文件生成动态链接库.so;
  6. java中加载.so文件,使用第一步中声明的方法。

相关命令:

1
2
3
4
5
6
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:.
javac HelloWorld.java
javah HelloWorld
gcc -fPIC -I /usr/lib/jvm/jdk/include -I /usr/lib/jvm/jdk/include/linux -shared libHelloWorld.c -o libHelloWorld.so
export LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH # 为了找到so文件
java -Djava.library.path=. HelloWorld # 也是为了找到so文件(二选一即可)

动态加载

利用RegisterNatives方法来注册Java方法与JNI函数的映射。

  1. 利用结构体JNINativeMethod数组记录 Java 方法与 JNI 函数的对应关系
  2. 实现 JNI_OnLoad 方法,在加载动态库后,执行动态注册
  3. 调用 FindClass 方法,获取Java对象
  4. 调用 RegisterNatives方法,传入 Java 对象、JNINativeMethod;
  5. 数组及注册方法数完成注册;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 以下是c++版本的,c语言版本的话很简单,只要把:
// env->改成(*env)->
// 调用的方法参数第一个参数加上env即可。
#define JNI_CLASS_PAPT "com/xxx"
JNIEXPORT jstring JNICALL native_test(JNIEnv *env, jobject instance) {
return env->NewStringUTF("hello world");
}
static JNINativeMethod g_methods[] = {
// Java层方法、参数类型、native方法
{"get_hello_world", "()Ljava/lang/String;", (void*)native_test}
};

// 动态库加载时回调方法
jint JNI_OnLoad(JavaVM *vm, void *reserved) {
JNIEnv *env = NULL;
vm->GetEnv((void**)&env, JNI_VERSION_1_8);
jclass clazz= env->FindClass(JNI_CLASS_PAPT);

// 注册Java和natvie方法映射表
env->RegisterNatives(clazz
, g_methods
, sizeof(g_methods)/sizeof(g_methods[0]));
return JNI_VERSION_1_8;
}

参见jni.h中的JNINativeMethod结构体:

1
2
3
4
5
typedef struct {
const char* name; // java方法名
const char* signature;// java方法签名
void* fnPtr; // c函数指针
} JNINativeMethod;

JNI原理

本质其实就是JVM使用了so动态链接库中的函数,所以关键在于函数名的映射。
一个典型的native方法的签名如下:

1
2
3
4
5
6
// native方法的签名由类名(含包名,点换成下划线)和方法名拼接而成:
JNIEXPORT void JNICALL Java_packname_classname_methodname
(JNIEnv *env, jobject obj)
{
/*Implement Native Method Here*/
}

可见,JVM调用native方法的时候,需要传递一个JNIEnv指针和一个jobject指针。

JNIEnv: 包含访问JVM的接口,可以进行native数组和java数组转换,字符串转换,对象实例化、抛异常等等java能做的事情;

jobject: 声明native方法的java对象。

每一个Java线程对应一个JNIEnv
JNIEnv指针仅在native方法当前线程中有效;如果手动保存到其他地方,然后在其他线程中想要使用,需要调用AttachCurrentThread来挂靠当前线程到jvm,使用完毕后调用DetachCurrentThread脱离jvm。
挂靠样例:

1
2
3
4
5
// 1. 
JNIEnv *env;
(*g_vm)->AttachCurrentThread (g_vm, (void **) &env, NULL);
// 2. 脱离:
(*g_vm)->DetachCurrentThread (g_vm);

类型转换

native和java的基本类型能自动互转,复杂类型(数组、数组、对象)则要使用JNIEnv显式地进行转换。

字符串转换(C++版本):

1
2
3
4
5
6
7
8
9
10
extern "C"
JNIEXPORT void JNICALL Java_ClassName_MethodName
(JNIEnv *env, jobject obj, jstring javaString)
{
// java字符串=>c字符串
const char *nativeString = env->GetStringUTFChars(javaString, 0);
// do something with nativeString
// 释放:
env->ReleaseStringUTFChars(javaString, nativeString);
}

c语言版本就是参数多了env参数:

1
2
3
4
5
6
7
8
JNIEXPORT void JNICALL Java_ClassName_MethodName
(JNIEnv *env, jobject obj, jstring javaString)
{
// 转换:
const char *nativeString = (*env)->GetStringUTFChars(env, javaString, 0);
// 释放:
(*env)->ReleaseStringUTFChars(env, javaString, nativeString);
}

基本类型的映射

native类型 Java类型 描述 java类型签名(signature)
unsigned char jboolean unsigned 8位 Z
signed char jbyte signed 8位 B
unsigned short jchar unsigned 16位 C
short jshort signed 16位 S
long jint signed 32位 I
long long__int64 jlong signed 64位 J
float jfloat 32位 F
double jdouble 64位 D
void void V

string类的类型签名: Ljava/lang/String;
整型数组的类型签名: [I
int[][]的签名: [[I

JNI代码中调用java对象方法

1. 调用实例方法

首先我们有env和obj,步骤是:

  1. 用env、obj获取class对象cls;
  2. 用env、cls和方法签名反射获得方法引用mid;
  3. 用env、obj、mid调用方法。
1
2
3
4
5
6
7
8
9
JNIEXPORT void JNICALL  Java_InstanceMethodCall_nativeMethod(JNIEnv *env, jobject obj) { 
jclass cls = (*env)->GetObjectClass(env, obj);
jmethodID mid = (*env)->GetMethodID(env, cls, "callback", "()V");
if (mid == NULL) {
return; /* method not found */
}
printf("In C\n");
(*env)->CallVoidMethod(env, obj, mid);
}

2. 调用静态方法

前两步和刚才一样,第三部把Obj换成cls即可:

  1. 获取cls;
  2. 获取mid;
  3. 用env、cls、mid调用静态方法。
1
2
3
4
5
6
7
8
9
10
JNIEXPORT void JNICALL  Java_StaticMethodCall_nativeMethod(JNIEnv *env, jobject obj) { 
jclass cls = (*env)->GetObjectClass(env, obj);
jmethodID mid =
(*env)->GetStaticMethodID(env, cls, "callback", "()V");
if (mid == NULL) {
return; /* method not found */
}
printf("In C\n");
(*env)->CallStaticVoidMethod(env, cls, mid); // 这里是cls
}

JNI需要注意的点

  1. native方法自己管理内存,jvm不gc这部分;
  2. JNI调用开销较大,不宜频繁调用;(java数组、字符串都会线性拷贝)
  3. JNI方法平台有关,移植性差;
  4. c代码里显式释放内存;
  5. 字符编码问题。

第四点一般是获取和释放成对使用:(多少get就有多少delete或release)

1
2
GetObjectField=>DeleteLocalRef
GetStringUTFChars=>ReleaseStringUTFChars

最后一个字符编码问题:
JNI里的这几个函数实际上用的是修改版本的UTF-8,并不完全等效于UTF-8

1
2
3
4
5
NewStringUTF
GetStringUTFLength
GetStringUTFChars
ReleaseStringUTFChars
GetStringUTFRegion

用户应当使用这几个函数,先创建UTF-16,然后安全地转换成标准UTF-8

1
2
3
4
5
6
7
NewString
GetStringLength
GetStringChars
ReleaseStringChars
GetStringRegion
GetStringCritical
ReleaseStringCritical

静态链接库和动态链接库

引子

JNI的时候发现要使用so文件、动态链接库,那么究竟什么是动态链接库呢?

参考:
https://www.zhihu.com/question/20484931

概念

库有两种:静态库和动态库。
静态库: .a,.lib;
动态库:.so,.dll;
windows:.lib,.dll;
linux:.a,.so;

非库:
生成.o文件: g++ -c hellospeak.cpp;
// 只产生编译的代码(没有链接link)
生成.out文件: g++ hellospeak.cpp speak.cpp -o hellospeak;
// 执行完整的编译过程,并且生成一个a.out文件。

链接过程

源文件(.h,.cpp)
=>预编译=>编译=>汇编
=>链接
=>可执行文件

这里我们主要关心链接阶段。

静态库

链接阶段:
汇编生成的.o+引用的库=> 可执行文件;

缺点

  1. 静态库在内存中可能造成空间浪费。
    如100个进程都使用了静态库A,则内存中有100份占用。
  2. 更新库不灵活。

动态库(又称共享库)

链接阶段:
不加入引用的库。推迟到运行时。
运行时:
动态加载.so,.dll文件。

缺点

多了运行时计算符号链接的开销,但是这个开销不大。

所以jni只能使用动态链接库。(linux的话就是so文件)

gcc相关参数

https://colobu.com/2018/08/28/15-Most-Frequently-Used-GCC-Compiler-Command-Line-Options/

-fPIC: 产生位置无关的代码;
-shared: 产生共享库;(动态库)
例子:

1
2
gcc -c -Wall -Werror -fPIC Cfile.c
gcc -shared -o libCfile.so Cfile.o

-static: 生成静态链接的文件
例子:

1
gcc main.c -static -o main -lpthread

CriticalNative:降低JNI开销

引子

Android中有@CriticalNative注解:
https://source.android.google.cn/devices/tech/dalvik/improvements
里面说到:

@FastNative 可以使原生方法的性能提升高达 2 倍,@CriticalNative 则可以提升高达4倍。

那么这是怎么做到的呢?

native方法

调用native方法时,JVM的工作步骤:
(源码: http://hg.openjdk.java.net/jdk8u/jdk8u/hotspot/file/4d9931ebf861/src/cpu/x86/vm/sharedRuntime_x86_64.cpp#l1723)

  1. 创建栈帧;
  2. 根据ABI移动参数到寄存器或者栈;(ABI: 应用二进制接口)
  3. 封装对象引用到JNI handlers;
  4. 获取静态方法的JNIEnv*jclass,把他们作为额外参数传递;
  5. 检查是否调用method_entry的trace函数;
  6. 检查是否调用对象锁;(synchronized)(optinal)
  7. 检查native方法是否已经链接;(懒加载函数检查、链接)
  8. 线程状态从in_java转变为in_native;
  9. 调用native方法;
  10. 检查是否需要safepoint;
  11. 线程状态转回in_java;
  12. 解锁对象锁;(optional)
  13. notify method_exit;(optional)
  14. 将对象结果解出,重置JNI handlers;
  15. 处理JNI异常;
  16. 移除栈帧。

开销比较大,主要是用于各种参数拷贝,尤其是遇到数组,需要来回拷贝、检查。

此时,如果是足够简单的native方法,可以用Critical Natives来降低开销。

Critical Native方法

Critical Natives方法是需要满足下列约束的native方法:

  1. 必须是static且没有synchronized; (省掉上一节的6、12步)
  2. 参数类型必须是基本类型或基本类型的数组;(省掉上一节中的对象相关3、14)
  3. 具体实现不能调用JNI函数(也就是不使用JNIEnv* envjclass cls,既然不使用就不用传给它了),不能分配java对象或者抛出异常;(省掉上一节中的4、15)
  4. 不能运行太长时间.(因为它会阻塞gc)

基于这个原理的话, critical native方法比普通native方法快的原因其实是节省了一些调用开始和结束的开销,因此如果被调用的方法如果是时间占用的大头的话,其实这个优化幅度就很小了。
反之如果是频繁调用的方法,而且每次调用的数据量很小,此时调用开销和执行开销是同量级,那么累计的优化幅度就会很大。
(比如只是长度为16的数组计算的话,计算力提升可以达到2~3倍。)

满足上述约束以后,Critical Natives方法还需要进行下列声明:

  1. 方法名以JavaCritical_开头;
  2. 没有额外的JNIEnv*jclass参数;(因为是static方法,自然也就没有jobject参数了)
  3. java数组传递的时候用两个参数: 数组长度、数组引用(基本类型)。
    // 这样不再需要调用GetArrayLengthGetByteArrayElements等函数。

此外critical natives方法变成临界区。
native方法示例:

1
2
3
4
5
6
7
8
9
JNIEXPORT jint JNICALL
Java_com_package_MyClass_nativeMethod(JNIEnv* env, jclass klass, jbyteArray array) {
jboolean isCopy;
jint length = (*env)->GetArrayLength(env, array);
jbyte* buf = (*env)->GetByteArrayElements(env, array, &isCopy);
jint result = process(buf, length);
(*env)->ReleaseByteArrayElements(env, array, buf, JNI_ABORT);
return result;
}

Critical Natives方法示例:

1
2
3
4
JNIEXPORT jint JNICALL
JavaCritical_com_package_MyClass_nativeMethod(jint length, jbyte* buf) {
return process(buf, length);
}

critical版本的方法是JIT需要的(默认是调用超过1500次,可以调JIT参数-XX:CompileThreshold=invocations);
普通native版本的方法是解释器需要的;

因此实际用的时候,这俩版本的代码都要写上。比如是这样的:

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
#include <jni.h>

static int sum(jbyte* array, int length) {
int result = 0;
int i;
for (i = 0; i < length; i++) {
result += array[i];
}
return result;
}
/*
* Class: com_tencent_xxx_test_Natives
* Method: javaCriticalImpl
* Signature: ([B)I
*/
JNIEXPORT jint JNICALL Java_com_tencent_xxx_test_Natives_javaCriticalImpl
(JNIEnv* env, jclass cls, jbyteArray array){
jboolean isCopy;
jint length = (*env)->GetArrayLength(env, array);
jbyte* buf = (jbyte*) (*env)->GetPrimitiveArrayCritical(env, array, &isCopy);
jint result = sum(buf, length);
(*env)->ReleasePrimitiveArrayCritical(env, array, buf, JNI_ABORT);
// 有副作用的c函数用0; 无副作用的c函数直接用JNI_ABORT.
return result;
}

JNIEXPORT jint JNICALL
JavaCritical_com_tencent_xxx_test_Natives_javaCriticalImpl(jint length, jbyte* buf) {
return sum(buf, length);
}

(之所以这么繁琐的原因是这个特性和Unsafe一样是jdk内部使用的,没有公开发布给普通程序员,正式发布估计要到jdk10了)

参考:
http://cr.openjdk.java.net/~jrose/panama/native-call-primitive.html
http://mail.openjdk.java.net/pipermail/panama-dev/2015-December/000225.html
https://stackoverflow.com/questions/36298111/is-it-possible-to-use-sun-misc-unsafe-to-call-c-functions-without-jni/36309652#36309652

程序计算加速之SIMD相关概念

What: 什么是SIMD

SIMD全称Single Instruction Multiple Data,单指令多数据流,是一种采用一个控制器来控制多个处理器,同时对一组数据(又称“数据向量”)中的每一个分别执行相同的操作从而实现空间上的并行性的技术。

也就是说SIMD需要CPU指令集的支持,才能用一条指令就同时并行计算多个数据。当然了,这里同时计算时运用的是同一种运算,比如都是加法。

WHY: 为什么要使用SIMD

能并行计算肯定是要比串行计算快的。
SIMD是cpu层面的加速,当然还有gpu层面的加速比如cuda编程。
如果需要大量浮点数计算、矩阵计算,比如游戏场景、机器学习场景下都是需要这些加速技术的。

不同版本和历史

既然说到cpu,肯定绕不开intel和AMD。
最早是intel推出的,利用了多余的寄存器来加速多媒体运算,后来逐渐标准化:

缩写:

1
2
3
MMX(可能是MultiMedia eXtension的缩写)
SSE(Streaming SIMD Extensions)
AVX(Advanced Vcetor Extension) : 对SSE的后续扩展,主要分为AVX、AVX2、AVX512三种。在目前常见的机器上,大多只支持到AVX系列。

版本

1
2
3
4
5
6
7
8
9
MMX: intel, Pentium;
SSE: intel, Pentium 3;
SSE2: intel, Pentium 4;
SSE3: intel, Pentium 4;
SSE4: intel, Core 2 Duo; 128位。
SSE5: AMD;
AVX: intel, 因为SSE5被AMD抢先出了,intel恼羞成怒改名叫AVX了; 支持256位。
AVX2: intel, 加入了整形支持。支持256位。
AVX512: intel, 支持521位。

HOW: 如何使用SIMD

JAVA中使用

jdk8的话,可以jinfo -flag <pid>一下,这三个其实是默认配置(java8):

1
2
3
-XX:UseAVX=2
-XX:UseSSE=5
-XX:+UseSSE42Intrinsics

可以受益的操作:
加减乘除、乘累加。
所以jvm是默认会对一些代码进行SIMD优化,具体方法是自己构造数组,比如本来只是要统计一个数组的总和,用一个局部变量即可,可以改成用一个长度为8的局部数组(或者16、具体长度需要benchmark才知道最优,要符合cpu的SIMD支持长度),然后在8个位置上分别求和,最后把局部数组求和得到答案,这种代码会比直接求和快1倍。

上述trick自然是非常间接地使用了,直接使用SIMD的库还在开发中:
https://openjdk.java.net/jeps/338
估计要等到java10以后才能用上了。
也有一些国外的scala库(LMS): https://astojanov.github.io/blog/2017/12/20/scala-simd.html?tdsourcetag=s_pcqq_aiomsg
但不知道靠谱不靠谱。

直接使用的库在C语言中是有的,叫做SIMD Intrinsics
https://software.intel.com/sites/landingpage/IntrinsicsGuide/#techs=MMX,SSE,SSE2,SSE3,SSSE3,SSE4_1,SSE4_2,AVX,AVX2,FMA,AVX_512
比如_mm_set_ps1函数。

1
2
???-ss后缀的操作: 4个操作数中有一个参加运算;
???-ps后缀的操作:4个操作数都参加运算。

因此我们可以用JNI调用来使用SIMD。
为了一定程度上减少JNI开销的话,可以使用CriticalNative

参考资料

https://www.cnblogs.com/lgxZJ/p/8688430.html

clickhouse实战之jdbc接入

配置

安装好clickhouse后有几个关键的配置需要调整。
clickhouse的配置文件主要有两个:

1
2
vi /etc/clickhouse-server/config.xml # 服务器配置 
vi /etc/clickhouse-server/users.xml # 客户端连接的默认配置

config.xml里需要调整的主要是数据文件的目录、http端口、监听地址:

1
2
3
4
5
6
<http_port>8080</http_port> <!-- 默认是8123-->
<listen_host>0.0.0.0</listen_host> <!-- 默认只监听本地127.0.0.1-->
<path>/var/lib/clickhouse/</path> <!-- 需要chown -R clickhouse:clickhouse这个目录 如果后续要修改,也可以停服后通过软链接移动到别的目录-->

<uncompressed_cache_size>8589934592</uncompressed_cache_size>
<mark_cache_size>5368709120</mark_cache_size>

此外也能通过配置文件发现默认的tcp端口是9000,interserver_http_port是9009。

users.xml里需要调整的主要是:

1
2
<max_memory_usage>20000000000</max_memory_usage><!-- 单个查询的最大内存使用bytes-->
<password></password>

启动命令

1
2
3
4
5
6
# 服务端:
sudo service clickhouse-server start
# 客户端(多行模式):
clickhouse-client -m
# 客户端执行sql文件:
clickhouse-client -mn < 1.sql

jdbc

首先是引入依赖:

1
2
3
4
5
6
7
8
9
10
11
12
<!-- https://mvnrepository.com/artifact/ru.yandex.clickhouse/clickhouse-jdbc -->
<dependency>
<groupId>ru.yandex.clickhouse</groupId>
<artifactId>clickhouse-jdbc</artifactId>
<version>0.1.54</version>
<exclusions>
<exclusion>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>

这里我项目里其他地方用fasterxml,有版本冲突,因此这里exclude掉了。

然后配置yml:

1
2
3
4
5
6
spring.clickhouse.hikari:
idle-timeout: 1000
jdbc-url: jdbc:clickhouse://<your_host_or_ip_address>:8080/default
driverClassName: ru.yandex.clickhouse.ClickHouseDriver
maximumPoolSize: 10
rewriteBatchedStatements: true

最后像普通jdbc一样使用即可:

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
// 1. 配置:
@Configuration
@ConfigurationProperties(prefix = "spring.clickhouse.hikari")
public class ClickhouseDSConfig extends HikariConfig {
@Bean(name="chds")
public DataSource dataSource() throws SQLException {
return new HikariDataSource(this);
}
}

// 2. 存储层:
@Repository
public class ChDao extends JdbcDaoSupport implements IChDao {
@Autowired
public ChDao(@Qualifier("chds") DataSource ds) {
setDataSource(ds);
}

@Override
public Map<String, Object> query(String sql, Object... args) {
return this.getJdbcTemplate().queryForMap(sql, args);
}

@Override
public List<Map<String, Object>> queryForList(String sql, Object... args) {
return this.getJdbcTemplate().queryForList(sql, args);
}
}

// 3. 使用:
@Autowired
IChDao dao;

public void run() {
System.out.println("begin~~~");
String sqlDB = "show databases";//查询数据库
System.out.println(dao.queryForList(sqlDB));
System.out.println(dao.queryForList("show tables;"));
}

神秘的monad——函数式编程

monad确实比较难理解,我认真翻了一个星期资料才理解。

讲得比较好的参考资料:
http://josephguan.github.io/2016/06/25/monad-in-scala/
比较形象的、有图的:
http://blog.forec.cn/2017/03/02/translation-adit-faamip/
数学上讲得比较多的:(scala版代码可用)
https://segmentfault.com/a/1190000008000905

参考资料3中的scala版代码:

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
// 1. 半群:
trait SemiGroup[A] {
def op(a1: A, a2: A): A
}

// 2. monoid: (还不是monad) 比半群多一个零元
trait Monoid[A] extends SemiGroup[A] {
def zero: A
}

// 函子: (有map函数就是Functor)
trait Functor[F[_]] {
// 输入一个A的容器F[A],输入一个A类型=>B类型的变化
// 输出B类型的容器F[B]
def map[A, B](a: F[A])(f: A => B): F[B]
}


object MonadTest {
def main(args: Array[String]): Unit = {
val stringMonoid = new Monoid[String] {
def op(a1: String, a2: String) = a1 + a2

def zero = ""
}

def listMonoid[A] = new Monoid[List[A]] {
def op(a1: List[A], a2: List[A]) = a1 ++ a2

def zero = Nil
}

def optionMonoid[A] = new Monoid[Option[A]] {
def op(a1: Option[A], a2: Option[A]) = a1 orElse a2

def zero = None
}

def listFunctor = new Functor[List] {
def map[A, B](a: List[A])(f: (A) => B) = a.map(f)
}

}
}


/*trait Monad[M[_]] {
def unit[A](a: A): M[A] //identity
def join[A](mma: M[M[A]]): M[A]
}*/
// Monad: (有unit和flatmap就是monad)
trait Monad[M[_]] {
def unit[A](a: A): M[A]

def flatMap[A, B](fa: M[A])(f: A => M[B]): M[B]

// def join[A](mma: M[M[A]]): M[A] = flatMap(mma)(ma => ma)
}

附抄scala版的monad(参考资料1):

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
trait Monad[+T] {
def flatMap[U]( f : (T) => Monad[U] ) : Monad[U]
def unit(value : B) : Monad[B]
}

// map可以理解为flatmap的特化:
def map[U](f : (T) => U) : Monad[U] = flatMap(v => unit(f(v)))

// Try类型monad:
val result:Try[Int] = Try("5".toInt).flatMap{a =>
Try("6a".toInt).flatMap{b =>
Try("9".toInt).flatMap{c => Try(a + b +c )}}}
// for是flatmap的语法糖:
val result: Try[Int] = for (
a <- Try("5".toInt);
b <- Try("6a".toInt);
c <- Try("9".toInt)
) yield( a + b + c)

// 1.1含幺半群G:
trait Monad[+T]
// 1.2二元封闭、结合运算:
def flatMap[U]( f : (T) => Monad[U] ) : Monad[U]
// 1.3幺元:
def unit(value : B) : Monad[B]
// 2.1结合律/封闭:
monad.flatMap(f).flatMap(g) == monad.flatMap(v => f(v).flatMap(g)) // associativity
// 案例:
val multiplier : Int => Option[Int] = v => Some(v * v)
val divider : Int => Option[Int] = v => Some(v/2)
val original = Some(10)

original.flatMap(multiplier).flatMap(divider) ===
original.flatMap(v => multiplier(v).flatMap(divider))

// 2.2 左幺元
unit(x).flatMap(f) == f(x)
// 案例:
val multiplier : Int => Option[Int] = v => Some(v * v)
val item = Some(10).flatMap(multiplier)
item === multiplier(10)

// 2.3 右幺元
monad.flatMap(unit) == monad
// 案例:
val value = Some(50).flatMap(v => Some(v))
value === Some(50)

// 3. 范畴
高阶类型(如List[T+])

// 4. 函子(Functor)
// 函数, Int => String
def foo(i:Int): String = i.toString
// 函子, List[T] => Set[T]
scala> def baz[T](l:List[T]): Set[T] = l.toSet

// 5. 自函子(Endofunctor):
把一个类型映射到自身类型,比如Int=>Int, String=>String
例如flatmap:
def flatMap[U]( f : (T) => Monad[U] ) : Monad[U]

下面开始是我个人的理解

函数式语言

函数是一等公民。
无函数副作用。

(学校里教的)
函数可以像普通变量一样使用。(比c里的函数指针更进一步)

更函数式一点

尽量无状态,最好都像lambda演算一样,有很深的递归。
用递归代替循环。

Monad就是这个思想的一个具体实现。

代码层面理解

Monad在scala中就是一个有flatmap的容器,可以把函数fmap的输出收集起来打平回原来的Monad类型。
比较好理解的Monad类型是容器类型:List,Option.

形象上理解

Monad形象上理解类似于有管道操作的容器,可以把函数fmap的输出适配回Monad类型,方便投入下一个函数中。

比较严密的定义上理解:

(去掉范畴学的数学术语,简化理解)
Monad是一个我们定义的集合,它上面有零元(如Option中的None\List中的nil),它上面还有一种二元操作op,op(A,B)的结果依然属于Monad(封闭性),并且运算满足结合律(可以随意加括号)。
所以如果有unit函数(生成零元),flatmap函数(把二元操作打平回集合元素类型,满足封闭性),就可以成为一个Monad了。至于结合律,由于函数都满足结合律,因此可以忽略。

总结:
有map函数: Functor、函子
有ap函数(参数为函数的map): Applicative、应用
有flatmap函数: Monad

disruptor笔记——代替blockingQueue和java9flowAPI

背景

blockingQueue缺点:

  1. 竞态严重: producer\consumer;
  2. cache不友好: 线程a结束后,线程b的所有缓存都污染。

discruptor方案

理想适用场景:
1个producer,多个consumer.

循环队列:
producer: 存储自己的游标(cursor);
consumer: 存储自己的消费offset。

  1. 循环队列: 降低竞态,分离了多个游标;(当然还是有竞态,用内存屏障)
  2. cursor中加入padding: 避免cursor缓存污染;(填充上7个long)
  3. 用CAS和忙等(busy spin)代替锁。(资源换性能)
  4. 预先申请了一大片内存:避免gc干扰。
  • 优化
  1. batch commit; // 避免竞态次数 比如消费者不是每次只读1个,它直接询问生产进度seqK,保存下来,然后一直消费到seqK之前都不用再询问cursor. (询问需要穿过consumer barrier)
  2. 多个producer时: CAS写入;
  3. 支持复杂dag优化。

附:

1
2
3
4
5
6
// padding:
public final static class VolatileLong
{
public volatile long value = 0L;
public long p1, p2, p3, p4, p5, p6; // comment out
}

信息交流

consumer获取producer信息

通过consumer barrier从获取cursor信息: 最新生产sequence number;
这种策略下,消费者不需要知道其他消费者的情况(独立offset)

producer获取信息

通过producer barrier获取ring buffer和消费者信息。
等待最慢的消费者释放空间:获取最慢的消费者的offset。以便获得更多可读的节点空间。(这里也可以批处理,同时获得多个空间,同时写)

两阶段提交:

  1. 数据写入节点;
  2. commit.

复杂dag支持

如上图所示的菱形结构可能在实际业务中会出现。
producer进度: 22
c1进度: 21
c2进度: 18
c3进度: 15
此时producer想要继续写的时候卡住,因为15的位置还不可用。
也就是生产者的速度受制于叶子节点的消费者。

c1,c2的处理结果一般是原地写入entry的不同字段,避免冲突。
(entry节点的定义可以有多个值字段)

更多详情直接参见:
http://wiki.jikexueyuan.com/project/disruptor-getting-started/write-ringbuffer.html

maven依赖:
https://mvnrepository.com/artifact/com.lmax/disruptor/3.4.2

领域驱动设计-第7~17章笔记

第七章 实例:货物运输系统

1. 需求

  1. 跟踪货物的主要处理部署;
  2. 预约货物;
  3. 货物到达某一处理步骤时自动发送发票。

领域语言

货物: cargo
客户: customer
规格: specification
运输动作: carrier movement

  1. 一个cargo(货物)涉及到多个customer(客户),每个customer承担不同角色。
  2. cargo的运送目标已指定;
  3. 由一系列满足specification(规格)的carrier movement(运输动作)来完成运送目标。

上述类图中Customer包括托运人、收货人、快递员等角色。
(都是我们软件要服务的客户)
Handling Event可以细分为不同种类的事件(装货、卸货、提货…)

隔离领域

要把领域层划分出来,首先识别出3个用户层的应用程序功能:

  1. 跟踪查询: 访问某个cargo过去、现在的处理情况;(Delivery History)
  2. 预定: 注册一个cargo;
  3. 事件日志记录: 为1准备信息。
    三个应用层类:
    Tracking Query,Booking Application,Incident Logging Applicatoin。
    这三个应用层类只是协调者,负责向领域层提问。

区分Entity和Value Object

看对象是必须被跟踪的实体还是仅表示一个基本值。

Customer: Entity
Cargo: Entity
Handling EventCarrier Movement: Entity
Delivery History: Entity
Delivery Specification: Value Object: 可替换,货物满足的规则只要等效即可,并不一定需要是某一个id的规则。
Role: Value Object.

设计关联

双向关联往往会产生问题,因此要研究把双向关联转换成单向关联。
好处:

  1. 双向关联=>单向关联:降低出错概率;
  2. 研究遍历方向过程中:让领域更加清晰。

把低频需求的遍历方向交给数据库实现;
留下的单向遍历作为领域层的单向引用。

Aggregate边界

Customer: 根
Location: 根
Carrier Movement: 根
Cargo: 根,它的边界可以囊括所有因它才存在的弱实体: Delivery History,Delivery Specification
(分发历史、分发规则(VO)、处理事件)

Handling Event

  1. 查找某个Delivery History中的Handling Event;
  2. 查找某次Carrier Movement的操作,需要Handling Event的id,需要Handling Event作为根。

创建Repository

作为根的Entity创建repository.
(其他就不创建了,精简类的数量)

如图有7个Entity,5个根。
应对的需求:

  1. 用户选择承担不同角色:发货方、收货人;Customer;
  2. 货物目的地需要Location;
  3. 用户需要查找装货的Carrier Movement;
  4. 用户需要输入系统哪个Cargo完成了装货: Cargo;
  5. Handling Event的需求待定。
    如下图是加上了repository后:

用例

1. 更改目的地

Delivery Specification是个VO,可以直接创建一个新的,替换旧的即可。

2. 重复预订

常常会需要基于旧的cargo作为原型创建新的cargo。
(重复下单同一种商品)

  1. Delivery History: 应创建新、空的;(其他弱实体同)
  2. Customer Roles: 和原来的cargo引用同样的运输角色Customer。
  3. Tracking ID: 新增。

总结三类:

  1. 弱实体、边界内:创建新的、空的。
  2. 边界外:可以引用相同的。
  3. 根:新增(自增id)。

从需求频次出发简化设计

Handling Event相关需求的频次:
创建、新增:高频
查询: 低频

由于查询Delivery History中的Handling Event是低频需求,因此可以考虑不在Delivery History中直接存储Handling Event数组,这样节省了存储开销,也降低了维护一致性的成本。这里创建、新增Handling Event是高频的,因此如果Delivery History中是存储数组,要频繁维护一致性,而且是Agg边界外的改动引起Agg边界内的变动,属于不合理设计。

综上:可将Delivery History中的Handling Event改为即时查询接口,而不是直接存储数组。

优点:使Handling Event的新建变得简单,不会与Cargo Agg发生争用。

换句话说,类似于我们平时设计表字段的时候,高频查询的字段直接放到同一个表里头(可能有时候会反三范式),低频的抽出来扔另一张表(弱实体)里。原文这里是对象级的讨论。

Module:模块化

将紧密关联的实体封装到一个模块。

引入新特性: 配额检查

Allocation Checker: 确保高利润的商品能够运输完,确保大部分商品不会因为运力不足退单毁约。

  • 输入:
  1. 某cargo: 已经预订了多少; (或这个分类已经预订了多少)
  2. 某cargo: 最大预订配额。(或这个分类最大配额)
  • 输出:
    是否能继续预订。

实现1

如小猿的做法: 配额是存储在商品信息里头的。
(参见warehouse)

实现2:

配额是由另一个系统提供的。
同一个商品可以属于不同的类别,影响不同层面的配额。
这样配额相关属性抽离出来变成一类弱实体。

第三部分 通过重构来加深理解

目标:巧妙的领域模型
手段:不断重构,加深对领域的理解

重构

重构的定义是在不改变功能的前提下重新设计它。

重构的层次:

  1. 微重构;(累积成更深层次重构);// 参见《重构》一书
  2. 源于对领域的新认知;
  3. 设计模式重构。

深层模型

浅层模型: 在需求文档中确定名词和动词,初始建模;(不够成熟深入)(只有具体元素)
深层模型: 穿过领域表象,清楚表达领域专家主要关注点以及最相关知识。(恰当的抽象元素和具体元素)

深层模型与柔性设计

(柔性设计详见第10章)
好的深层模型能方便地支持柔性设计。

发现过程

(发现过程、捕捉领域核心概念详见第9章)
第11章: 分析模式
第12章: 设计模式

第八章 突破

如上图所示,重构在某个节点的投入可能会有很大的回报。
(如果突然孵化了对项目的最重要理解,会给项目带来重大冲击)
即使是小的改进也可以防止系统退化。

突破案例

背景:
管理银团贷款的程序。
基本需求:
跟踪支持整个贷款过程。

大致过程

原先的设计绑定了放贷股份和信贷股份,错误的理解。
突然有一天明白了两者基本无关联,重新设计了模型,得到突破,快速迭代。

总结: 好的模型能让无技术背景的业务方也能快速理解。(而不是抱怨专业性太强看不懂)

第九章 深层模型

倾听语言

线索:

  1. 用户长期抱怨、频繁查询的场景,可能是遗漏了重要领域对象。
  2. 领域专家试图纠正你的术语;
  3. 用户感到困惑的名词。
  4. 始终无法形成DSL。(讨论中的术语经常超出DSL范围)

会计示例

主要讲的是学了会计学以后模型更合理,深层。

约束的提炼

约束包括显式规则、隐式规则。
遇到如下情况时,应将隐式规则提炼到显式对象(显式规则):

  1. 计算约束所需数据从定义上不属于这个对象;
  2. 相关规则在多个对象出现,代码重复;
  3. 设计和需求围绕着这些规则,而这些约束却分散在过程代码中。

将规则显式提炼的案例:超订策略

如上图voyage表示实际座席、cargo表示售出货物。
同时引入overbooking policy来显式封装超订的规则约束。

两个矛盾的点:

  1. 不希望过程变成模型的主要部分;
  2. 重要的过程则必须显露在模型中。
    把握这个边界的诀窍:
    这个过程是否经常被领域专家提起,或者仅作为程序机制的一部分。

模式: Specification

一般性的,可以将约束、规则提取出来,作为领域层的Value Object。
用途包括:

  • 选择
  • 过滤
  • 按规格创建/生成对象。

第十章 柔性设计

柔性设计是深层模型的补充。
当我们把隐式概念抽离显式表达出来以后,就有原料来进行柔性设计了。

过多抽象层、间接设计=>过度设计
适当抽象层、间接设计=>柔性设计

简单并不容易做到。
柔性设计需要揭示深层次的底层模型,把它潜在的部分明确展示出来。

具体方法包括如下:

1. 模式:表现意图的接口(Intention-Revealing Interface)

Intention-Revealing Interface: 表现意图的接口
有了它以后能够区分出:
Side-Effect-Freefunction: 无副作用的函数
StandAloneClass: 松耦合对象
Conceptual Contours: 概念边界\概念轮廓
Closeure of Operation: 闭合操作
甚至能基于接口直接编写单元测试中的断言。
(有时候可能无法达到这么理想,需要在单元测试中写Assert来进一步注释)

设计人员的客户包括其他合作开发人员。
接口中包含更多信息时,开发人员可以更有效地使用对象。
(否则就必须深入研究对象的内部机制、理解细节,失去了封装的价值)

接口设计重点:
给出意图、副作用、作用;
但无需给出具体实现细节。

2. 模式:无副作用函数(Size-Effect-free function)

通过区分有无副作用,可以进一步降低查看底层实现的开销。
常见的无副作用操作:查询。
可以通过VO对象把一些操作也转化成无副作用。

一些复杂操作可以进一步分解成:有副作用、无副作用的两个操作。

挖掘深层模型案例:
油漆:

第一步:把接口意图明确(混合两种油漆)

第二步:原来的方法只修改paint1,不改paint2;不符合常识,后继开发人员也无法理解。改成深层模型,原来的paint改为不可变(Stock Paint),单独引入被混合后的油漆(Mixed Paint)。

3. 模式: Assertion

用断言把副作用明确表示出来。

4. 概念轮廓、概念边界

我们应该对每个依赖关系提出质疑,直到证实它确实表示对象的基本概念。
尽量把模块之间的依赖重构为模块内依赖;
模块内依赖重构为尽量少的对象之间的依赖。

5. 低耦合的对象

6. 模式:闭合操作 Closure of Operation

实数集合上进行加减乘除后结果仍在实数集合中,这就是闭合操作。
像刚才油漆的混合操作之后得到的仍然是油漆,这就极大降低了依赖。

7. 声明式设计

声明式语言常见的有sql、各种配置文件。
比如把nginx的配置文件nginx.conf看作一种语言,则它是声明式的。(无法限定过程细节)
声明式设计就是写一段DSL,然后生成一份满足声明的约束条件的代码。比如mybatis里头用工具(jar包)+xml配置生成orm相关java代码。

  • 好处: 避免开发人员去写单调乏味容易出错的代码;
  • 坏处: 生成的代码不灵活,声明可能不足以表达一切。

8. 声明式设计风格

将上述几个模式组合以后,可以使用声明式设计风格。

声明式风格的Specification

1.用逻辑运算组合Specification (闭包操作模式)

运算组合结果还是Specification

如图,可以通过子类的方法实现这种设计。(开销很大)

如图,还可以通过逻辑算法来实现这种设计,这个栈的含义是:
and ( not (armored) , not(ventilated))
这种实现的优点: 对象个数少,内存使用效率高;
这种实现的缺点: 需要更高级的开发人员。

9. 切入问题的角度 (如何优化设计)

1. 分割子领域

2. 尽可能利用已有的形式

从头创建一个严密的概念框架不能作为一项日常工作。
因此我们经常需要对建立已久的概念系统加以修改和利用。

示例: 股份数学

还钱=>钱的分配按放贷股份

首先第一步: 把有无副作用的函数分离;(查归查,改归改)
3个函数:
计算分配方案;
执行(分配方案);
查询余额。

第二步:把隐式概念变成显式概念

显式引入股份份额的概念(share_pie)。
然后把分配方案的计算委托给share_pie,这样简化了loan对象,可以进行复杂的计算。
share_pie可以作为VO(因为计算是无副作用而且通用的)

第三步: 引入闭合操作(运算)

股份的份额运算变成闭合操作,并且由于是VO(不可变),每次返回新的Share pie.

最后把上层调用代码用声明式的风格改写即可。

核心思想: 把复杂计算封装到无状态的VO中。看情况引入闭合逻辑运算,进一步扩充计算能力。

第11章 分析模式的应用

案例: 账户的利息计算

需求:

  1. 计算利息;
  2. 跟踪借款、付款、手续费;
    (两种过账)
    初始类图:

引入复式记账(简化平账的并发问题)

加入每次的交易记录(不可变条目),类似于所有快照都记录。
(Transaction)

进一步挖掘需求

区分“应计项目”(accrual)和实际过账;
应记项目:立即发生;
实际过账:可以延迟。
例如利息可以每天计算,但只在月末过账。(例如夜间批量过账)
新的类图:

注意到图中获取利息和费用的函数都是无副作用的。

进一步考察过账需求

过账的触发时机:

  1. 立即触发: 每次新增交易(Entry被插入)都触发,进行所有更新;
  2. 手动触发:向Account发送命令来触发过账规则;(进行更新)
  3. 基于规则触发:由代理驱动。
    实际实现中可能根据过账的类型来决定触发时机(是否实时到账)。

第12章 将设计模式应用于模型

有些设计模式可以用作领域模式:

Strategy(Policy)模式

模式中有一些可以灵活更换的策略(无状态)。
如路径查找中,可以选择时间最短或者成本最低等等策略。

Composite模式

复杂领域建模时,会遇到多个部分组成的重要对象。(可能继续嵌套)
例如航线可能由多个航段组成。航段可以进一步划分。

原文: 其他可用的设计模式不再一一列举

第13章 通过重构得到更深层的理解

(1)以领域为本;
(2)用不同的方式看待事物;
(3)坚持与领域专家对话。

开始重构

一段复杂或笨拙的代码:
问题的根源在于领域模型中的概念或者关系发生了错误。

另一种例外就是代码很整洁,但是与领域专家的语言不一致,这可能会埋下隐患,因此依然需要重构。

方法:

  • 请教领域专家:寻找灵感;
  • 借鉴已有的经验、案例。
  • 不用完全证明修改的合理性后再修改,应该掌握一个度然后持续重构。
    (类似于物种进化过程中的爆发变化和间断平衡)

第四部分: 战略设计

三个主题:

  1. 上下文:ContextMap,也就是模块化;(Bounded Context)
  2. 精炼: 重点关心项目中最有价值、特殊的方面,其他组件外包;(Core Domain)
  3. 大比例结构: 大分层。(4个左右)(Responsibility Layer)

第14章 上下文: 保持模型的完整性

需要保证模型的内部统一性,不要有模棱两可的意义、规则的冲突。
(举个案例两个团队使用同一个模型出错,最后分开成两个不同场景的模型了)

Bounded context: 限界上下文,定义每个模型的应用范围;
Context Map: 上下文图,给出项目上下文和它们之间关系的总体视图;

Continuous Integration: 持续集成,小项目使用,模型统一;
Shared Kernel: 共享内核;平等团队合作;
Customer/Supplier Teams: 上下游合作,有共同的直接上级;
Conformist: 跟随者,沿用类似内核;(上游写得不错,直接拿过来增强即可)
Open Host Service: 支持多个客户;(与多个外部系统集成时)
Seperate Ways: 团队自由工作(没有共同直接上级);或者上游写得太烂,直接抛弃重写。
Anticorruption Layer: 隔离层,单向转换。(与遗留系统集成时)或者上游写得太烂,一边重构一边用。

Bounded Context

类似于细胞膜一样,缩小模型的命名空间、覆盖范围。
降低成员之间沟通的成本(DSL中术语太多记不住,洪泛了)

一个模型只在一个上下文中使用。

// Bounded Context和Module还是有所区别。一个是逻辑上的,一个是物理上的。

识别不一致:

  1. 场景发生变化后:接口不匹配了。
  2. 重复的概念和假同源: 使用相同的术语,但其实是不同的模型。

Continuous Integration: 持续集成

在一个Bounded Context中的模型应该持续集成,保持一致性。
(小团队、高频交流)

Context Map: 全局视图

ContextMap同时服务于项目管理和软件设计。
甚至要按照它来安排办公室的物理位置。

案例:预订context和运输context

预订Context: 完成Route Specification=>地点代码的转换;
运输Context: 完成Node标识=>行程表、航程安排的转换。
两个上下文之间的接口非常小,可以由Side_Effect_free function构成,由于
同时使用两个上下文,因此可以应用有效的路线安排算法。

其他要点

  1. 确定Context的边界;(每个人都知道)
  2. 每个上下文应当有名字,方便讨论;(加入DSL)

将这两点文档化。

Context Map中一些常见的模式

持续集成模式:
紧密集成产品的优秀团队:大的统一的模型

Shared Kernel(共享内核)/Customer-supplier(客户供应商):

  • 团队协调能力有限;
  • 为不同的用户群提供服务;

Separate Way(独立自主)模式:

  • 集成并不重要时;

Open Host Service(开放主机服务)/Anticorruption Layer(防护层):

  • 与遗留系统或外部系统进行一定程度集成时。

Shared Kernel (共享内核)

持续集成是开销最大的,开销稍微小一点的是共享内核。
(仅持续集成内核部分)

从领域模型中选出两个团队都同意共享的一个子集。
一个团队在没与另一个团队商量之前不应擅自更改它。

测试
需要自动测试套件

可以每周进行一次内核的合并。

Shared Kernel通常是Core Domain(参见精炼部分,Core Domain就是精炼出来的项目需要解决的最核心逻辑),或者一组Generic Subdomain(通用子领域)。

Customer/Supplier Development Team(客户/供应商模式)

适用情况:

  1. 一个子系统服务于另一个子系统;
  2. 下游很少向上游反馈信息,单向依赖;
  3. 两个子系统为完全不同的用户群服务。

上下游很自然得分割到两个Bounded Context中。

注意事项:

  • 上下游负责的两个团队的行政关系:最好有共同的直接上级;
    原因:需要正式规定团队之间的关系、责任。
    两者有工作依赖关系,相互制约,如果无法互相推动可能导致交付delay。

测试
两个团队一起开发自动验收测试,验证预期的接口。
降低耦合性。上游团队做出修改时不必担心对下游团队产生副作用。
(接力赛时前面的选手不能一直回头看,他需要相信队友能把棒准确交到他手中,否则整个团队的速度都会慢下来)

Conformist(跟随者模式)

适用情况:
依然是上下游关系,但没有共同直接上级。
(管理层次相隔很远,无法推动)

此时下游团队只能靠自己了,3种选择:

  1. 放弃对上游的利用: Separete Way(独立自主模式)
  2. Anticorruption Layer: (防护层模式)上游写得很烂,一边重构一边用;
  3. Conformist: (跟随者模式)上游写得不错,拿过来进行增强即可。

Conformist与Shared Kernel类似都是用了相同内核,但是Conformist中另一个团队对合作没有兴趣。

Anticorruption Layer(防护层模式)

适用情况:
遗留代码写得烂,或上游写得不行但重写代价太高时,只能一边写一边重构。

一般重构不要直接全盘否定,这样工作量太大不可能立即完成。

实现

Facade: 外观模式: 子系统可供替换的接口,方便切换新老实现;
Adapter: 适配器:把新老系统转换成相同的接口。

个人思考:
微服务级别的隔离,部分请求发给新服务(一切都要灰度测试)

Separate Way(独立自主模式)

如果集成的收益很小,代价很高,可以考虑不集成。

Open Host Service(开放主机服务)

适用情况:
需要和大量其他系统集成时。

定义一个协议,使我们的系统可以作为一组Service供其它系统访问。
开放这个协议,让所有需要与我们系统集成的人都可以使用它。
当有新的集成需求时,增强并扩展这个协议。
(如果只是特殊需求,可以写个一次性的转换器,共享协议应该简单而且内聚)

Published Language(公共语言)

两个Bounded Context之间模型转换的时候,
交换信息时如果有共同语言(无歧义)能简化转换。
如Open Host Service模式中,如果能发明一种简单好理解的共享协议,别的系统就能快速接入。

公共语言可能是: XML,JSON…

模型的集成统一

集成的过程中往往会出现互相冲突的领域模型。
因此要适当简化,宁可缺少喷水功能,也不要包含不正确的特性。

Context的边界选择:

大的Context适用情况:

  1. 用一个统一的模型来处理时,用户任务之间流动更顺畅;
  2. 一个内聚模型比两个更容易理解;
  3. 两个模型转换很难;
  4. 共享语言可以使团队沟通起来更清楚。

小的Context适用情况:

  1. 降低了开发之间的沟通开销;
  2. 降低规模后:持续集成更容易了;
  3. 太大的上下文需要更高级的抽象模型:相关技巧人员短缺;
  4. 不同模型满足一些特殊需求。

集成外部系统的经验

  1. 首先考虑不集成:Seperate Way模式;
  2. 外部系统写得好:Conformist模式:
  3. 外部系统写得烂:Anticorruption Layer。

第15章 精炼 (Core Domain)

领域驱动的核心是把领域层提取出来;
还可以进一步把领域层中最核心要解决的问题(项目的立项根因)提取出来:
Core Domain。

核心思想:专注于核心问题,而不被大量次要问题所淹没。

精炼包括:

  1. 帮助成员掌握系统的总体设计及协调;
  2. 找到一个适度规模的核心模型,加入到通用语言,促进沟通;
  3. 指导重构;
  4. 专注于模型中最有价值的部分;
  5. 指导外包、现成组件的使用以及任务委派。

Core Domain模式

尽量压缩Core Domain,在Core Domain中努力开发深层模型和柔性设计。
(让最有才能的人来开发Core Domain,自主开发的软件的最大价值在于对Core Domain的完全控制。应该让最有才能的人+领域专家长期合作开发)

Domain Vision Statement: 领域前景说明
Highlighted Core : 突出的核心
Generic Subdomain: 通用子领域:模型中最普通不特别的部分;
Cohesive Mechanism: 内聚机制
Seperated Core: 隔离的核心:核心外的实现可替换
Abstract Core: 抽象内核:连核心的实现也是可替换的。

Generic Subdomain

通用子领域:与项目目标无直接联系,增加复杂性,不限于仅在本项目可以使用。(如数据库连接池这种纯技术的部分、带时区的日期和时间功能)

它们的解决方案:

  1. 购买现成的;
  2. 使用开源的;
  3. 把实现外包出去;
  4. 内部实现它。

Domain Vision Statement领域前景说明(1页)

不涉及技术指标,但要把项目和其他项目区分开来。
描述支持的功能和目标。
(区别于某个版本的技术规格)

Highlighted Core(3~7页)

在代码级完成Core Domain前,可以先文档级描述Core Domain。
描述Core Domain及内部元素的主要交互。
尽量精简。

Cohesive Mechanism(内聚机制)

分离出去的代码要内聚,用Intention-revealing接口来公开功能。
从而留下更小的Core Domain。
(例如可以分离Specification对象(规格))

Generic Subdomain与Cohesize Mechanism都是为Core domain减负。

Segregated Core(分离内核)

等到上述步骤完成,内核逐渐与其他部分分离开。
进一步重构,彻底去掉代码耦合,把内核分离出来。

Abstract Core(抽象内核)

把模型中最基本的概念识别出来,分离到不同的类、抽象类、接口中。
详细的实现留在子领域定义的module中。

综上
重构时也应当优先重构Core Domain。

第16章 大比例结构

前文:上下文(Bounded Context),精炼(Core Domain/Generic Subdomain)
分离出很多Module后,要找一个类非常困难。
这个时候为了便于管理: 大比例结构(约4层)

Evolving Order: 逐步进化演化。
System Metaphor: 隐喻思维;(用一些比喻、如防火墙)
Responsibility layer: 职责模式
Knowlege level: 知识级别模式;
Plugggable Component Framework: 解耦组件;

System Metaphor模式

隐喻模式。
例如核和外层的比喻,防火墙的比喻。
用比喻来分层。(但是宁缺毋滥)

Responsibility Layer职责分层模式

类似于MVC中Repository等,上层可以访问下层,下层则不能访问上层。
还可以根据访问频率、状态变化频率分层。

运输系统/投资银行案例

作业层
能力层
决策支持层
潜能层
承诺层

(类似于Controller,logic,repository层等等)

Knowledge level知识级别模式

与Reponsibily layer的区别在于两个层之间互相依赖。
案例用的是养老金分配的知识级别模式。
某些模型能根据元数据来工作,知识级别较高。

Pluggable Component Framework

一个中央hub上支持所需的协议,可以灵活替换组件。
这种模式一般是经过很长时间演变后产生。
(起码在Abstract Core之后)

最小化

一开始使用最松散的System metaphor隐喻模式,逐渐深化。

第17章 领域驱动设计的综合运用

综合使用前面的三点:
上下文、精炼、大比例结构。

大比例结合上下文(bounded context)

把不同bounded context放到不同层

大比例结合精炼

帮助理清Core Domain内部关系和Generic subdomain之间关系。
(放到不同层)

大比例结合上下文后的图如上所示。

战略设计决策的6个要点

  1. 决策传达到整个团队;
  2. 决策过程收集反馈;
  3. 计划允许演变;
  4. 架构团队和开发团队都需要聪明人;
  5. 简约、谦逊原则;(不要对开发形成障碍)
  6. 对象职责专一而开发人员是多面手。

hll算法原理

What: HLL/HyperLogLog是啥

近似计算uv的算法,每一千万错误率0.5%.
谷歌改进后的算法为HLL++/HyperLogLog plus算法,改进了一些边界和精度问题(分类处理了稀疏和稠密的数据集情况,稀疏转化成稠密)。HLL++在边界条件下从1.5%优化到0.5%。而且不会出现突变高的错误率情况。

论文:
http://static.googleusercontent.com/media/research.google.com/en/us/pubs/archive/40671.pdf

How: HLL原理

n次伯努利

进行了n次进行抛硬币实验,每次分别记录下第一次抛到正面的抛掷次数K1~Kn,那么可以用n次实验中最大的抛掷次数Kmax;
则可以预估实验组数量为n的预估值=2^Kmax.
参考:
http://www.rainybowe.com/blog/2017/07/13/%E7%A5%9E%E5%A5%87%E7%9A%84HyperLogLog%E7%AE%97%E6%B3%95/index.html

LC算法

所有数据hash以后,从低位开始第一个1的位置K。
预估值为2^K.

HLL算法

分桶后求调和平均,概率上减少异常。

redis中的实现

代码:https://github.com/antirez/redis/blob/unstable/src/hyperloglog.c
内存: 2^14个桶,每个桶6bit。(实际作为一个大数组12KB。)

  1. 每个输入通过hash算法得出64bit哈希值x;
  2. x的低14位,用来选择桶号(0-2^14-1号)Mi;
  3. x的高50位,用来找K(也就是第一次出现1的位置,或者说0后缀的长度),把K存入Mi。

这样处理完所有用户输入后,用公式算出n的估计值:

对于第三点中的K,(也就是n次伯努利里的Kmax) (对于每个用户id的Kmax值存入桶的6位中)
高位剩下50位,第一个1的位置最大是50,而2^6=64,所以能够存下50这个数字(以及其他所有Kmax)。

HLL++的话还要加入更多的边界调整。

可视化模拟

http://content.research.neustar.biz/blog/hll.html
上述链接中是m=64个桶(4*16的方阵),每个格子中存放一个十进制数,实际最大是2^6也就是64,如果新来了K值,则会和原来的K值做逻辑交运算。