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

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


  • 首页

  • 归档

  • 搜索

解释器模式

发表于 2021-11-26

这是我参与11月更文挑战的第14天,活动详情查看:2021最后一次更文挑战

概述

如上图,设计一个软件用来进行加减计算。我们第一想法就是使用工具类,提供对应的加法和减法的工具方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码//用于两个整数相加
public static int add(int a,int b){
return a + b;
}

//用于两个整数相加
public static int add(int a,int b,int c){
return a + b + c;
}

//用于n个整数相加
public static int add(Integer ... arr) {
int sum = 0;
for (Integer i : arr) {
sum += i;
}
return sum;
}

上面的形式比较单一、有限,如果形式变化非常多,这就不符合要求,因为加法和减法运算,两个运算符与数值可以有无限种组合方式。比如 1+2+3+4+5、1+2+3-4等等。

显然,现在需要一种翻译识别机器,能够解析由数字以及 + - 符号构成的合法的运算序列。如果把运算符和数字都看作节点的话,能够逐个节点的进行读取解析运算,这就是解释器模式的思维。

定义:

给定一个语言,定义它的文法表示,并定义一个解释器,这个解释器使用该标识来解释语言中的句子。

在解释器模式中,我们需要将待解决的问题,提取出规则,抽象为一种“语言”。比如加减法运算,规则为:由数值和+-符号组成的合法序列,“1+3-2” 就是这种语言的句子。

解释器就是要解析出来语句的含义。但是如何描述规则呢?

文法(语法)规则:

文法是用于描述语言的语法结构的形式规则。

1
2
3
4
r复制代码expression ::= value | plus | minus
plus ::= expression ‘+’ expression
minus ::= expression ‘-’ expression
value ::= integer

注意: 这里的符号“::=”表示“定义为”的意思,竖线 | 表示或,左右的其中一个,引号内为字符本身,引号外为语法。

上面规则描述为 :

表达式可以是一个值,也可以是plus或者minus运算,而plus和minus又是由表达式结合运算符构成,值的类型为整型数。

抽象语法树:

在计算机科学中,抽象语法树(AbstractSyntaxTree,AST),或简称语法树(Syntax tree),是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。

用树形来表示符合文法规则的句子。

结构

解释器模式包含以下主要角色。

  • 抽象表达式(Abstract Expression)角色:定义解释器的接口,约定解释器的解释操作,主要包含解释方法 interpret()。
  • 终结符表达式(Terminal Expression)角色:是抽象表达式的子类,用来实现文法中与终结符相关的操作,文法中的每一个终结符都有一个具体终结表达式与之相对应。
  • 非终结符表达式(Nonterminal Expression)角色:也是抽象表达式的子类,用来实现文法中与非终结符相关的操作,文法中的每条规则都对应于一个非终结符表达式。
  • 环境(Context)角色:通常包含各个解释器需要的数据或是公共的功能,一般用来传递被所有解释器共享的数据,后面的解释器可以从这里获取这些值。
  • 客户端(Client):主要任务是将需要分析的句子或表达式转换成使用解释器对象描述的抽象语法树,然后调用解释器的解释方法,当然也可以通过环境角色间接访问解释器的解释方法。

案例实现

【例】设计实现加减法的软件

代码如下:

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
java复制代码//抽象角色AbstractExpression
public abstract class AbstractExpression {
public abstract int interpret(Context context);
}

//终结符表达式角色
public class Value extends AbstractExpression {
private int value;

public Value(int value) {
this.value = value;
}

@Override
public int interpret(Context context) {
return value;
}

@Override
public String toString() {
return new Integer(value).toString();
}
}

//非终结符表达式角色 加法表达式
public class Plus extends AbstractExpression {
private AbstractExpression left;
private AbstractExpression right;

public Plus(AbstractExpression left, AbstractExpression right) {
this.left = left;
this.right = right;
}

@Override
public int interpret(Context context) {
return left.interpret(context) + right.interpret(context);
}

@Override
public String toString() {
return "(" + left.toString() + " + " + right.toString() + ")";
}
}

///非终结符表达式角色 减法表达式
public class Minus extends AbstractExpression {
private AbstractExpression left;
private AbstractExpression right;

public Minus(AbstractExpression left, AbstractExpression right) {
this.left = left;
this.right = right;
}

@Override
public int interpret(Context context) {
return left.interpret(context) - right.interpret(context);
}

@Override
public String toString() {
return "(" + left.toString() + " - " + right.toString() + ")";
}
}

//终结符表达式角色 变量表达式
public class Variable extends AbstractExpression {
private String name;

public Variable(String name) {
this.name = name;
}

@Override
public int interpret(Context ctx) {
return ctx.getValue(this);
}

@Override
public String toString() {
return name;
}
}

//环境类
public class Context {
private Map<Variable, Integer> map = new HashMap<Variable, Integer>();

public void assign(Variable var, Integer value) {
map.put(var, value);
}

public int getValue(Variable var) {
Integer value = map.get(var);
return value;
}
}

//测试类
public class Client {
public static void main(String[] args) {
Context context = new Context();

Variable a = new Variable("a");
Variable b = new Variable("b");
Variable c = new Variable("c");
Variable d = new Variable("d");
Variable e = new Variable("e");
//Value v = new Value(1);

context.assign(a, 1);
context.assign(b, 2);
context.assign(c, 3);
context.assign(d, 4);
context.assign(e, 5);

AbstractExpression expression = new Minus(new Plus(new Plus(new Plus(a, b), c), d), e);

System.out.println(expression + "= " + expression.interpret(context));
}
}

优缺点

1,优点:

  • 易于改变和扩展文法。

由于在解释器模式中使用类来表示语言的文法规则,因此可以通过继承等机制来改变或扩展文法。每一条文法规则都可以表示为一个类,因此可以方便地实现一个简单的语言。

  • 实现文法较为容易。

在抽象语法树中每一个表达式节点类的实现方式都是相似的,这些类的代码编写都不会特别复杂。

  • 增加新的解释表达式较为方便。

如果用户需要增加新的解释表达式只需要对应增加一个新的终结符表达式或非终结符表达式类,原有表达式类代码无须修改,符合 “开闭原则”。

2,缺点:

  • 对于复杂文法难以维护。

在解释器模式中,每一条规则至少需要定义一个类,因此如果一个语言包含太多文法规则,类的个数将会急剧增加,导致系统难以管理和维护。

  • 执行效率较低。

由于在解释器模式中使用了大量的循环和递归调用,因此在解释较为复杂的句子时其速度很慢,而且代码的调试过程也比较麻烦。

使用场景

  • 当语言的文法较为简单,且执行效率不是关键问题时。
  • 当问题重复出现,且可以用一种简单的语言来进行表达时。
  • 当一个语言需要解释执行,并且语言中的句子可以表示为一个抽象语法树的时候。

本文转载自: 掘金

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

Jeecg如何实现数据权限/隔离?用户上下文变量没有user

发表于 2021-11-26

前言:

在使用如若依、Jeecg等后台管理系统进行二次开发的时候,我们总会涉及到数据隔离相关的内容,如每个非管理员用户应该都只能看到自己创建的数据,而不是所有的数据,本文将以jeecg为例像大家介绍如何每个用户之间的数据隔离的效果。

先从功能需求说起,当查询房屋列表的时候,每个房东(用户)应该只能看到自己的房屋(house)表数据,而房屋和房东的关系存放于房东(house_holder)表中,所以我们需要通过两个表的关联来实现数据隔离的查询。而jeecg中要如何实现呢?我们一起啦看看吧

基于Jeecg实现:

先放出官方文档中跟数据隔离有关的内容:doc.jeecg.com/2044046

  1. 进入菜单管理页面,找到想要进行数据隔离的菜单,点击添加一个下级并进行配置

image.png
选择按钮/权限并配置菜单路径(笔者能力有限暂时无法弄清菜单路径和授权表示的作用在哪)
image.png
然后对添加的这个按钮/权限进行数据规则的配置,
由于我们需要要从另外一张表中去查出跟user_id有关的house_id所以要使用自定义SQL的方式

image.png

  1. 在角色管理中对用角色进行数据权限的授权
    给需求中的房东用户配置上数据权限

image.png

  1. 在后端对应的controller中的list方法中加上@PermissionData注解,该注解有一个pageComponent参数为数据隔离的菜单的前端组件路径,如果配置了只有该前端组件路径会被拦截,不配置的话就拦截全部请求。

大家可能注意到了,官方文档中给出了系统上下文变量有sys_user_code, 有sys_user_name,有sys_org_code但就是没有SQL表达式中的#{sys_user_id},而我们想要实现的效果只能给予user_id来实现,那么我们要怎么实现呢?

修改底层源码

笔者先给出如何修改,实现将当前登录用户的id存入数据权限的系统上下文变量中,来探讨其源码是如何进行修改的。

以下文件直接通过idea的search every where 的功能即可找到对应类

3.1. SysUserCacheInfo中添加一个String类型的sysUserId和其对应的setter/getter

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码...
private String sysUserId;

// 用idea生成setter/getter,如果懒的话直接用@Data然后删除其他的setter/getter也成
public String getSysUserId() {
return sysUserId;
}

public void setSysUserId(String sysUserId) {
this.sysUserId = sysUserId;
}
...

3.2. SysBaseApiImpl中的getCachUser为3.1的CacheInfo的新增的属性赋值

1
2
3
4
5
6
7
8
9
10
11
java复制代码public SysUserCacheInfo getCacheUser(String username) {
SysUserCacheInfo info = new SysUserCacheInfo();
info.setOneDepart(true);
LoginUser user = this.getUserByName(username);
if(user!=null) {
//加上下面一行代码
info.setSysUserId(user.getId());
info.setSysUserCode(user.getUsername());
...
}
...

3.3. DataBaseConstant中添加数据权限的系统上下文的key值

1
2
3
4
5
6
7
8
9
java复制代码...
public static final String SYS_USER_CODE_TABLE = "sys_user_code";
/**
* 添加下面这一行代码
*/
String SYS_USER_ID = "sys_user_id";

public static final String SYS_USER_NAME = "sysUserName";
...

3.4. 在JwtUtil中的getUserSystemData放将sys_user对应的value设置为当前用户的id

1
2
3
4
5
6
7
8
9
10
11
java复制代码...
if(key.equlas(...){

}else if (key.equals(DataBaseConstant.TENANT_ID) || key.toLowerCase().equals(DataBaseConstant.TENANT_ID_TABLE)){
...
}
// 加上这一个else if 的判断就完成啦
}else if (key.equals(DataBaseConstant.SYS_USER_ID)){
returnValue = sysUser.getId();
}
...

基于上面的4个步骤的小改动,我们就能够成功的为jeecg的数据权限添加上当前用户id值了。然后进行测试应该就能发现我们成功进行了基于user_id的多表关联的数据隔离啦。

底层深探

通过对@PermissionData注解的跳转,我们能看到其对应着一个名为PermissionDataAspect的切面

image.png

在切面中查询出了对应用户的数据权限模型(PermissionDataRuleModel),并且于当前用户的userInfo都放入了request请求中。

image.png
数据权限模型的值如下,可以看到,其值就是我们刚刚在后台中配置的数据规则

image.png

而JeecgDataAuthorUtils.installUserInfo的代码如下:

image.png

其将userinfo存放在了request当中(setter),但该切面到此处就结束了。 我们再看看整个进入Controller的list方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码/**
* 分页列表查询
*
* @param house
* @param pageNo
* @param pageSize
* @param req
* @return
*/
@AutoLog("房产主表-分页列表查询")
@ApiOperation(value="房产主表-分页列表查询", notes="房产主表-分页列表查询")
@GetMapping("/list")
@PermissionData
public IPage<House> queryPageList(House house,
@RequestParam(name="pageNo", defaultValue="1") Integer pageNo,
@RequestParam(name="pageSize", defaultValue="10") Integer pageSize,
HttpServletRequest req) {
QueryWrapper<House> queryWrapper = QueryGenerator.initQueryWrapper(house, req.getParameterMap());
Page<House> page = new Page<House>(pageNo, pageSize);
IPage<House> pageList = houseService.page(page, queryWrapper);
return pageList;
}

有一个QueryGenerator.initQueryWrapper其传入了Request的ParameterMap,这个parameterMap中包含了在切面是存入的userInfo和datarulemodel,所以数据的隔离会不会是在这里实现的呢? 我们在进去看看,

image.png

再进入这里installMplus方法:

image.png

看到这,我们应该就找到了jeecg中居于mybatis plus实现的数据权限/隔离的代码所在地啦,但这个类十分的冗余判断,难以阅读,所以作者给出几个该类中和数据权限有关的代码:

installMplus方法中通过我们之前在切面中传入的SysPermissionDataRuleModel开始解析我们自定义的SQL语句中的模板变量。

image.png
这个getSqlRuleValue方法就是模板替换的核心啦

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码public static String getSqlRuleValue(String sqlRule){
try {
//获取sql语句中的模板变量:实际就是通过正则匹配sqlRule中的${}字符
Set<String> varParams = getSqlRuleParams(sqlRule);
for(String var:varParams){
// 根据模板变量的key值来返回具有实际意义的value:通过JwtUtil的getSystemData方法来返回value
String tempValue = converRuleValue(var);
// 替换模板变量为真正的value
sqlRule = sqlRule.replace("#{"+var+"}",tempValue);
}
} catch (Exception e) {
log.error(e.getMessage(), e);
}
return sqlRule;
}

public static String converRuleValue(String ruleValue) {
String value = JwtUtil.getUserSystemData(ruleValue,null);
return value!= null ? value : ruleValue;
}

getSqlRuleValue方法的debug变量如下
image.png

JwtUtil中的getSystemData的代码如下:其实就是一大堆的if-else + contstant常量罢了

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
java复制代码/**
* 从当前用户中获取变量
* @param key
* @param user
* @return
*/
public static String getUserSystemData(String key,SysUserCacheInfo user) {
if(user==null) {
user = JeecgDataAutorUtils.loadUserInfo();
}
// 获取登录用户信息
LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();

...一些正则匹配

//替换为系统登录用户帐号
if (key.equals(DataBaseConstant.SYS_USER_CODE)|| key.toLowerCase().equals(DataBaseConstant.SYS_USER_CODE_TABLE)) {

}
... 一些对于DataBaseContants中的KEY的else-if判断

}else if (key.equals(DataBaseConstant.SYS_USER_ID)){
returnValue = sysUser.getId();
}

return returnValue;
}

总结

数据隔离实际上就是对SQL进行拼接,如果只是对每个权限挨个写一个SQL的话,谁都会。其难点其实主要在于如何在不污染业务代码的情况下完成SQL的拼接,Jeecg通过切面 + MyBatisPlus的QueryWrapper进行实现了这样的效果。

本文转载自: 掘金

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

十大经典排序之:基数排序 |计数排序 基数排序 计数排序

发表于 2021-11-26

「这是我参与11月更文挑战的第26天,活动详情查看:2021最后一次更文挑战」

基数排序

基数排序原理

今天的排序算法可能比之前的稍微难点。基数排序是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。属于“分配式排序”,又称“桶子法”或bin sort。最早用于解决卡片排序的问题。

基数排序是可以应对字符的,针对于字符串的基数排序就诞生了,它是在计数排序的基础上进行了改进,在某些时候,基数排序法的效率高于其它的稳定性排序法。基数排序从最低为开始来排序的,从低位到高位,按位排序,按位排序必须是稳定的。

基本思想:对于每个元素x,如果我们知道了小于x的元素的个数,就可以确定输出数组中元素x的位置,那么直接将元素x放到输出数组中。比如有3小于x的元素,那在输出数组中,x肯定位于第4个位置。

算法实现

1、算法描述

  1. 将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零。
  2. 从最低位开始,依次进行一次排序。
  3. 这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列。

2、图示
在这里插入图片描述

3、算法空间复杂度和时间复杂度

时间复杂度:

  • 最坏:o(d(r+n)d(r+n)d(r+n))
  • 最好:o(d(rd+n)d(rd+n)d(rd+n))
  • 平均:o(d(r+n)d(r+n)d(r+n))

空间复杂度(辅助存储):o(rd+nrd+nrd+n)

稳定性:稳定

ps:r:关键字基数 d:长度 n:关键字个数

例题

用基数排序将以下数列按照从小到大的顺序输出:123,45,6,22,99,1,38,41,7,0

java代码:

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
java复制代码import java.util.*;
public class Test {
//pos=1表示个位,pos=2表示十位
public static int getNumInPos(int num, int pos) {
int tmp = 1;
for (int i = 0; i < pos - 1; i++) {
tmp *= 10;
}
return (num / tmp) % 10;
}

//求得最大位数d
public static int getMaxW(int[] arr) {
int max = arr[0];
for (int i = 0; i < arr.length; i++) {
if (arr[i] > max)
max = arr[i];
}
int tmp = 1, d = 1;
while (true) {
tmp *= 10;
if (max / tmp != 0) {
d++;
} else
break;
}
return d;
}
public static void radixSort(int[] arr, int d) {
int[][] array = new int[10][arr.length + 1];
for (int i = 0; i < 10; i++) {
array[i][0] = 0;
// array[i][0]记录第i行数据的个数
}
for (int pos = 1; pos <= d; pos++) {
for (int i = 0; i < arr.length; i++) {
// 分配过程
int row = getNumInPos(arr[i], pos);
int col = ++array[row][0];
array[row][col] = arr[i];
}
for (int row = 0, i = 0; row < 10; row++) {
// 收集过程
for (int col = 1; col <= array[row][0]; col++) {
arr[i++] = array[row][col];
}
array[row][0] = 0;
// 复位,下一个pos时还需使用
}
}
}

public static void main(String[] args) {
int[] arr=new int[]{123,45,6,22,99,1,38,41,7,0};
//基数排序
radixSort(arr,getMaxW(arr));

System.out.println("基数排序后的结果是:");
System.out.println(Arrays.toString(arr));
}

}

计数排序

计数排序原理

计数排序是一个非基于比较的排序算法,它的优势在于在对一定范围内的整数排序时,快于任何比较排序算法。其核心在于将输入的数据值转化为键存储在额外开辟的数组空间中,作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。

简单来说,就是通过数组下标来确定正确的位置,并在数组中记录出现的次数,最后得到有序数据。

核心思想:统计每个整数在序列中出现的次数,进而推导出每个整数在有序序列中的索引

算法实现

1、算法描述

  1. 找出待排序的数组中最大和最小的元素
  2. 统计数组中每个值为i的元素出现的次数,存入数组C 的第i项
  3. 对所有的计数累加 (从C 中的第一个元素开始,每一项和前一项相加)
  4. 反向填充目标数组:将每个元素i放在新数组的第C[i]项,每放一个元素就将C[i]减去1

2、图示
在这里插入图片描述

3、算法空间复杂度和时间复杂度

时间复杂度:

  • 最坏:o(n+kn+kn+k)
  • 最好:o(n+kn+kn+k)
  • 平均:o(n+kn+kn+k)

空间复杂度(辅助存储):o(n+kn+kn+k)

稳定性:稳定

例题

用计数排序将以下数列按照从小到大的顺序输出:
66,13,51,76,81,26,57,69,23

java代码:

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复制代码import java.util.*;
public class Test {
public static void countSort(int[] arr) {
int min = arr[0];
int max = arr[0];

for (int i = 1; i < arr.length; i++) {
if (min > arr[i]) min = arr[i];
if (max < arr[i]) max = arr[i];
}
// 构建一个新的数组,把原数组中数据的值当作下标存入,下标从0开始;因为max要放入对应下标中,所以要+1
int[] bucketArr = new int[max + 1];
for (int k = 0; k < arr.length; k++) {
//数字每出现一次,就在原基础上+1
bucketArr[arr[k]] = bucketArr[arr[k]] + 1;
}

int finalIndex = 0;
for (int n = min; n < bucketArr.length; n++) {

if (bucketArr[n] > 0) {
for (int l = 0; l < bucketArr[n]; l++) {
arr[finalIndex++] = n;
}
}
}
}


public static void main(String[] args) {
int[] arr=new int[]{66,13,51,76,81,26,57,69,23};
//记数排序
countSort(arr);

System.out.println("记数排序后的结果是:");
System.out.println(Arrays.toString(arr));
}

}

本文转载自: 掘金

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

微服务小记:初识Eureka

发表于 2021-11-26
  1. Eureka是什么

Eureka作为Spring Cloud Netflix子项目的注册中心,提供了服务的注册与发现功能。在微服务的项目中,每个服务分布在不同的机器中,而Eureka就用于提供一个平台让所有服务注册进来,并在请求其他服务时从Eureka注册列表中获取地址。

  1. Eureka的原理

Eureka框架采用的CS架构,通过一个Server、多个Client来互通并提供功能服务。

Eureka Server作为服务注册的服务端,是服务注册和发现功能的中心,用来管理Eureka Client的注册信息。Eureka Server自身也会作为一个Client注册进来,所有的Eureka Client可以通过Server中的注册列表发现其他的服务。

Eureka Client即是一个Java程序客户端,也就是一个独立运行的服务。Eureka为了Server中管理Client的方便,会对每个Eureka Client进行周期性的心跳检查,保证列表中注册服务的可用性。Eureka Client中为了应对服务之间的请求交互,使用了基于轮询的负载均衡器。

  1. Eureka的特点

作为中心节点的Eureka Server,不仅需要定时检查其他Eureka Client的监控状态,还要即使判断自身服务的可行性。如果在15分钟内超过85%的注册节点的心跳出现故障,那么就认为Eureka Server与其他Eurka Client之间的连接出现故障。出现故障时,Eureka会做出如下反应:

  • Eureka认为此时可能是网络故障导致,因此不再将Client从Server中移除
  • Eureka此时仍然可以接收新服务的注册和查询请求,但是不会与其他节点做同步处理,以保证当前节点可用
  • 网络恢复稳定后,这段时间的信息会进行同步处理,不会影响服务的使用
  1. Eureka的使用

Eureka作为服务注册和发现中心,分为提供注册和发现功能的服务端和进行注册和被发现的客户端。

4.1 创建服务端EurekaServer

在创建项目时,SpringBoot中已经为我们提供好了相关的启动器信息,创建一个EurekaServer端需要在项目中引入Spring Cloud Discovery下的Eureka Server依赖,以及提供服务正常运行的Spring Web依赖。

image.png

此时服务端根据启动器引入的依赖信息为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
xml复制代码 <!-- eureka server依赖信息 -->
 <dependency>
     <groupId>org.springframework.cloud</groupId>
     <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
 </dependency>
 ​
 <!-- 使用spring cloud时的版本管理,没有时eureka依赖下载失败 -->
 <dependencyManagement>
     <dependencies>
         <dependency>
             <groupId>org.springframework.cloud</groupId>
             <artifactId>spring-cloud-dependencies</artifactId>
             <version>${spring-cloud.version}</version>
             <type>pom</type>
             <scope>import</scope>
         </dependency>
     </dependencies>
 </dependencyManagement>

项目创建完成后,为了能够保证服务的正常运行,还需要对项目进行配置。作为Eureka Server服务,首先要在启动类上使用@EnableEurekaServer注解标注当前项目作为服务端,除此之外,还要在配置文件中定义服务端的配置信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
properties复制代码# 服务信息
spring.application.name=eureka-server
server.port=8081

# 服务端也作为一个客户端存在,此处关闭其客户端的注册功能
eureka.client.register-with-eureka=false
eureka.client.fetch-registry=false
# 定义服务端保护机制
eureka.server.enable-self-preservation=true
eureka.server.renewal-percent-threshold=0.5

# 定义注册中心路径
eureka.client.service-url.default-zone=http://${eureka.instance.hostname}:${server.port}/eureka

配置完成后,Eureka Server项目就可以正常的启动了,启动成功后控制台输出Eureka启动的信息。

image.png

为了更好的管理Eureka Client的注册信息,Eureka Server还提供了一个可视化页面来观察当前注册的服务信息。服务启动后可以访问httP://hostname:{hostname}:hostname:{port} ,其中会显示当前注册在Eureka Server服务中的客户端列表信息。

image.png

4.2 创建客户端 Eureka Client

创建客户端在流程上和服务端基本是相同的,不过在选择启动器时使用Spring Web和Spring Cloud Discovery Client两个依赖信息。

image.png

项目创建后得到的maven依赖信息和Eureka Server服务的大体相同,也是将Server换成了client。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
xml复制代码<!--eureka客户端依赖信息 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

Eureka Client客户端启动时需要在启动类上标注@EnableEurekaClient注解,同时也要在配置文件中配置上服务本身信息和远程访问注册中心的地址信息。

1
2
3
4
5
6
7
8
9
10
properties复制代码#服务信息
server.port=8091
spring.application.name=producer

#注册中心路径
eureka.client.serviceUrl.defaultZone=http://localhost:8081/eureka

#心跳间隔检测配置
eureka.instance.lease-renewal-interval-in-seconds=5
eureka.instance.lease-expiration-duration-in-seconds=10

配置完成后成功启动项目,就可以看到Client通过配置的路径找到注册中心并将自己注册进去。

image.png

此时再次打开Eureka Servel的管理页面,可以看到客户端已经注册进去了。

image.png

本文转载自: 掘金

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

Spring的Import注解四种使用方式

发表于 2021-11-26

来介绍@Import注解的使用之前,我们先看源码:

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Import {

/**
* {@link Configuration @Configuration}, {@link ImportSelector},
* {@link ImportBeanDefinitionRegistrar}, or regular component classes to import.
*/
Class<?>[] value();

}

从注释中,我们可以看到@Import注解可以将@Configuration、ImportSelector、ImportBeanDefinitionRegistrar和普通的component类导入。

准备Spring环境

Spring依赖

1
2
3
4
5
xml复制代码<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.7</version>
</dependency>

导入@Configuration类

需要导入的bean:Hello

1
2
3
4
5
6
java复制代码public class Hello {

public void print() {
System.out.println("hello word");
}
}

配置类:HelloConfiguration

1
2
3
4
5
6
7
8
java复制代码@Configuration
public class HelloConfiguration {

@Bean
public Hello createHello() {
return new Hello();
}
}

启动类:SpringTestApplication

1
2
3
4
5
6
7
8
9
java复制代码@Import(HelloConfiguration.class)
public class SpringTestApplication {

public static void main(String[] args) {
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(SpringTestApplication.class);
Hello hello = applicationContext.getBean(Hello.class);
hello.print();
}
}

启动之后,控制台输出如下:

1
2
3
arduino复制代码hello word

Process finished with exit code 0

说明Hello对象确实通过@Import注解注入到了容器中。

导入ImportSelector

ImportSelector实现类:HelloImportSelector

ImportSelector是一个接口,实现这个接口需要重写selectImports方法。

selectImports方法会返回一个String数组。这个数组包含的元素是需要被导入容器中的类的全限定名。

下面我们实现ImportSelector接口并重写selectImports方法,将Hello类的全限定名返回。

1
2
3
4
5
6
7
8
java复制代码public class HelloImportSelector implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
List<String> classNameList = new ArrayList<>();
classNameList.add("com.xgc.entity.Hello");
return StringUtils.toStringArray(classNameList);
}
}

启动类:SpringTestApplication

1
2
3
4
5
6
7
8
9
java复制代码@Import(HelloImportSelector.class)
public class SpringTestApplication {

public static void main(String[] args) {
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(SpringTestApplication.class);
Hello hello = applicationContext.getBean(Hello.class);
hello.print();
}
}

启动之后,控制台输出如下:

1
2
3
arduino复制代码hello word

Process finished with exit code 0

导入ImportBeanDefinitionRegistrar

ImportBeanDefinitionRegistrar实现类:HelloImportDefinitionRegistrar

1
2
3
4
5
6
7
8
java复制代码public class HelloImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {

@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
RootBeanDefinition rootBeanDefinition = new RootBeanDefinition(Hello.class);
registry.registerBeanDefinition("hello", rootBeanDefinition);
}
}

启动类:SpringTestApplication

1
2
3
4
5
6
7
8
9
java复制代码@Import(HelloImportBeanDefinitionRegistrar.class)
public class SpringTestApplication {

public static void main(String[] args) {
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(SpringTestApplication.class);
Hello hello = applicationContext.getBean(Hello.class);
hello.print();
}
}

启动之后,控制台输出如下:

1
2
3
arduino复制代码hello word

Process finished with exit code 0

直接导入Bean

启动类:SpringTestApplication

1
2
3
4
5
6
7
8
9
java复制代码@Import(Hello.class)
public class SpringTestApplication {

public static void main(String[] args) {
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(SpringTestApplication.class);
Hello hello = applicationContext.getBean(Hello.class);
hello.print();
}
}

启动之后,控制台输出如下:

1
2
3
arduino复制代码hello word

Process finished with exit code 0

本文转载自: 掘金

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

ElasticSearch从入门到精通(持续更新)—

发表于 2021-11-26

这是我参与11月更文挑战的第12天,活动详情查看:2021最后一次更文挑战。

(持续更新….)

什么是分词器

分词器 接受一个字符串作为输入,将这个字符串拆分成独立的词或 语汇单元(token) (可能会丢弃一些标点符号等字符),然后输出一个 语汇单元流(token stream) 。

全文搜索引擎会用某种算法对要建索引的文档进行分析, 从文档中提取出若干Token(词元), 这些算法称为Tokenizer(分词器), 这些Token会被进一步处理, 比如转成小写等, 这些处理算法被称为Token Filter(词元处理器), 被处理后的结果被称为Term(词), 文档中包含了几个这样的Term被称为Frequency(词频)。 引擎会建立Term和原文档的Inverted Index(倒排索引), 这样就能根据Term很快到找到源文档了。 文本被Tokenizer处理前可能要做一些预处理, 比如去掉里面的HTML标记, 这些处理的算法被称为Character Filter(字符过滤器), 这整个的分析算法被称为Analyzer(分析器)

Analysis

Analysis: 文本分析是把全文本转换一系列单词(term/token)的过程,也叫分词。Analysis是通过Analyzer来实现的。

当一个文档被索引时,每个Field都可能会创建一个倒排索引(Mapping可以设置不索引该Field)。

倒排索引的过程就是将文档通过Analyzer分成一个一个的Term,每一个Term都指向包含这个Term的文档集合。

当查询query时,Elasticsearch会根据搜索类型决定是否对query进行analyze,然后和倒排索引中的term进行相关性查询,匹配相应的文档。

Analyzer

分析器(analyzer)都由三种构件块组成的:characterfilters , tokenizers , token filters。

character filter 字符过滤器;在一段文本进行分词之前,先进行预处理,比如过滤html标签。

tokenizers 分词器;英文分词可以根据空格将单词分开,中文分词比较复杂,可以采用机器学习算法来分词。

Token filters Token过滤器;将切分的单词进行加工。大小写转换(例将“Quick”转为小写),去掉词(例如停用词像“a”、“and”、“the”等等),或者增加词(例如同义词像“jump”和“leap”)。

三者顺序:Character Filters—>Tokenizer—>Token Filter

三者个数:analyzer = CharFilters(0个或多个) + Tokenizer(恰好一个) + TokenFilters(0个或多个)

二、ES内置分词器

ES内置了一堆分词器,如下:

  • Standard Analyzer - 默认分词器,按词切分,小写处理
  • Simple Analyzer - 按照非字母切分(符号被过滤), 小写处理
  • Stop Analyzer - 小写处理,停用词过滤(the,a,is)
  • Whitespace Analyzer - 按照空格切分,不转小写
  • Keyword Analyzer - 不分词,直接将输入当作输出
  • Patter Analyzer - 正则表达式,默认\W+(非字符分割)
  • Language - 提供了30多种常见语言的分词器
  • Customer Analyzer 自定义分词器

这里主要说下标准Standard分词器,Simple,whitespace

Standard Analyzer

standard 是默认的分析器。
#标准分词器

1
2
3
4
5
json复制代码POST _analyze
{
"analyzer": "standard",
"text": "Like X 国庆放假的"
}

结果:

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
json复制代码{
"tokens" : [
{
"token" : "like",
"start_offset" : 0,
"end_offset" : 4,
"type" : "<ALPHANUM>",
"position" : 0
},
{
"token" : "x",
"start_offset" : 5,
"end_offset" : 6,
"type" : "<ALPHANUM>",
"position" : 1
},
{
"token" : "国",
"start_offset" : 7,
"end_offset" : 8,
"type" : "<IDEOGRAPHIC>",
"position" : 2
},
{
"token" : "庆",
"start_offset" : 8,
"end_offset" : 9,
"type" : "<IDEOGRAPHIC>",
"position" : 3
},
{
"token" : "放",
"start_offset" : 9,
"end_offset" : 10,
"type" : "<IDEOGRAPHIC>",
"position" : 4
},
{
"token" : "假",
"start_offset" : 10,
"end_offset" : 11,
"type" : "<IDEOGRAPHIC>",
"position" : 5
},
{
"token" : "的",
"start_offset" : 11,
"end_offset" : 12,
"type" : "<IDEOGRAPHIC>",
"position" : 6
}
]
}

Simple Analyzer

simple 分析器当它遇到只要不是字母的字符,就将文本解析成term,而且所有的term都是小写的。

#Simple

1
2
3
4
5
json复制代码POST _analyze
{
"analyzer": "simple",
"text": "Like X 国庆放假 的"
}

结果:

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
json复制代码{
"tokens" : [
{
"token" : "like",
"start_offset" : 0,
"end_offset" : 4,
"type" : "word",
"position" : 0
},
{
"token" : "x",
"start_offset" : 5,
"end_offset" : 6,
"type" : "word",
"position" : 1
},
{
"token" : "国庆放假",
"start_offset" : 7,
"end_offset" : 11,
"type" : "word",
"position" : 2
},
{
"token" : "的",
"start_offset" : 12,
"end_offset" : 13,
"type" : "word",
"position" : 3
}
]
}

whitespace Analyzer

按照空格分词,英文不区分大小写,中文不分词。

#whitespace

1
2
3
4
5
json复制代码POST _analyze
{
"analyzer": "whitespace",
"text": "Like X 国庆放假 的"
}

结果:

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
json复制代码{
"tokens" : [
{
"token" : "Like",
"start_offset" : 0,
"end_offset" : 4,
"type" : "word",
"position" : 0
},
{
"token" : "X",
"start_offset" : 5,
"end_offset" : 6,
"type" : "word",
"position" : 1
},
{
"token" : "国",
"start_offset" : 7,
"end_offset" : 8,
"type" : "word",
"position" : 2
},
{
"token" : "庆放假",
"start_offset" : 9,
"end_offset" : 12,
"type" : "word",
"position" : 3
},
{
"token" : "的",
"start_offset" : 13,
"end_offset" : 14,
"type" : "word",
"position" : 4
}
]
}

三、IK中文分词器

中文的分词器现在大家比较推荐的就是 IK分词器,当然也有些其它的比如 smartCN、HanLP。

IK分词器安装

开源分词器 Ik 的github:github.com/medcl/elast…

注意 IK分词器的版本要你安装ES的版本一致,我这边是7.9.0那么就在github找到对应版本,然后重启ES。

IK有两种颗粒度的拆分:

1、ik_smart: 会做最粗粒度的拆分

1
2
3
4
5
bash复制代码POST /_analyze
{
"text":"Howill的中华人民共和国国徽",
"analyzer":"ik_smart"
}

结果:

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
json复制代码{
"tokens" : [
{
"token" : "howill",
"start_offset" : 0,
"end_offset" : 6,
"type" : "ENGLISH",
"position" : 0
},
{
"token" : "的",
"start_offset" : 6,
"end_offset" : 7,
"type" : "CN_CHAR",
"position" : 1
},
{
"token" : "中华人民共和国",
"start_offset" : 7,
"end_offset" : 14,
"type" : "CN_WORD",
"position" : 2
},
{
"token" : "国徽",
"start_offset" : 14,
"end_offset" : 16,
"type" : "CN_WORD",
"position" : 3
}
]
}

2、ik_max_word: 会将文本做最细粒度的拆分

1
2
3
4
5
json复制代码POST _analyze
{
"analyzer": "ik_max_word",
"text": "Howill的中华人民共和国国徽"
}

结果:

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
json复制代码{
"tokens" : [
{
"token" : "howill",
"start_offset" : 0,
"end_offset" : 6,
"type" : "ENGLISH",
"position" : 0
},
{
"token" : "的",
"start_offset" : 6,
"end_offset" : 7,
"type" : "CN_CHAR",
"position" : 1
},
{
"token" : "中华人民共和国",
"start_offset" : 7,
"end_offset" : 14,
"type" : "CN_WORD",
"position" : 2
},
{
"token" : "中华人民",
"start_offset" : 7,
"end_offset" : 11,
"type" : "CN_WORD",
"position" : 3
},
{
"token" : "中华",
"start_offset" : 7,
"end_offset" : 9,
"type" : "CN_WORD",
"position" : 4
},
{
"token" : "华人",
"start_offset" : 8,
"end_offset" : 10,
"type" : "CN_WORD",
"position" : 5
},
{
"token" : "人民共和国",
"start_offset" : 9,
"end_offset" : 14,
"type" : "CN_WORD",
"position" : 6
},
{
"token" : "人民",
"start_offset" : 9,
"end_offset" : 11,
"type" : "CN_WORD",
"position" : 7
},
{
"token" : "共和国",
"start_offset" : 11,
"end_offset" : 14,
"type" : "CN_WORD",
"position" : 8
},
{
"token" : "共和",
"start_offset" : 11,
"end_offset" : 13,
"type" : "CN_WORD",
"position" : 9
},
{
"token" : "国",
"start_offset" : 13,
"end_offset" : 14,
"type" : "CN_CHAR",
"position" : 10
},
{
"token" : "国徽",
"start_offset" : 14,
"end_offset" : 16,
"type" : "CN_WORD",
"position" : 11
}
]
}

不管是拼音分词器还是IK分词器,当深入搜索一条数据是时,必须是通过分词器分析的数据,才能被搜索到,否则搜索不到。

四、配置分词器

创建指定分词器的索引,索引创建之后就可以使用ik进行分词了,当你使用ES搜索的时候也会使用ik对搜索语句进行分词,进行匹配。

创建索引时设置分词器
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
json复制代码PUT book_wills
{
"settings":{
"number_of_shards": "6",
"number_of_replicas": "1",
//指定分词器
"analysis":{
"analyzer":{
"ik":{
"tokenizer":"ik_max_word"
}
}
}
},
"mappings":{
"novel":{
"properties":{
"author":{
"type":"text"
},
"wordCount":{
"type":"integer"
},
"publishDate":{
"type":"date",
"format":"yyyy-MM-dd HH:mm:ss || yyyy-MM-dd"
},
"briefIntroduction":{
"type":"text"
},
"bookName":{
"type":"text"
}
}
}
}
}

使用

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
bash复制代码GET book_will/_analyze
{
"text":"刘德华在香港唱歌",
"analyzer": "ik_max_word"
}

{
"tokens" : [
{
"token" : "刘德华",
"start_offset" : 0,
"end_offset" : 3,
"type" : "CN_WORD",
"position" : 0
},
{
"token" : "在",
"start_offset" : 3,
"end_offset" : 4,
"type" : "CN_CHAR",
"position" : 1
},
{
"token" : "香港",
"start_offset" : 4,
"end_offset" : 6,
"type" : "CN_WORD",
"position" : 2
},
{
"token" : "唱歌",
"start_offset" : 6,
"end_offset" : 8,
"type" : "CN_WORD",
"position" : 3
}
]
}
IK分词和拼音分词的组合使用

当我们创建索引时可以自定义分词器,通过指定映射去匹配自定义分词器。

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
bash复制代码PUT /my_index
{
"settings": {
"analysis": {
"analyzer": {
"ik_smart_pinyin": {
"type": "custom",
"tokenizer": "ik_smart",
"filter": ["my_pinyin", "word_delimiter"]
},
"ik_max_word_pinyin": {
"type": "custom",
"tokenizer": "ik_max_word",
"filter": ["my_pinyin", "word_delimiter"]
}
},
"filter": {
"my_pinyin": {
"type" : "pinyin",
"keep_separate_first_letter" : true,
"keep_full_pinyin" : true,
"keep_original" : true,
"limit_first_letter_length" : 16,
"lowercase" : true,
"remove_duplicated_term" : true
}
}
}
}
}
映射analyzer属性

当我们建type时,需要在字段的analyzer属性填写自己的映射

1
2
3
4
5
6
7
8
9
10
11
12
13
14
bash复制代码PUT /my_index/my_type/_mapping
{
"my_type":{
"properties": {
"id":{
"type": "integer"
},
"name":{
"type": "text",
"analyzer": "ik_smart_pinyin"
}
}
}
}

本文转载自: 掘金

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

事件选择模型(select模型) 事件选择模型

发表于 2021-11-26

这是我参与11月更文挑战的第二十四天,活动详情查看:2021最后一次更文挑战

事件选择模型

目的和要求

  1. 了解事件选择模型的应用场景;
  1. 掌握事件选择模型的通讯过程;
  1. 掌握事件选择模型的代码实现;
  1. 了解事件选择模型的改进方法:有序及增加客户端数量。

服务器端:

1、包含网络头文件网络库

1
2
3
4
5
6
7
arduino复制代码#include <WinSock2.h>

#include <stdio.h>

#pragma comment(lib, "Ws2_32.lib")

#pragma warning(disable:4996);

2、打开网络库

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
arduino复制代码int nRes = WSAStartup(wdVersion, &wdScokMsg);

    if (0 != nRes)

    {

         switch (nRes)

         {

         case WSASYSNOTREADY:

             printf("解决方案:重启。。。\n");

             break;

         case WSAVERNOTSUPPORTED:

             break;

         case WSAEINPROGRESS:

             break;

         case WSAEPROCLIM:

             break;

         case WSAEFAULT:

             break;

         }

         return 0;
    }

3、校验版本

1
2
3
4
5
6
7
8
9
10
11
scss复制代码if (2 != HIBYTE(wdScokMsg.wVersion) || 2 != LOBYTE(wdScokMsg.wVersion))

    {

         printf("版本有问题!\n");

         WSACleanup();

         return 0;

    }

4、创建SOCKET

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
ini复制代码SOCKET socketServer = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (INVALID_SOCKET == socketServer)

    {

         int err = WSAGetLastError();

         //清理网络库,不关闭句柄

         WSACleanup();

         return 0;

    }

    struct sockaddr_in si;

    si.sin_family = AF_INET;

    si.sin_port = htons(12345);//用htons宏将整型转为端口号的无符号整型

    si.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");

5、绑定地址与端口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
scss复制代码if (SOCKET_ERROR == bind(socketServer, (const struct sockaddr*)&si, sizeof(si)))

    {

         int err = WSAGetLastError();//取错误码

         printf("服务器bind失败错误码为:%d\n", err);

         closesocket(socketServer);//释放

         WSACleanup();//清理网络库

 

         return 0;

    }

    printf("服务器端bind成功!\n");

6、开始监听

1
2
3
4
5
6
7
8
9
10
11
12
scss复制代码if (SOCKET_ERROR == listen(socketServer, SOMAXCONN))
    {
         int err = WSAGetLastError();//取错误码

         printf("服务器监听失败错误码为:%d\n", err);

         closesocket(socketServer);//释放

         WSACleanup();//清理网络库
         return 0;
    }
    printf("服务器端监听成功!\n");

7、事件选择模型

7.1定义fd_sockevent_set结构体

1
2
3
4
5
6
7
8
9
10
ini复制代码WSAEVENT eventServer = WSACreateEvent();
    if (eventServer == WSA_INVALID_EVENT)
    {
         int createerr = WSAGetLastError();

         closesocket(socketServer);

         WSACleanup();
         return 0;
}

7.2为服务器创建事件句柄

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
scss复制代码if (WSAEventSelect(socketServer, eventServer, FD_ACCEPT) == SOCKET_ERROR)

    {

         int selecterr = WSAGetLastError();

         WSACloseEvent(eventServer);

         closesocket(socketServer);

         WSACleanup();

         return 0;

    }

7.3为服务器SOCEKT和事件句柄绑定事件码:FD_ACCEPT,并放入

1
2
3
4
5
6
7
ini复制代码sockevent_set

sockevent_set.evnetall[sockevent_set.count] = eventServer;

sockevent_set.sockall[sockevent_set.count] = socketServer;

sockevent_set.count++;

7.4循环查询事件状态是否有信号,没有信号则重新查询

7.4.1使用WSAWaitForMultipleEvents查询有信号的事件对应socket句柄下标
1
ini复制代码DWORD soindex = retSignal - WSA_WAIT_EVENT_0;
7.4.2使用WSAEnumNetworkEvents获取事件操作码
7.4.3如果是FD_ACCEPT操作码
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
scss复制代码if (NetworkEvents.lNetworkEvents & FD_ACCEPT)

         {

             //判断FD_ACCEPT错误码对应位是否有值

             if (NetworkEvents.iErrorCode[FD_ACCEPT_BIT] == 0)

             {

                  //正常处理,创建客户端

                  SOCKET socketClient = accept(sockevent_set.sockall[soindex], NULL, NULL);

                  //创建失败则跳过

                  if (socketClient == INVALID_SOCKET)

                  {

                      continue;

                  }

                  //创建成功则为该SOCKET创建事件对象

                  WSAEVENT wsaClientEvent = WSACreateEvent();

                  //失败则关闭SOCKET句柄

                  if (wsaClientEvent == WSA_INVALID_EVENT)

                  {

                      closesocket(socketClient);

                      continue;

                  }

                  //绑定,投递客户端事件对象

                  //客户端事件码通常有三种

                  if (WSAEventSelect(socketClient, wsaClientEvent, FD_READ | FD_WRITE | FD_CLOSE) == SOCKET_ERROR)

                  {

                      //出错关闭句柄,关闭事件对象

                      closesocket(socketClient);

                      WSACloseEvent(wsaClientEvent);

                      //获取错误码略

                      continue;

                  }

                  //绑定投递成功后将客户端事件和SOCKET放到sockevent_set里面

                  sockevent_set.evnetall[sockevent_set.count] = wsaClientEvent;
                  sockevent_set.sockall[sockevent_set.count] = socketClient;
                  sockevent_set.count++;
             }
            else
             {
                  //出现异常不影响其他处理
                  continue;

             }
         }
7.4.4如果是FD_WRITE操作码
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
perl复制代码if (NetworkEvents.lNetworkEvents & FD_WRITE)

         {

             //判断错误码对应位是否有值,没有说明SOCKET没有错误

             if (NetworkEvents.iErrorCode[FD_WRITE_BIT] == 0)

             {

                  if (send(sockevent_set.sockall[soindex], "连接成功~", sizeof("连接成功~"), 0) == SOCKET_ERROR)

                  {

                      int FD_WRITEsenderr = WSAGetLastError();

                      printf("得到FD_WRITE操作send函数执行的错误码为:%d\n", FD_WRITEsenderr);

                      continue;

                  }

             }

             else

             {

                  printf("得到FD_WRITE操作的错误码为:%d\n", NetworkEvents.iErrorCode[FD_WRITE_BIT]);

                  continue;

             }

         }

##### 7.4.5如果是FD_READ操作码

if (NetworkEvents.lNetworkEvents & FD_READ)

         {

             //判断错误码对应位是否有值,没有说明SOCKET没有错误

             if (NetworkEvents.iErrorCode[FD_READ_BIT] == 0)

             {

                  char strRecv[1500] = { 0 };

                  if (recv(sockevent_set.sockall[soindex], strRecv, sizeof(strRecv), 0) == SOCKET_ERROR)

                  {

                      int FD_READrecverr = WSAGetLastError();

                      printf("得到FD_READ操作recv函数执行的错误码为:%d\n", FD_READrecverr);

                      continue;

                  }

                  //打印接收的信息

                  printf("接收的消息为:%s\n", strRecv);

             }

             else

             {

                  printf("得到FD_READ操作的错误码为:%d\n", NetworkEvents.iErrorCode[FD_READ_BIT]);

                  continue;

             }

         }

7.4.5如果是FD_CLOSE操作码

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
scss复制代码if (NetworkEvents.lNetworkEvents & FD_CLOSE)

         {

             printf("FD_CLOSE操作\n");

             printf("得到FD_CLOSE操作的错误码为:%d\n", NetworkEvents.iErrorCode[FD_CLOSE_BIT]);

 

             //清理下线的客户端套接字

             closesocket(sockevent_set.sockall[soindex]);

             sockevent_set.sockall[soindex] = sockevent_set.sockall[sockevent_set.count - 1];

             //清理下线的客户端事件

             WSACloseEvent(sockevent_set.evnetall[soindex]);

             sockevent_set.evnetall[soindex] = sockevent_set.evnetall[sockevent_set.count - 1];

 

             sockevent_set.count--;

 

         }
//释放fd_sockevent_set中的事件和SOCKET句柄

for (int i = 0; i < sockevent_set.count; i++)

    {

         WSACloseEvent(sockevent_set.evnetall[i]);

         closesocket(sockevent_set.sockall[i]);

    }
    WSACleanup();

    system("pause");

客户端

可以查看以前文章Select(TCP)模型

运行结果

image.png

事件选择模型相对于SELECT模型有什么不同?

Select(选择)模型是Winsock中最常见的I/O模型。之所以称其为“Select模型”,是由于它的“中心思想”便是利用select函数,实现对I/O的管理。最初设计该模型时,主要面向的是某些使用UNIX操作系统的计算机,它们采用的是Berkeley套接字方案。Select模型已集成到Winsock 1.1中,它使那些想避免在套接字调用过程中被无辜“锁定”的应用程序,采取一种有序的方式,同时进行对多个套接字的管理。由于Winsock 1.1向后兼容于Berkeley套接字实施方案,所以假如有一个Berkeley套接字应用使用了select函数,那么从理论角度讲,毋需对其进行任何修改,便可正常运行。

事件选择模型是Winsock提供的另一个有用的异步I/O模型。和WSAAsyncSelect模型类似的是,它也允许应用程序在一个或多个套接字上,接收以事件为基础的网络事件通知。由WSAAsyncSelect模型采用的网络事件来说,它们均可原封不动地移植到新模型。在用新模型开发的应用程序中,也能接收和处理所有那些事件。该模型最主要的差别在于网络事件会投递至一个事件对象句柄,而非投递至一个窗口例程。

本文转载自: 掘金

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

Kerberos02:创建AD和安装客户端

发表于 2021-11-26

这是我参与11月更文挑战的第26天,活动详情查看:2021最后一次更文挑战

前言

在Kerberso中,所有租户的账号密码都存储在Account Database(简称AD)中,这样当用户每次使用kinit认证的时候,KDC就会去AD中查询用户是否存在。

Account Database

新建Account Database

在服务器端使用下面命令创建AD。

1
css复制代码kdb5_util create -s -r [realm]
  1. 执行成功之后,在/var/kerberos/krb5kdc目录下生成一系列的服务端配置文件
  2. -s表示生成stash file,并在其中存储master server key,-r指定realm

添加Database Administrator

每个数据库需要添加一个管理员,AD也不例外。Database Administrator也是一个principal,不过是一个能够管理database的principal

使用以下命令来创建数据库管理员的principal:

1
lua复制代码kadmin.local-q"addprinc admin/admin"

设置AD的ACL(Access ControlList)权限

在kadm5.ac文件中l写入 /admin@HADOOP.COM。

  1. kadmin daemon会使用该文件来管理对AD的访问权限
  2. 名称匹配*/admin@EXAMPLE.COM的principal,都认为是admin,权限是 *
  3. 这些admin可以操作其他的principal

启动deamon:

①. systemctl startkadmin.service
②. systemctl startkrb5kdc.service

安装客户端

安装软件

1
复制代码yum install -y krb5-workstation krb5-libs krb5-auth-dialog

配置文件

拷贝服务器的/etc/krb5.conf放到客户机上

AD操作

进入AD

  1. root用户,KDC服务器可以通过kadmin.local直接登录(aroot/admin@BDX.SD.CM)
  2. Client先使用kinit进行验证,Kinit admin/admin,再使用kadmin登录

kadmin命令

  1. addprinc test:添加principal
  2. delprinc test:描述principal
  3. listprincs:查看principal列表

在kadmin中生成keytab文件

通过执行下面命令来生成每个用户所需要的keytab文件。

1
bash复制代码xst-k/path/name.keytabprincipal

kinit

kinit -R:延长TGT的过期时间,如果不能,可能renewlife设置为0day了

  1. ticket过期后,如果想延长,一种方法是重新申请(需要输入密码),另一种是renew(不需要输入密码),每renew一次,就延长一个lifetime
  2. renew操作本身也有lifetime,即在ticket renew lifetime。在此lifetime之内,才能进行renew操作
  3. 如果max_renewable_life=0,那么在客户端 ticket_lifetime 结束时就会获取一个新的 ticket
  4. kinit -R xxx.keytab:在renewable_life内来续约

本文转载自: 掘金

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

JAVA对接WebService WebService学习

发表于 2021-11-26

这是我参与11月更文挑战的第18天,活动详情查看:2021最后一次更文挑战

WebService学习

webService简介

webService接口,cxf框架,用xml配置暴露的接口,接口提供方可以用已实现序列化接口的对象来接收,当然,一般接口的数据结构没有那么简单,那我们也可以相对应的在我们要接收数据的对象里加List或者对象属性来实现。cxf框架会自动把xml数据转化为我们所需的对象,底层传输的实际上是通过http协议传输xml数据(可以写一个拦截器获取发送的xml文件)

说白了就是xml文档的生成、传递、解析的过程。客户端生成xml文件后通过网络传送给服务器,服务器解析xml,获取参数执行方法得到返回值,之后生成xml文件,再传输给客户端,客户端解析xml后显示数据。

xml传输数据的话一般是webSerivce,偏银行多一些;而Json则偏互联网多一些。暴露接口的话,我们可以写一个controller,用@RequestMapping(“xxxx”)来指定接口的调用地址;底层传输的都是json字符串,我们可以用String来接收,也可以用对象来接收(实现序列化接口),一般用的框架有阿里的fastjson跟springMvc自带的jackson

暴露webservice服务

场景一:Spring boot2.X

环境:Spring boot2.X

  1. 添加cxf依赖
1
2
3
4
5
xml复制代码<dependency>
<groupId>org.apache.cxf</groupId>
<artifactId>cxf-spring-boot-starter-jaxws</artifactId>
<version>3.2.5</version>
</dependency>
  1. 在外放的Facade接口上添加@WebService注解,方法上添加@WebMethod注解
1
2
3
4
5
java复制代码@WebService
public interface AuthServiceFacade {
@WebMethod
AuthResponse auth(AuthRequest request);
}
  1. 实现类上添加注解@WebService注解
1
2
3
4
5
java复制代码@Component
@Service
@WebService(targetNamespace = "http://webservice.company.com/",endpointInterface = "com.***.auth.service.facade.AuthServiceFacade")
public class BankCardSingleAuthServiceImpl extends AbstractAuthService
implements AuthServiceFacade {}
  1. 设置WebServiceConfig

场景二:通过CommandLineRunner暴露

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码//SpringBoot启动后会自动加载并暴露服务,服务ip可以动态适配
@Configuration
@Order(1)//数字为初始化顺序
public class CxfConfig implements CommandLineRunner {

@Value("${url:localhost}")
private String url ;

@Override
public void run(String... args){
Service service = new Service();
Endpoint.publish("http://"+url+":8080/service", service);
}
}

Wsdl文件

重要属性

重要标签的说明

· types - 数据类型(标签)定义的容器,里面使用schema定义了一些标签结构供message引用

· message - 通信消息的数据结构的抽象类型化定义。引用types中定义的标签

· operation - 对服务中所支持的操作的抽象描述,一个operation描述了一个访问入口的请求消息与响应消息对。

· portType - 对于某个访问入口点类型所支持的操作的抽象集合,这些操作可以由一个或多个服务访问点来支持。

· binding - 特定端口类型的具体协议和数据格式规范的绑定。

· service- 相关服务访问点的集合

· port - 定义为协议/数据格式绑定与具体Web访问地址组合的单个服务访问点。

常见错误

无法通过包wsdl文件以maven打包的方式生成

需要删除maven打包的targe再打包即可生成

@WebService注解的类中无法注入@Autowrite

这个问题让我很头痛,没想到是通过使用静态块去做的。

1
2
3
4
5
6
7
8
9
10
11
java复制代码@WebService(endpointInterface = "***", serviceName = "***")
@Component
public class WebService implements IWebService {
@Autowired
private static ISecondService secondService;
// 这里使用静态,让 service 属于类
static {
//装载单个配置文件实例化ApplicationContext容器
ApplicationContext ctx = new ClassPathXmlApplicationContext("spring\\dubbo.xml");
secondService = (ISecondService) ctx.getBean("secondService");
}

soupUI能调试通但注册到别人服务中无法接收到请求,这个问题可能是因为两个服务之间格式不一样,我们可以在注册服务中再包一层,使用适配器设计模式处理这个问题。

本文转载自: 掘金

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

深入理解分布式事务,这一篇就够了 导读概述 分布式理论 Se

发表于 2021-11-26

导读概述

随着业务的快速发展,业务复杂度越来越高,大部分互联网公司几乎都会从单体走向分布式,特别是转向微服务架构,随之而来就必然遇到分布事务这个难题。

本文主要介绍一下Seata是如何保证数据一致性的,将从以下几个方面介绍一下分布式实现方案

1、介绍一下分布式理论(此部分如果已经掌握了可以跳过,看下面部分分享)

2、什么是Seata

3、Seata AT模式下如何解决分布式事务,应用场景

4、Seata TCC模式下如何解决分布式事务、应用场景、变种场景使用

5、Seata 下的Saga模式

本文以下单占库存案例为主线分享Seata分布式解决方案。

分布式理论

什么是事务?

指的就是一个操作单元,在这个操作单元中的所有操作最终要保持一致的行为,要么所有操作都成功,要么所有的操作都被撤销。

使用事务目的是什么

说明白了就是保证数据的一致性。

什么是本地事务呢

我们常常指的是关系型数据库的事务,关系型数据库事务四大特性:
  • Atomicity(原子性) :要么都成功,要么都失败。
  • Consistency(一致性): 在事务开始之前和事务结束以后,数据库的完整性没有被破坏,完整性包括外键约束、应用定义的等约束不会被破坏。
  • Isolation(隔离性):数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。
  • Durability(持久性):事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。

图片

优点:可以保证单机数据库层面的数据一致性。

为什么要使用分布式事务

分布式事务在分布式环境下,为满足可用性、性能与降级服务的需要,降低一致性与隔离性的要求,一方面遵循 BASE 理论

  • 基本业务可用性(Basic Availability)
  • 柔性状态(Soft state)
  • 最终一致性(Eventual consistency)

同样的,分布式事务也部分遵循 ACID 规范:

  • 原子性:严格遵循
  • 一致性:事务完成后的一致性严格遵循;事务中的一致性可适当放宽
  • 隔离性:并行事务间不可影响;事务中间结果可见性允许安全放宽
  • 持久性:严格遵循

需求是这样的:
随着业务发展在微服务架构下,【订单服务】和【库存服务】在两个业务系统里,同时分别调用对应的订单DB和库存DB,要求保证:要么库存和订单下单一起成功,如果库存占用和订单下单有一方失败要么就要回滚,保证数据的一致性。 此时场景数据库级别的事务就有些捉襟见肘了,此时分布式事务就派上用场了。

图片

分布式事务常见解决手段有哪些呢?

两阶段提交2PC、三阶段提交3PC、TCC、SAGA、本地消息表、基于可靠消息保证最终一致、最大努力通知等方案。


由于分布式事务方案,无法做到完全的ACID的保证,没有一种完美的方案,能够解决掉所有业务问题。因此在实际应用中,会根据业务的不同特性,选择最适合的分布式事务方案。

什么是分布式事务呢?

1、分布式事务 :

  • 首先应用于分布式系统场景,分布式事务是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。\
  • 例如在电商系统中,下单接口通常会扣减库存、添加优惠券、生成订单 id, 而订单服务与库存、添加优惠券、下单接口的成功与否,不仅取决于本地的 db 操作,而且依赖第三方系统的结果,这时候分布式事务就保证这些操作要么全部成功,要么全部失败。本质上来说,分布式事务就是为了保证不同数据库的数据一致性。

2、常见使用场景: **** 如用户注册送积分事务、创建订单减库存事务,银行转账事务等都可以应用分布式事务,分布式事务就是为了保证在分布式场景下,数据操作的正确执行。

提到分布式场景就绕不开CAP理论,三者只能满足两点,来解决分布式场景的问题,那么接下来先了解一下什么是CAP。

CAP原则

CAP 原则又称 CAP 定理,又被叫作布鲁尔定理,指的是在一个分布式系统中, Consistency(一致性)、 Availability(可用性)、Partition tolerance(分区容错性),三者不可得兼。

图片

CAP 原则的精髓就是要么AP,要么CP,要么AC,但是不存在CAP。如果在某个分布式系统中数据无副本,那么系统必然满足强一致性条件,因为只有独一数据,不会出现数据不一致的情况,此时C和P两要素具备,但是如果系统发生了网络分区状况或者宕机,必然导致某些数据不可以访问,此时可用性条件就不能被满足,即在此情况下获得了CP系统,但是CAP不可同时满足。


即要保证数据一致还要保证可用,鱼和熊掌不可兼得,那么就要取舍,于是产生以下几种一致性策略。

强一致性、弱一致性、最终一致性

1.强一致性

任何一次读都能读到某个数据的最近一次写的数据。系统中的所有进程,看到的操作顺序,都和全局时钟下的顺序一致。简言之,在任意时刻,所有节点中的数据是一样的。如:2PC、3PC、XA都属于强一致性方案。

2.弱一致性
数据更新后,如果能容忍后续的访问只能访问到部分或者全部访问不到,则是弱一致性。

3.最终一致性 不保证在任意时刻任意节点上的同一份数据都是相同的,但是随着时间的迁移,不同节点上的同一份数据总是在向趋同的方向变化。简单说,就是在一段时间后,节点间的数据会最终达到一致状态。如:TCC和消息队列模式、Saga模式属于最终一致性的解决方案。

了解了分布式事务中的强一致性和最终一致性理论,下面介绍几种常见的分布式事务的解决方案。

Seata 分布式事务

什么是Seata?

Seata是一款阿里巴巴开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。

什么是Seata 的AT模式?

AT模式的特点就是对业务无入侵式,整体机制分二阶段提交
  • 基于支持本地 ACID 事务的关系型数据库。
  • Java 应用,通过 JDBC 访问数据库。

整体机制:两阶段协议的演变

一阶段:

业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。

二阶段:

提交异步化,非常快速地完成;回滚通过一阶段的回滚日志进行反向补偿。

Seata术语:

  • TC (Transaction Coordinator) 事务协调者 :维护全局和分支事务的状态,驱动全局事务提交或回滚。
  • TM (Transaction Manager) 事务管理器:定义全局事务的范围:开始全局事务、提交或回滚全局事务。
  • RM (Resource Manager) 资源管理器:管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
在 AT 模式下,用户只需关注自己的业务SQL,用户的业务SQL 作为一阶段,Seata 框架会自动生成事务的二阶段提交和回滚操作。

Seata 的AT模式使用场景

怎么理解AT模式呢,咱们以占用库存和创建订单为例:

图片

  • TM端功能包括【占用库存】和【创建订单】的操作,使用@GlobalTransaction进行全局事务开启、提交、回滚。
  • TM开始RPC调用远程服务调用分支【库存】服务和【创建订单】服务。
  • RM端seata-client通过扩展DataSourceProxy,实现自动生成undo_log与TC上报。
  • TM告知TC提交/回滚全局事务。
  • TC通知RM各自执行commit/rollback操作,同时清除undo_log。
下面为你介绍一下AT模式下的占库存和生单流程拆解如下:

第一阶段(占用库存过程)

图片

  • TM开启全局事务。
  • 库存注册分支事务,获取全局锁。
  • 本地事务提交:插入前镜像beforeImage->undolog日志,占用库存,插入后镜像afterImage->undolog日志,生成undo log一并提交,将本地事务提交的结果上报给TC。

第一阶段(创建订单过程)

图片

  • 创建订单注册分支事务,获取全局锁。
  • 本地事务提交:插入前镜像beforeImage->undolog日志,创建订单成功,插入后镜像afterImage->undolog日志,生成undo log一并提交,将本地事务提交的结果上报给TC。

第二阶段: 提交或回滚

图片

  • 场景1.如果占用库存成功 订单创建成功,则提交全局事务结束,删除本地undolog记录
  • 场景2.如果占用库存或订单创建某一方失败,则反向操作undolog回滚日志,删除本地undlog记录。

AT模式下如何保证数据一致的?

一阶段提交如何保持一致性?

  • TM:method下单服务方法执行时,由于该方法具有@GlobalTranscational标志,该TM会向TC发起全局事务,生成XID(全局锁)。
  • RM写表的过程,Seata 会拦截业务SQL,首先解析 SQL 语义,在业务数据被更新前,将其保存成before image,然后执行业务SQL,在业务数据更新之后,再将其保存成after image,最后生成行锁。以上操作全部在一个数据库事务内完成,这样保证了一阶段操作的原子性。

图片

二阶段如何保持一致性的?

  • 因为“业务 SQL”在一阶段已经提交至数据库, 所以 Seata 框架只需将一阶段保存的快照数据和行锁删掉,完成数据清理即可。
  • 正常:TM执行成功,通知TC全局提交,TC此时通知所有的RM提交成功,删除UNDO_LOG回滚日志。

图片

异常阶段如何保持一致性?

异常:TM执行失败,通知TC全局回滚,TC此时通知所有的RM进行回滚,根据undo_log反向操作,使用before image还原业务数据,删除undo_log,但在还原前要首先要校验脏写,对比“数据库当前业务数据”和 “after image”,如果两份数据完全一致就说明没有脏写,可以还原业务数据,如果不一致就说明有脏写,出现脏写就需要转人工处理了。

图片

什么是Seata 的TCC模式?

TCC 模式需要用户根据自己的业务场景实现 Try、Confirm 和 Cancel 三个操作;事务发起方在一阶段执行 Try 方式,在二阶段提交执行 Confirm 方法,二阶段回滚执行 Cancel 方法
  • 一阶段 :Try 资源的检测和预留。
  • 二阶段 commit 或 rollback回滚行为。

Confirm:执行的业务操作提交;要求 Try 成功 Confirm 一定要能成功。

Cancel:预留资源释放。

相应的,TCC模式(try-confirm-cancel),不依赖于底层数据资源的事务支持:

  • 一阶段 prepare 行为:调用自定义的 prepare 逻辑,Try 资源的检测和预留。
  • 二阶段 commit 行为:调用自定义的 commit 逻辑,执行的业务操作提交;要求 Try 成功Confirm 一定要能成功,失败则重试。
  • 二阶段 rollback 行为:调用自定义的 rollback 逻辑,预留资源释放。

所谓 TCC 模式,是指支持把自定义的分支事务纳入到全局事务的管理中。

图片

Seata 的TCC模式使用场景

怎么理解TCC模式呢,咱们还以下单扣库存为例,Try 阶段去占库存,Confirm 阶段则实际扣库存,如果库存扣减失败 Cancel 阶段进行回滚,释放库存。

TCC不存在资源阻塞的问题,因为每个方法都直接进行事务的提交,一旦出现异常通过则 Cancel 来进行回滚补偿,这也就是常说的补偿性事务,拆解流程请看下图:

**占用库存storage TCC流程图如下:**

图片

  • try: 冻结库存操作。
  • confirm: 占用库存操作。
  • cancel: 回滚事务,直接调用回滚库存接口。

**订单系统 order TCC流程图如下:**

图片

  • try: 新增订单-状态更新成【创建中】。
  • confirm: try成功,下单操作成功,订单状态由【创建中】更新成【创建成功】。
  • cancel: 回滚事务,删除该订单记录。

**小结** **:**

当占用库存成功同时订单下成功时,提交全局事务,当接入TCC模式,最重要的事情就是考虑如何将业务模型拆成2阶段,实现成TCC的3个方法,并且保证Try成功Confirm一定能成功。相对于AT模式,TCC模式对业务代码有一定的侵入性,但是TCC模式无AT模式的全局行锁,TCC性能会比AT模式高很多。

Seata 的TCC模式变种实现方案?

在说TCC变种简化变种方案前,先说一说现实的业务场景:
现实业务并不一定如我们所愿,在业务已成规模,订单系统 和 库存系统,都是在分布式场景,各自的业务接口,各自的DB表,如何保证数据一致呢?已实现逻辑采用业务代码异常调用返向补偿实现的,伪代码如下:

1
2
3
4
5
6
7
8
9
10
11
scss复制代码try{   
占用库存()  
try{    
下单方法()  
}cach(){ 
释放库存();   
取消订单();   
}  
}cach(){  
释放库存();
}
  • 库存服务:1.占用库存接口 2.释放库存接口。
  • 订单服务:1.下单接口 2.取消订单接口。

流程图如下:

图片

**基于现有逻辑如何改造成分布式事务实现方案呢?**

方案1:

采用AT模式,需要做的工作:

  • 在业务库 订单数据库和库存数据库 分别创建undo_log日志。
  • RM端(库存业务和订单业务代码)seata-client通过扩展DataSourceProxy,实现自动生成undo_log与tc上报,需要业务嵌入代码实现。

缺点:需要对库存业务库和订单业务库存分别存加 undo_log表,事务相对比较长。

方案2:采用Seata TCC模式

  • try阶段:提供冻结库存方法 和 预占用订单方法 需要改造库存表和订单表提供中间状态标识。
  • confirm阶段:提供占用库存方法和下单成功方法。
  • cancel阶段:提供返向 释放库存和取消订单方法。

缺点:业务代码要写库存锁定状态业务逻辑+订单创建中的中间态。

从上面两个方案不难看出都有一个通病,都需要业务代码改造,侵入强,耦合度高,能否有一种,即不需要业务代码改造成本又低的方案呢?

方案3:

接下来的方案是在TCC模式上做了些简化:

  • 即在try阶段:直接占库存成功和下单成功。
  • 在confirm阶段:不做业务处理,直接返回true,认为是成功的。
  • rollback阶段:做返回的取消操作即可。

缺点:浪费了一次commit空提交 IO交互。

优点:业务侵入性小,可以适应现有业务场景,偶合度低。

简化版本的TCC实现方案的时序图如下:

图片

Seata TCC分布式场景下如何保证幂等呢?

首先说一下什么是幂等? 幂等意思:通常指对同一个系统,使用同样的条件,一次请求和重复的多次请求对系统资源的影响是一致的.因为网络抖动或拥堵可能会超时,事务管理器会对资源进行重试操作,所以很可能一个业务操作会被重复调用,为了不因为重复调用而多次占用资源,造成业务脏数据,就需要保证幂等性。如何保证幂等呢? 此时需要对服务设计时进行幂等控制,通常我们可以用事务xid或业务主键判重来控制,或采用业务唯一标识来做幂等,总之还是需要业务自己来保证幂等的。

Seata下的Saga模式?

1.Saga 是一种补偿协议,Saga 理论出自 Hector & Kenneth 1987发表的论文 Sagas,其核心思想是将长事务拆分为多个本地短事务。解决了什么问题? 2.Saga模式是seata提供的长事务解决方案,在Saga模式中,业务流程中每个参与者都提交本地事务,当出现某一个参与者失败则补偿前面已经成功的参与者,一阶段正向服务和二阶段补偿服务都由业务开发实现。

图片

如图:

T1-T3都是正向的业务流程,都对应着一个冲正逆向补偿操作C1-C3

适用场景:

  • 业务流程长、业务流程多。
  • 参与者包含其它公司或遗留系统服务,无法提供 TCC 模式要求的三个接口。

优势:

  • 一阶段提交本地事务,无锁,高性能。
  • 事件驱动架构,参与者可异步执行,高吞吐。
  • 补偿服务易于实现。

缺点: Saga 模式由于一阶段已经提交本地数据库事务,且没有进行“预留”动作,所以不能保证隔离性。后续会讲到对于缺乏隔离性的应对措施。

seata分布式事务常见问题

常见问题答疑

****问:Seata框架如何来保证事务的隔离性的?****

答

因seata一阶段本地事务已提交,为防止其他事务脏读脏写需要加强隔离。

1.读隔离:Seata(AT 模式)的默认全局隔离级别是读未提交,必须要求全局的读已提交 ,目前 Seata 的方式是通过select语句加for update代理方法增加@GlobalLock+@Transactional或@GlobalTransaction实现。


2.写隔离:
  • 一阶段本地事务提交前,需要确保先拿到全局锁 。
  • 拿不到全局锁 ,不能提交本地事务。
  • 拿 全局锁 的尝试被限制在一定范围内,超出范围将放弃,并回滚本地事务,释放本地锁。

****问:脏数据回滚失败如何处理?****

答: 1.脏数据需手动处理,根据日志提示修正数据或者将对应undo删除(可自定义实现FailureHandler做邮件通知或其他人工介入操作)。

2.关闭回滚时undo镜像校验,不推荐该方案。

注意事项:建议事前做好隔离保证无脏数据。

**问:Seata支持哪些RPC框架?**

答:

  1. AT 模式支持:Dubbo、Spring Cloud、Motan、GRPC 和 Sofa-RPC。
  2. TCC 模式支持:Dubbo、Spring Cloud和Sofa-RPC。

****问:Seata现阶段支持哪些分库分表解决方案?****

答:
现阶段只支持ShardingSphere。

****问:Seata目前支持高可用吗?****

****答:支持****

1.tc使用db模式共享全局事务会话信息,注册中心使用非file的seata支持的第三方注册中心。

2.注册中心包含:eureka、consul、nacos、etcd、zookeeper、sofa、redis、file (直连)。

小结:

  • 在当前的技术发展阶段,不存一个分布式事务处理机制可以完美满足所有场景的需求。一致性、可靠性、易用性、性能等诸多方面的系统设计约束,需要用不同的事务处理机制去满足。
  • 目前使用的流行度情况是:AT>TCC > Saga,Seata 项目最核心的价值在于:构建一个全面解决分布式事务问题的标准化平台。基于 Seata,上层应用架构可以根据实际场景的需求,灵活选择合适的分布式事务解决方案。

图片

分布式方案对比

Seata针对不同的业务场景提供了四种不同的事务模式,对比如下:

  • AT模式:AT 模式的一阶段、二阶段提交和回滚(借助undo_log表来实现)均由 Seata 框架自动生成,用户只需编写“业务SQL”,便能轻松接入分布式事务,AT 模式是一种对业务无任何侵入的分布式事务解决方案。
  • TCC模式:相对于 AT 模式,TCC 模式对业务代码有一定的侵入性,但是 TCC 模式无 AT 模式的全局行锁,TCC 性能会比 AT模式高很多。适用于核心系统等对性能有很高要求的场景。
  • SAGA模式:Sage 是长事务解决方案,事务驱动,使用那种存在流程审核的业务场景。
  • XA模式:XA模式是分布式强一致性的解决方案,但性能低而使用较少。

图片

• 后记 •

本文介绍了分布式事务的一些基础理论,主要对Seata分布式事务方案进行了讲解,在文章的后半部分主要给出了各种方案的常用场景。分布式事务本身就是一个技术难题,业务中具体使用哪种方案还是需要根据自身业务特点自行选择,每种方案在实际执行过程中需要考虑的点都非常多,复杂度较大,所以在非必要的情况下,分布式事务能不用就尽量不用,希望对大家有所帮助,如果感觉有所收获,可以动动小手指给点个赞,感谢阅读!

微信扫码 欢迎加博主关注

本文转载自: 掘金

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

1…162163164…956

开发者博客

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