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

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


  • 首页

  • 归档

  • 搜索

LeetCode-数组中数字出现的次数(单身狗问题)

发表于 2021-10-25

小知识,大挑战!本文正在参与「程序员必备小知识」创作活动

本文已参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金

前言:🛫初来掘金!欢迎大佬们支持!相互学习,相互成长!

题目要求

一个数组中只有两个数字是出现一次,其他所有数字都出现了两次。编写一个函数找出这两个只出现一次的数字。


方法:

  • 由于其他数都出现两遍,把这些数异或在一起,结果为0->,所以数组中所有元素异或起来,实际是两个单身狗异或的结果记为ret
  • 由于两个单身狗不相同,所有异或的结果至少有1个比特位为1 (异或特点:对应比特位,相同为0,不同为1)
  • 所以可以对ret逐位判断,找到为1的比特位所在的位置进行分组。pos:两个比特位不同的位置(即异或的结果对应的比特位为1的位置:可任意位置,只要对应比特位为1即可)
  • 把数组中所有元素的pos位为1的放在一组,把pos位为0的放在另一组。认为最低位是第0位

图解

image.png

image.png


image.png

选择二进制序列中的哪一个比特位为1进行分组没有关系,只要是选择异或结果的二进制序列中某一位为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
c复制代码int main()
{
int arr[] = {1,1,2,5,6,5,7,8,7,8};
int sz = sizeof(arr)/sizeof(arr[0]);
int ret = 0;
int i = 0;
//将数组中的所有元素进行异或,得到的结果就是两个单身狗异或的结果
for(i = 0;i<sz;i++)
{
ret^=arr[i];
}
//得到ret的二进制序列中比特位为1的位置
int pos = 0;
for(i = 0;i<32;i++)
{
//ret不断左移i位,与1相与,如果结果为1,说明该比特位为1
if( ( (ret>>i) &1 ) ==1 )
{
pos = i;
break;
}
}
//将数组元素中二进制序列pos位为1的分到1组中,为0的分到另一组
int m = 0;
int n = 0;
for(i = 0;i<sz;i++)
{
if( ( (arr[i] >>pos)&1 )== 1 )
{
//pos位置比特位为1和m异或
m ^=arr[i];
}
else
{
//pos位置比特位为0和n异或
n ^= arr[i];
}
}
printf("%d %d\n",m,n);
return 0;
}

leetcode题目:

链接:剑指 Offer 56 - I. 数组中数字出现的次数 - 力扣(LeetCode) (leetcode-cn.com)

image.png

函数接口:

1
2
3
4
5
6
arduino复制代码/**
* Note: The returned array must be malloced, assume caller calls free().
*/
int* singleNumbers(int* nums, int numsSize, int* returnSize){

}

思路和上面的基本一致,但是要注意,这个题目要重新开辟一个空间用来保存两个单身狗,所以数组只需开辟两个大小的空间即可。

题目已经明确说明,动态开辟的数组不用我们释放,由调用者自己释放。所以我们只需要返回该数组的起始地址即可!!

注意点:动态开辟的数组的元素最初要初始化为0,否则最初动态开辟的数组的两个元素都是随机值,这样进行分组异或的话就会出错

->为数组元素初始化可以使用:直接使用下标初始化:

e.g. arr[0] = 0 arr[1] = 0

也可以使用memeset()函数进行初始化!


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
c复制代码/**
* Note: The returned array must be malloced, assume caller calls free(). ->后序我们不用释放,留个使用者释放
*/
int* singleNumbers(int* nums, int numsSize, int* returnSize){
int i = 0;
int ret = 0;
//数组的元素进行异或,得到的结果就是两个单身狗异或的结果
for(i = 0;i<numsSize;i++)
{
ret ^=nums[i];
}
//得到ret的二进制序列中比特位为1的位置pos
int pos = 0;
for(i = 0;i<32;i++)
{
//两个单身狗异或结果不断左移,直到找到比特位为1的位置
if( ((ret>>i)&1) == 1 )
{
pos = i;
break;
}
}
//将数组元素比特位pos位置为1的分到一组,为0的分到另一组进行异或
//开辟数组用来存储两个单身狗
int* newarr = (int*)malloc(sizeof(int)*2);
//注意:数组元素要初始化为0,否则不能通过,得到的是随机值
newarr[0] = 0;
newarr[1]= 0;
for(i = 0;i<numsSize;i++)
{
//元素比特位pos位置为1的和数组第一个元素异或,为0的和数组第二个元素异或
if( ((nums[i]>>pos)&1) == 1)
{
newarr[0] ^=nums[i];
}
else
{
newarr[1] ^=nums[i];
}
}
//要返回数组中所含元素 ,让用户得知数组的元素个数。
*returnSize = 2;
return newarr;
}

好了,今天就到这儿吧,欢迎大佬们点赞、收藏、评论呀!笔者水平有限,欢迎各位大佬批评指正!再次感谢!

本文转载自: 掘金

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

netty系列之 netty对http2消息的封装 简介 h

发表于 2021-10-25

小知识,大挑战!本文正在参与「程序员必备小知识」创作活动

本文已参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金。

「欢迎在评论区讨论,掘金官方将在掘力星计划活动结束后,在评论区抽送100份掘金周边,抽奖详情见活动文章」。

简介

无论是什么协议,如果要真正被使用的话,需要将该协议转换成为对应的语言才好真正的进行应用,本文将从http2消息的结构出发,探讨一下netty对http2消息的封装,带大家领略一下真正的框架应该做到什么程度。

http2消息的结构

http2和http1.1不同的是它使用了新的二进制分帧,通过客户端和服务器端建立数据流steam来进行客户端和服务器端之间消息的交互。其中数据流是一个双向字节流,用来发送一条或者多条消息。

消息是客户端和服务端发送的一个逻辑上完整的数据。根据数据大小的不同,可以将消息划分为不同的帧Frame。也就是说message是由不同的frame组成的。

frame就是http2中进行通信的最小单位,根据上一节的介绍,我们知道frame有这样几种:

  • DATA frame
  • HEADERS frame
  • PRIORITY frame
  • RST_STREAM frame
  • SETTINGS acknowledgment frame
  • SETTINGS frame
  • PING frame
  • PING acknowledgment
  • PUSH_PROMISE frame
  • GO_AWAY frame
  • WINDOW_UPDATE frame
  • Unknown Frame

我们看一下http2中stream和frame的一个大体的结构:

在http2中,一个TCP连接,可以承载多个数据流stream,多个stream中的不同frame可以交错发送。

每个frame通过stream id来标记其所属的stream。

有了上面的http2的基本概念,我们接下来就看下netty对http2的封装了。

netty对http2的封装

Http2Stream

作为一个TCP连接下面的最大的单位stream,netty中提供了接口Http2Stream。注意,Http2Stream是一个接口,它有两个实现类,分别是DefaultStream和ConnectionStream。

Http2Stream中有两个非常重要的属性,分别是id和state。

id前面已经介绍了,是stream的唯一标记。这里要注意由客户端建立的 Stream ID 必须是奇数,而由服务端建立的 Stream ID 必须是偶数。另外Stream ID 为 0 的流有特殊的作用,它是CONNECTION_STREAM_ID,1 表示HTTP_UPGRADE_STREAM_ID。

state表示stream的状态,具体而言,stream有下面几个状态:

1
2
3
4
5
6
7
scss复制代码        IDLE(false, false),
RESERVED_LOCAL(false, false),
RESERVED_REMOTE(false, false),
OPEN(true, true),
HALF_CLOSED_LOCAL(false, true),
HALF_CLOSED_REMOTE(true, false),
CLOSED(false, false);

为什么状态需要区分local和remote呢?这是因为stream连接的两端,所以有两端的状态。

和stream状态相对应的就是http2的生命周期了。netty提供了Http2LifecycleManager来表示对http2生命周期的管理:

1
2
3
4
5
6
7
8
arduino复制代码    void closeStreamLocal(Http2Stream stream, ChannelFuture future);
void closeStreamRemote(Http2Stream stream, ChannelFuture future);
void closeStream(Http2Stream stream, ChannelFuture future);
ChannelFuture resetStream(ChannelHandlerContext ctx, int streamId, long errorCode,
ChannelPromise promise);
ChannelFuture goAway(ChannelHandlerContext ctx, int lastStreamId, long errorCode,
ByteBuf debugData, ChannelPromise promise);
void onError(ChannelHandlerContext ctx, boolean outbound, Throwable cause);

分别是关闭stream,重置stream,拒绝新建stream:goAway,和处理出错状态这几种。

Http2Frame

stream之后,就是真实承载http2消息的Http2Frame了。在netty中,Http2Frame是一个接口,它有很多具体的实现。

Http2Frame的直接子类包括HTTP2GoAwayFrame、HTTPPingFrame、Http2SettingsFrame和HTTP2SettingsAckFrame。

其中goAway表示不接受新的stream,ping用来进行心跳检测。SETTINGS用来修改连接或者 Stream 流的配置。

netty中专门有一个Http2Settings类和其对应。

在这个类中定义了一些特别的setting名字:

SETTINGS 名字 含义
SETTINGS_HEADER_TABLE_SIZE 对端索引表的最大尺寸
SETTINGS_ENABLE_PUSH 是否启用服务器推送功能
SETTINGS_MAX_CONCURRENT_STREAMS 接收端允许的最大并发 Stream 数量
SETTINGS_INITIAL_WINDOW_SIZE 发送端的窗口大小,用于 Stream 级别流控
SETTINGS_MAX_FRAME_SIZE 设置帧的最大大小
SETTINGS_MAX_HEADER_LIST_SIZE 对端头部索引表的最大尺寸

除了上面讲的4个frame之外,其他的frame实现都继承自Http2StreamFrame,具体而言有PriorityFrame,ResetFrame,HeadersFrame,DataFrame,WindowUpdateFrame,PushPromiseFrame和UnknownFrame。

各个frame分别代表了不同的功能。这里最重要的就是Http2HeadersFrame和Http2DataFrame。

Http2HeadersFrame主要是客户端发送给服务器端的http2请求。

具体而言除了标准的http1.1的header之外,http2还支持下面的header:

1
2
3
4
5
6
7
8
9
10
11
sql复制代码      METHOD(":method", true),

SCHEME(":scheme", true),

AUTHORITY(":authority", true),

PATH(":path", true),

STATUS(":status", false),

PROTOCOL(":protocol", true);

对于Http2DataFrame来说,他本身是一个ByteBufHolder,用来传递具体的数据信息。data frame的Payload直接存储在ByteBuf中。

总结

以上就是netty对http2消息的封装了。

本文的例子可以参考:learn-netty4

本文已收录于 www.flydean.com/28-netty-wr…

最通俗的解读,最深刻的干货,最简洁的教程,众多你不知道的小技巧等你来发现!

欢迎关注我的公众号:「程序那些事」,懂技术,更懂你!

本文转载自: 掘金

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

五千字总结 你真的懂MySql吗? 深入MySql难点和要点

发表于 2021-10-25

本文已参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。

深入MySql难点和要点

从大的方向看,mysql分为两层,server层和存储引擎层。存储引擎可以认为是插件,像innodb和myisam引擎都是其中之一。innodb引擎功能强大,是我们现在使用的最多的引擎。

本文涉及的存储引擎都是innodb引擎,讲的地方都是根据自己的经验和知识总结而来,不是很全面,讲的片面的地方希望大家都指正。

Server

Server是支持MySQL的一切使用特性的,比如索引功能,join功能,还有各种函数功能,比如limit、group、rand和now等提供的功能。

分层功能

server从上到下分为连接层、语法解析、语义解析层、查询优化层和执行层。

  • 连接层负责处理连接、校验权限。
  • 语法解析和语义解析层负责解析SQL语句,看包含了哪几张表,看是CRUD中的哪一个,column列的字段是否正确等。
  • 在查询优化层,MySQL计算这个crud语句是使用表的哪一个索引(这些语句如果有where条件命中索引的话),如果有多个索引命中,则选择效率最高的那个。

ddl语句在解析后,优化层不知道能不能帮忙做点优化(比如alter table add column选择的算法,是copy,还是选择inplace?,lock的选择,是none /shared mode/exclusive?),感觉ddl语句的算法选择还是会由innodb引擎自己选择一个开销最小的(尽量不加锁,尽量保证online,少io(各种日志文件 ))。

优化层大概知道要扫描的行的数量,explain命令是上面优化层的分析结论.

  • 优化层执行之后,Server执行层开始执行,和引擎交互。开始执行的时候,从客户端发起的这条查询query的状态就进入Sending data状态。比如对于insert,执行器发起一条insert指令,引擎执行完后就返回结果给执行层,server就认为执行成功了。对于一个事务,比如3条更新语句和一行delete语句,执行层也会依次给引擎发送事务的begin,…,commit指令。

执行查询的时候,会发起很精细的指令,比如

mysql> select * from T where ID=10;

  • ID如果没有索引,就发起

取这个表(主键索引)的第一行,判断ID是否=10,不是则跳过,是则保存在结果集(不知道是不是就是net_buffer)

取“下一行”,重复相同的逻辑

结果集返回给客户端

  • ID如果有主键索引,就发起:

查询这个表主键索引树的ID=10的,没查到则跳过,查到则保存在结果集

结果集返回给客户端

  • ID如果有普通索引,就发起:

查询这个表普通索引树的ID=10的,没查到则结束,查到则保存在结果集

取“下一行”,重复相同的逻辑

结果集返回给客户端

和引擎接口的两阶段提交

所以Server的执行层会一行一行的发起给引擎的指令,然后交互处理。对于事务,执行器在commit时候,会调用xa_prepare准备些binlog,更具体的情况是下面两阶段提交的xa协议过程。

xa0.png

总体效果来看执行层在事务结束的时候,会记录变更到Binlog日志文件。
(根据设置的binlog格式是row还是statement,image参数是Min还是Full来往binlog里面写入特定格式的二进制数据。)

MySQL认为所有的query(包括简单的select)都是一个事务,默认autocommit=on。这也是为什么binlog的记录里面,BEGIN(event type是Query)和commit(event type =Xid)都是一个event。

但是select语句不会记录在binlog里面。哪怕有加锁的情况,比如这个情况:

1
2
3
4
5
mysql复制代码begin ;

select * from t limit 1 for update ;

commit ;

BINLOG只会记录数据成功的情况,如果update实际没有更改一行数据,那么这个也不会记录的,在事务commit的时候,如果确实发生了改动,则会记录到binlog里面,另外注意binlog里面还有一个gtid的情况,这个在事务begin前都会写一个set gtid_next(这个用于特殊的场景,主备切换用的)

1
2
3
4
5
6
7
8
9
mysql复制代码SET @@SESSION.GTID_NEXT= '625900b7-e2b7-11eb-9c96-b8cef604b769:77166'|Gtid

BEGIN|Query

table_id: 240 (test.t)|Table_map

table_id: 240 flags: STMT_END_F |Update_rows

COMMIT /* xid=13152312 */|Xid

xid也不是事务ID,xid用于两阶段提交的redo log和binlog,保证这两者的一致性。

可是xid为什么不使用事务ID,因为事务ID是会复用的(系统中不存在这个事务,后面使用的事务当然就可以复用了),但是xid需要记录到binlog,不能重复,所以xid和trx_id没有复用。

ddl语句也会记录到binlog里面,这样备库也会按照这种方式去创建表结构,回放同样的ddl(比如drop table /create table)。

InnoDB引擎

现在我们可以看看底层的innodb引擎都做了什么。MySQL支持支持事务的ACID特性,离不开innoDB的支持。存储引擎的首要功能就是存储数据,InnoDB使用B+树结构来存储表的数据。

索引和存储数据结构

那要选择怎样的数据结构呢?

  • 有序数组查找很快,但是插入的效率很低。
  • 哈希表查找很快,插入和更新也快。但是为什么哈希表也不合适?哈希表完全是随机的,想要按照范围查询几乎不可能,这满足不了MySQL的业务要求。
  • 可以用来快速查找,快速更新、删除和新增的数据结构有很多,比如平衡二叉树LV树,红黑树等。

因为MySQL要存储的数据量很大,数据存在磁盘上,如果使用二叉树,树的层级很高,查找效率很慢。为了使得查询磁盘的次数减少,需要使用B-树。B-树存储索引和数据的节点一样,但是innodb对索引和数据的存储需求不同。所以InnoDB根据实际情况(叶子节点才存储数据,非叶子节点只存储索引),使用B+树作为组织数据存储和查找(CRUD)的数据结构。

要了解B-的过程,很有必要了解B-树的性质。

B-树性质:

  • M为树的阶,非叶子节点最多只有M个儿子,且M>2
  • root节点的儿子数为【2 M】,| 0
  • 除根节点以外的非叶子节点的儿子数为【M/2 M ]
  • 每个节点(root除外)存放至少M/2-1和至多M-1个关键字
  • 分裂前后、所有叶子节点位于同一层(那么一定是平衡的)

B+树叶满足b-树的要求,B+树的叶子节点的元素是所有数据,上层的数据只是用来索引、不作为最终结果。

关于B+树数据结构的增删改查这一块,算法还比较复杂,对B-树性质的理解和推演很有必要。可根据性质推演分裂过程。比如这个从一个节点分裂成5阶B-树的过程,如下可以看到:

btree.png

可以发现这个5阶的树(每页存四个数据,最多5个指针)只三层已经很存储很多节点了,而Mysql每页16k大小,四层已能存储大量的数据。

怎么支持事务

innodb引擎支持事务的ACID(原子性、一致性、持久性和隔离性)。

支持这些特性的代价非常高,往往意味着不太好的扩展性(这也就是mysql的水平扩展不如nosql的原因)。

原子性

原子性的意思就是要么同时成功,要么同时失败。

为了支持事务的原子性,InnoDB引擎设计了undolog日志,事务里面的每一个操作除了会修改实际的data,还会去undolog里面增加记录.

比如X->Y的操作,undolog会记录(事务ID:Y->X),这样回滚的时候,就直译可将数据从Y改为X。

undolog的作用不仅仅是用于事务回滚,还对下面提到的隔离级别镜像读提供了最有用的帮助。在RR(可重复读隔离级别)的时候,一个数据只读到自己事务开始时候的数据,后面这行数据如果被别的事务修改了,那么这行数据则会根据undolog的记录回滚。因为undolog日志不需要的内容可以清理掉,所以分析查找undo log日志的效率还是会挺高的。

隔离性

隔离性也就是指对并发的支持,引擎支持四种事务隔离级别,分别是串行化、RR(可重复读隔离级别)、RC(读已提交隔离级别)、RU(读未提交隔离级别)。

串行化就是完全按顺序执行,用一个线程同步阻塞的执行各个事务就好了。RU是read uncommitted,这个有事务和没事务完全就没有区别。这两个极端,使用关系数据库的业务一般都是不会选的。

RC是读已提交,其他事务已经提交的修改,在当前事务是可以读到的。RC存在不一致读的问题,在t1时刻读到的数据,和在t2读到的数据不一致。对于某些审计类的业务场景,比如明细和总额在不同的时刻读,就不一致了。这会造成业务的困扰。

tx.png

为了解决RC的问题,引擎设计了另外一种隔离级别RR,可重复读,在t1时刻读到的数据,和在t2读到的数据一致。也就是上面提到的undolog和READVIEW数据结构的设计使得可重复读成为可能,这种机制也叫做MVCC,多版本并发控制。

tx2.png

持久性

引擎需要支持持久性才能保证数据的安全可靠。内存掉电就丢数据了,引擎将数据存储到磁盘,才能保证数据的持久可靠。

假设引擎将数据存储到磁盘了,然后告诉执行器结果,执行器就将这个过程写到binlog。此时如果binlog写失败了,但是磁盘上的数据还在,这样备库和主库的数据就不一致了。

binlog写失败可能是各种各样的原因,比如磁盘满了,程序bug了,掉电了。此时执行器必须也返回一个binlog的结果给到引擎,如果binlog写失败,引擎再回滚数据。写到磁盘上的某块数据,再回滚。但是磁盘随机寻址折腾的效率很低,这么做效率很低。

为了保证性能,引擎设计了另外一种日志,redolog,如果有修改数据,在事务commit的时候,先不刷盘,而是将这个事务的修改append到redolog,这个也叫WAL(日志先行),并且标记为prepared状态,后面当执行器告知binlog也写成功的时候才标记为commit状态。

redolog.png

redolog是循环日志,有一个写入点(新日志都append到这里),还有一个checkpoint指针,相当于橡皮擦,checkpoint检查是否可以将检查点的的数据刷回磁盘。

redolog是存储引擎负责的,binlog是server执行器使用的。
redolog

一致性

前面讲述了ACID的原子性、隔离性和持久性三个性质在MySQL中是怎么满足的,一致性是在这三个性质得到很好满足的情况下,自然而然的结果。
比如采用了两阶段提交,来保证主背数据的一致性(binlog和redolog日志的一致性)。
在这样的情况下,业务的事务逻辑一致、主备一致、查询语义的一致等等都可以得到满足。

QA

梳理基础知识后,发现对理解MySQL还远远不够,还有很多的疑问,值得深究。

而且这一块的问题可以很有意思,也能触及到很多的细节。

1. 为什么有binlog,InnoDB还要使用redolog?

  • 从实现来看redolog是物理层面的日志记录,binlog是逻辑层面的。
  • redolog能保证持久性,而binlog不行
  • redolog是顺序写,写磁盘效率高
  • redolog能保证持久性。redolog和binlog通过xid保持一致,在故障恢复的时候,binlog可以通过xid来保证恢复和redolog一致(也就和其他备份都一致了)

2、insert into t2 select * from t1这条语句是怎么加锁的?

加锁的基本单位是next-key,

RR隔离级别下,对t1的所有记录加 next-key锁。

为了保证主备的一致性,这个地方本来是statement格式的才需要加next-key锁,这样可以保证日志和数据的一致性。不然备机上面的数据就和主机上面的不一致了。

又由于历史的一些原因,现在row格式的也都是加了next-key锁的。

3. redolog在checkpoint的时候刷数据到磁盘,是直接从redolog到磁盘,还是直接将buffer pool的数据刷到磁盘?

因为redolog记录的是磁盘地址的修改,比如A物理地址,数据从X-Y

buffer-pool是最新的data,对应的磁盘地址也都在内存当中,就直接将buffer-pool flush. 但是对于宕机重启的情况,从redolog的checkpoint的点开始,记录的地址不在buffer pool,则需要先将data load到内存,然后依次应用redo log里面所有涉及的修改,再刷新回磁盘。

4. RR有没有幻读,怎么解决的?

上面提到了RR,但是没有说到幻读这个事情。这篇文章提到了MVCC,但是还没有从锁的角度来解释并发写时候的控制。

写用当前读是什么意思?首先是因为写会加锁,尤其是next_key锁,还能防止幻读。

MySQL为了保证数据的强一致性,使用了加锁机制。在RR下,加锁的基本原则是next-key锁。

5. 为啥要commit的时候才能释放锁?

commit的时候表示成功了,才释放锁。不然中间可能还存在回滚。

当另外一个写能拿到锁的时候,那么读到的内容都是已经提交的,也就是当前读其实读已提交,未提交的数据还是读不到的。

所以当前读是读已提交。

在已提交上面加锁就防止了写覆盖。

比如两个事务都执行:update set t=t+1 where id=1; 最终就是+2;如果不用当前读就是+1;

所以不要站在读的立场去理解写(因为各种隔离级别,很容易站在读的立场去理解事务中的各种操作),在写的立场去理解写就很容易理解必要要加锁

写的时候记录了undolog,不影响业务需求想要的一致性读,可重复读。

读一个范围的时候:

1
2
3
4
5
6
7
sql复制代码beigin

select ... for update

select ... for update

commit

这是事务两次读,两次读不一致就是幻读,所以这个必须加锁,事务结束的时候才能释放锁。

6. 为什么不能有幻读?

幻读就是指在RR隔离级别下,一个事务里两次读的数据不一样。
这首先会导致语义不一致,其次,statement格式的 binlog日志和数据不一致,还会导致主备不一致。

所以next-key锁可以保证即使是在statement格式,binlog日志和数据也是一致的。

为了防止幻读,还引入了next-key锁,防止插入到间隙。

为什么next-key锁可以防止幻读呢?有没有一个理论上的公式。没有,但有一些原则,就是默认next-key锁(为何最后非要加一个key),唯一索引特殊处理,以及向右扫描策略。

next-key锁导致实际锁的范围很大(悲观锁),但是因为数据结构设计的原因,也只能如此了。

左开右闭的原因?可能只是一种工程原因。

不过可以从数据结构来理解锁的范围,看上面提到过的树结构:

btree.png
比如要给这个t表的索引树(普通索引c)where c=19 for update加锁,那么加的是next-key锁,也就是会锁住(14,23]这里的所有记录。

7. BufferPool是Server创建的数据结构,还是InnoDB的?

BufferPool当然是引擎创建的数据结构,使用LRU算法,比如我的db的buffer pool size是5G,而磁盘的数据量是100G,不可能把所有的数据都加载到内存的,所以需要使用LRU算法来选择哪些数据可以被淘汰。最近最少使用的数据least recently used的数据将会被淘汰(还针对业务新旧数据特征,做了很多改进优化)。

总结

本文并没有很全面的详述mysql中的所有知识点,但不影响我们查漏,这些都了解了吗?

  • innodbB+ 树存储
  • 叶子存树枝索引
  • 普通索引和主键树
  • 支持ac和id
  • 事务成功或失败
  • undolog来帮忙
  • 主键逆操作链回滚
  • 并发query要隔离
  • 隔离级别很复杂
  • 完全隔离串行化
  • 读未提交和已提交
  • RR 可重复读更隔离
  • 怎么支持?
  • MVCC多版本来控制
  • 事务ID是自增
  • ReadView记录当前活跃事务
  • 忽略活跃事务的修改
  • 修改记录还加锁
  • innodb行锁是单位
  • 事务结束释放锁
  • 其实事务修改是当前读
  • 而不再是可重复读
  • 这会存在另一个问题
  • 如果只加行锁会幻读
  • next-key锁来帮忙
  • 执行器控制引擎
  • 两阶段提交来一致
  • redolog和binlog常交互

本文转载自: 掘金

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

对比7种分布式事务方案,还是偏爱阿里开源的Seata(原理+

发表于 2021-10-25

前言

这是《Spring Cloud 进阶》专栏的第六篇文章,往期文章如下:

  • 五十五张图告诉你微服务的灵魂摆渡者Nacos究竟有多强?
  • openFeign夺命连环9问,这谁受得了?
  • 阿里面试这样问:Nacos、Apollo、Config配置中心如何选型?这10个维度告诉你!
  • 阿里面试败北:5种微服务注册中心如何选型?这几个维度告诉你!
  • 阿里限流神器Sentinel夺命连环 17 问?

这篇文章主要介绍一些目前主流的几种分布式解决方案以及阿里开源的一站式分布式解决方案Seata。

文章有点长,耐心看完,看完你还不懂分布式事务,欢迎来捶我……………

文章目录如下:

什么是分布式事务?

分布式对应的是单体架构,互联网早起单体架构是非常流行的,好像是一个家族企业,大家在一个家里劳作,单体架构如下图:

单体架构
但是随着业务的复杂度提高,大家族人手不够,此时不得不招人,这样逐渐演变出了分布式服务,互相协作,每个服务负责不同的业务,架构如下图:

分布式架构

因此需要服务与服务之间的远程协作才能完成事务,这种分布式系统环境下由不同的服务之间通过网络远程协作完成事务称之为分布式事务,例如用户注册送积分 事务、创建订单减库存事务,银行转账事务等都是分布式事务。

典型的场景就是微服务架构 微服务之间通过远程调用完成事务操作。 比如:订单微服务和库存微服务,下单的同时订单微服务请求库存微服务减库存。 简言之:跨JVM进程产生分布式事务。

什么是CAP原则?

CAP原则又叫CAP定理,同时又被称作布鲁尔定理(Brewer’s theorem),指的是在一个分布式系统中,不可能同时满足以下三点。

一致性(Consistency)

指强一致性,在写操作完成后开始的任何读操作都必须返回该值,或者后续写操作的结果。

也就是说,在一致性系统中,一旦客户端将值写入任何一台服务器并获得响应,那么之后client从其他任何服务器读取的都是刚写入的数据

一致性保证了不管向哪台服务器写入数据,其他的服务器能实时同步数据

可用性(Availability)

可用性(高可用)是指:每次向未崩溃的节点发送请求,总能保证收到响应数据(允许不是最新数据)

分区容忍性(Partition tolerance)

分布式系统在遇到任何网络分区故障的时候,仍然能够对外提供满足一致性和可用性的服务,也就是说,服务器A和B发送给对方的任何消息都是可以放弃的,也就是说A和B可能因为各种意外情况,导致无法成功进行同步,分布式系统要能容忍这种情况。除非整个网络环境都发生了故障。

为什么只能在A和C之间做出取舍?

分布式系统中,必须满足 CAP 中的 P,此时只能在 C/A 之间作出取舍。

如果选择了CA,舍弃了P,说白了就是一个单体架构。

一致性有几种分类?

CAP理论告诉我们只能在C、A之间选择,在分布式事务的最终解决方案中一般选择牺牲一致性来获取可用性和分区容错性。

这里的 “牺牲一致性” 并不是完全放弃数据的一致性,而是放弃强一致性而换取弱一致性。

一致性可以分为以下三种:

  • 强一致性
  • 弱一致性
  • 最终一致性

强一致性

系统中的某个数据被成功更新后,后续任何对该数据的读取操作都将得到更新后的值。

也称为:原子一致性(Atomic Consistency)、线性一致性(Linearizable Consistency)

简言之,在任意时刻,所有节点中的数据是一样的。例如,对于关系型数据库,要求更新过的数据能被后续的访问都能看到,这是强一致性。

总结:

  • 一个集群需要对外部提供强一致性,所以只要集群内部某一台服务器的数据发生了改变,那么就需要等待集群内其他服务器的数据同步完成后,才能正常的对外提供服务。
  • 保证了强一致性,务必会损耗可用性。

弱一致性

系统中的某个数据被更新后,后续对该数据的读取操作可能得到更新后的值,也可能是更改前的值。

但即使过了不一致时间窗口这段时间后,后续对该数据的读取也不一定是最新值。

所以说,可以理解为数据更新后,如果能容忍后续的访问只能访问到部分或者全部访问不到,则是弱一致性。

例如12306买火车票,虽然最后看到还剩下几张余票,但是只要选择购买就会提示没票了,这就是弱一致性。

最终一致性

是弱一致性的特殊形式,存储系统保证在没有新的更新的条件下,最终所有的访问都是最后更新的值。

不保证在任意时刻任意节点上的同一份数据都是相同的,但是随着时间的迁移,不同节点上的同一份数据总是在向趋同的方向变化。

简单说,就是在一段时间后,节点间的数据会最终达到一致状态。

总结

弱一致性即使过了不一致时间窗口,后续的读取也不一定能保证一致,而最终一致过了不一致窗口后,后续的读取一定一致。

什么是Base理论?

BASE理论是对CAP中的一致性和可用性进行一个权衡的结果,理论的核心思想就是:我们无法做到强一致,但每个应用都可以根据自身的业务特点,采用适当的方式来使系统达到最终一致性。

BA(Basic Available)基本可用

整个系统在某些不可抗力的情况下,仍然能够保证“可用性”,即一定时间内仍然能够返回一个明确的结果。这里是属于基本可用。

基本可用和高可用的区别:

  • “一定时间”可以适当延长 当举行大促(比如秒杀)时,响应时间可以适当延长
  • 给部分用户返回一个降级页面 给部分用户直接返回一个降级页面,从而缓解服务器压力。但要注意,返回降级页面仍然是返回明确结果。

S(Soft State)柔性状态

称为柔性状态,是指允许系统中的数据存在中间状态,并认为该中间状态的存在不会影响系统的整体可用性,即允许系统不同节点的数据副本之间进行数据同步的过程存在延时。

E(Eventual Consisstency)最终一致性

同一数据的不同副本的状态,可以不需要实时一致,但一定要保证经过一定时间后仍然是一致的。

分布式事务有哪几种解决方案?

在分布式架构下,每个节点只知晓自己操作的失败或者成功,无法得知其他节点的状态。当一个事务跨多个节点时,为了保持事务的原子性与一致性,而引入一个协调者来统一掌控所有参与者的操作结果,并指示它们是否要把操作结果进行真正的提交或者回滚(rollback)。

2阶段提交(2PC)

二阶段提交协议(Two-phase Commit,即 2PC)是常用的分布式事务解决方案,即将事务的提交过程分为两个阶段来进行处理。

两个阶段分别为:

  • 准备阶段
  • 提交阶段

参与的角色:

  • 事务协调者(事务管理器):事务的发起者
  • 事务参与者(资源管理器):事务的执行者

准备阶段(投票阶段)

这是两阶段的第一段,这一阶段只是准备阶段,由事务的协调者发起询问参与者是否可以提交事务,但是这一阶段并未提交事务,流程图如下图:

准备提交阶段

  1. 协调者向所有参与者发送事务内容,询问是否可以提交事务,并等待答复
  2. 各参与者执行事务操作,将 undo 和 redo 信息记入事务日志中(但不提交事务)
  3. 如参与者执行成功,给协调者反馈同意,否则反馈中止

提交阶段

这一段阶段属于2PC的第二阶段(提交 执行阶段),协调者发起正式提交事务的请求,当所有参与者都回复同意时,则意味着完成事务,流程图如下:

提交事务阶段

  1. 协调者节点向所有参与者节点发出正式提交(commit)的请求。
  2. 参与者节点正式完成操作,并释放在整个事务期间内占用的资源。
  3. 参与者节点向协调者节点发送ack完成消息。
  4. 协调者节点收到所有参与者节点反馈的ack完成消息后,完成事务。

但是如果任意一个参与者节点在第一阶段返回的消息为终止,或者协调者节点在第一阶段的询问超时之前无法获取所有参与者节点的响应消息时,那么这个事务将会被回滚,回滚的流程图如下:

回滚

  1. 协调者节点向所有参与者节点发出回滚操作(rollback)的请求。
  2. 参与者节点利用阶段1写入的undo信息执行回滚,并释放在整个事务期间内占用的资源。
  3. 参与者节点向协调者节点发送ack回滚完成消息。
  4. 协调者节点受到所有参与者节点反馈的ack回滚完成消息后,取消事务。

不管最后结果如何,第二阶段都会结束当前事务。

二阶段提交的事务正常提交的完整流程如下图:

事务正常提交完整流程

二阶段提交事务回滚的完整流程如下图:

事务回滚完整流程

举个百米赛跑的例子来具体描述下2PC的流程:学校运动会,有三个同学,分别是A,B,C,2PC流程如下:

  • 裁判:A同学准备好了吗?准备进入第一赛道….
  • 裁判:B同学准备好了吗?准备进入第一赛道….
  • 裁判:C同学准备好了吗?准备进入第一赛道….
  • 如果有任意一个同学没准备好,则裁判下达回滚指令
  • 如果裁判收到了所有同学的OK回复,则再次下令跑……
  • 裁判:1,2,3 跑…………
  • A同学冲刺到终点,汇报给裁判
  • B,C同学冲刺失败,汇报给裁判

2PC的缺点

二阶段提交看起来确实能够提供原子性的操作,但是不幸的是,二阶段提交还是有几个缺点的:

  • 性能问题:执行过程中,所有参与节点都是事务阻塞型的。当参与者占有公共资源时,其他第三方节点访问公共资源不得不处于阻塞状态。
  • 可靠性问题:参与者发生故障。协调者需要给每个参与者额外指定超时机制,超时后整个事务失败。协调者发生故障。参与者会一直阻塞下去。需要额外的备机进行容错。
  • 数据一致性问题:二阶段无法解决的问题:协调者在发出commit消息之后宕机,而唯一接收到这条消息的参与者同时也宕机了。那么即使协调者通过选举协议产生了新的协调者,这条事务的状态也是不确定的,没人知道事务是否被已经提交。
  • 实现复杂:牺牲了可用性,对性能影响较大,不适合高并发高性能场景。

2PC的优点

  • 尽量保证了数据的强一致,适合对数据强一致要求很高的关键领域。(其实也不能100%保证强一致)

3阶段提交(3PC)

三阶段提交协议,是二阶段提交协议的改进版本,三阶段提交有两个改动点。

  • 在协调者和参与者中都引入超时机制
  • 在第一阶段和第二阶段中插入一个准备阶段。保证了在最后提交阶段之前各参与节点的状态是一致的。

也就是说,除了引入超时机制之外,3PC把2PC的准备阶段再次一分为二,这样三阶段提交就有CanCommit、PreCommit、DoCommit三个阶段。处理流程如下:

3PC时序图

阶段一:CanCommit阶段

3PC的CanCommit阶段其实和2PC的准备阶段很像。协调者向参与者发送commit请求,参与者如果可以提交就返回Yes响应,否则返回No响应。

  • 事务询问:协调者向所有参与者发出包含事务内容的 canCommit 请求,询问是否可以提交事务,并等待所有参与者答复。
  • 响应反馈:参与者收到 canCommit 请求后,如果认为可以执行事务操作,则反馈 yes 并进入预备状态,否则反馈 no。

CanCommit阶段流程如下图:

CanCommit阶段

阶段二:PreCommit阶段

协调者根据参与者的反应情况来决定是否可以进行事务的PreCommit操作。根据响应情况,有以下两种可能。

  • 假如所有参与者均反馈 yes,协调者预执行事务。
    1. 发送预提交请求 :协调者向参与者发送PreCommit请求,并进入准备阶段
    2. 事务预提交 :参与者接收到PreCommit请求后,会执行事务操作,并将undo和redo信息记录到事务日志中(但不提交事务)
    3. 响应反馈 :如果参与者成功的执行了事务操作,则返回ACK响应,同时开始等待最终指令。

PreCommit

  • 假如有任何一个参与者向协调者发送了No响应,或者等待超时之后,协调者都没有接到参与者的响应,那么就执行事务的中断。
    1. 发送中断请求 :协调者向所有参与者发送abort请求。
    2. 中断事务 :参与者收到来自协调者的abort请求之后(或超时之后,仍未收到协调者的请求),执行事务的中断。

PreCommit

阶段三:doCommit阶段

该阶段进行真正的事务提交,也可以分为以下两种情况。

进入阶段 3 后,无论协调者出现问题,或者协调者与参与者网络出现问题,都会导致参与者无法接收到协调者发出的 do Commit 请求或 abort 请求。此时,参与者都会在等待超时之后,继续执行事务提交。

  • 执行提交
    1. 发送提交请求 协调接收到参与者发送的ACK响应,那么他将从预提交状态进入到提交状态。并向所有参与者发送doCommit请求。
    2. 事务提交 参与者接收到doCommit请求之后,执行正式的事务提交。并在完成事务提交之后释放所有事务资源。
    3. 响应反馈 事务提交完之后,向协调者发送ack响应。
    4. 完成事务 协调者接收到所有参与者的ack响应之后,完成事务。

docommit-提交事务

  • 中断事务:任何一个参与者反馈 no,或者等待超时后协调者尚无法收到所有参与者的反馈,即中断事务
    1. 发送中断请求 如果协调者处于工作状态,向所有参与者发出 abort 请求
    2. 事务回滚 参与者接收到abort请求之后,利用其在阶段二记录的undo信息来执行事务的回滚操作,并在完成回滚之后释放所有的事务资源。
    3. 反馈结果 参与者完成事务回滚之后,向协调者反馈ACK消息
    4. 中断事务 协调者接收到参与者反馈的ACK消息之后,执行事务的中断。

docommit-中断事务

优点

相比二阶段提交,三阶段提交降低了阻塞范围,在等待超时后协调者或参与者会中断事务。避免了协调者单点问题,阶段 3 中协调者出现问题时,参与者会继续提交事务。

缺点

数据不一致问题依然存在,当在参与者收到 preCommit 请求后等待 doCommit 指令时,此时如果协调者请求中断事务,而协调者无法与参与者正常通信,会导致参与者继续提交事务,造成数据不一致。

TCC(事务补偿)

TCC(Try Confirm Cancel)方案是一种应用层面侵入业务的两阶段提交。是目前最火的一种柔性事务方案,其核心思想是:针对每个操作,都要注册一个与其对应的确认和补偿(撤销)操作。

TCC分为两个阶段,分别如下:

  • 第一阶段:Try(尝试),主要是对业务系统做检测及资源预留 (加锁,锁住资源)
  • 第二阶段:本阶段根据第一阶段的结果,决定是执行confirm还是cancel
    1. Confirm(确认):执行真正的业务(执行业务,释放锁)
    2. Cancle(取消):是预留资源的取消(出问题,释放锁)

TCC

为了方便理解,下面以电商下单为例进行方案解析,这里把整个过程简单分为扣减库存,订单创建 2 个步骤,库存服务和订单服务分别在不同的服务器节点上。

假设商品库存为 100,购买数量为 2,这里检查和更新库存的同时,冻结用户购买数量的库存,同时创建订单,订单状态为待确认。

①Try 阶段

TCC 机制中的 Try 仅是一个初步操作,它和后续的确认一起才能真正构成一个完整的业务逻辑,这个阶段主要完成:

  • 完成所有业务检查( 一致性 ) 。
  • 预留必须业务资源( 准隔离性 ) 。
  • Try 尝试执行业务。

Try阶段

②Confirm / Cancel 阶段

根据 Try 阶段服务是否全部正常执行,继续执行确认操作(Confirm)或取消操作(Cancel)。

Confirm 和 Cancel 操作满足幂等性,如果 Confirm 或 Cancel 操作执行失败,将会不断重试直到执行完成。

Confirm:当 Try 阶段服务全部正常执行, 执行确认业务逻辑操作,业务如下图:

Try->Confirm

这里使用的资源一定是 Try 阶段预留的业务资源。在 TCC 事务机制中认为,如果在 Try 阶段能正常的预留资源,那 Confirm 一定能完整正确的提交。

Confirm 阶段也可以看成是对 Try 阶段的一个补充,Try+Confirm 一起组成了一个完整的业务逻辑。

Cancel:当 Try 阶段存在服务执行失败, 进入 Cancel 阶段,业务如下图:

Try-Cancel

Cancel 取消执行,释放 Try 阶段预留的业务资源,上面的例子中,Cancel 操作会把冻结的库存释放,并更新订单状态为取消。

最终一致性保证

  • TCC 事务机制以初步操作(Try)为中心的,确认操作(Confirm)和取消操作(Cancel)都是围绕初步操作(Try)而展开。因此,Try 阶段中的操作,其保障性是最好的,即使失败,仍然有取消操作(Cancel)可以将其执行结果撤销。
  • Try阶段执行成功并开始执行 Confirm阶段时,默认 Confirm阶段是不会出错的。也就是说只要Try成功,Confirm一定成功(TCC设计之初的定义) 。
  • Confirm与Cancel如果失败,由TCC框架进行==重试==补偿
  • 存在极低概率在CC环节彻底失败,则需要定时任务或人工介入

方案总结

TCC 事务机制相对于传统事务机制(X/Open XA),TCC 事务机制相比于上面介绍的 XA 事务机制,有以下优点:

  • 性能提升:具体业务来实现控制资源锁的粒度变小,不会锁定整个资源。
  • 数据最终一致性:基于 Confirm 和 Cancel 的幂等性,保证事务最终完成确认或者取消,保证数据的一致性。
  • 可靠性:解决了 XA 协议的协调者单点故障问题,由主业务方发起并控制整个业务活动,业务活动管理器也变成多点,引入集群。

缺点:

  • TCC 的 Try、Confirm 和 Cancel 操作功能要按具体业务来实现,业务耦合度较高,提高了开发成本。

本地消息表

本地消息表的方案最初是由 eBay 提出,核心思路是将分布式事务拆分成本地事务进行处理。

角色:

  • 事务主动方
  • 事务被动方

通过在事务主动发起方额外新建事务消息表,事务发起方处理业务和记录事务消息在本地事务中完成,轮询事务消息表的数据发送事务消息,事务被动方基于消息中间件消费事务消息表中的事务。

这样可以避免以下两种情况导致的数据不一致性:

  • 业务处理成功、事务消息发送失败
  • 业务处理失败、事务消息发送成功

整体的流程如下图:

本地消息表

上图中整体的处理步骤如下:

  • ①:事务主动方在同一个本地事务中处理业务和写消息表操作
  • ②:事务主动方通过消息中间件,通知事务被动方处理事务通知事务待消息。消息中间件可以基于 Kafka、RocketMQ 消息队列,事务主动方主动写消息到消息队列,事务消费方消费并处理消息队列中的消息。
  • ③:事务被动方通过消息中间件,通知事务主动方事务已处理的消息。
  • ④:事务主动方接收中间件的消息,更新消息表的状态为已处理。

一些必要的容错处理如下:

  • 当①处理出错,由于还在事务主动方的本地事务中,直接回滚即可
  • 当②、③处理出错,由于事务主动方本地保存了消息,只需要轮询消息重新通过消息中间件发送,事务被动方重新读取消息处理业务即可。
  • 如果是业务上处理失败,事务被动方可以发消息给事务主动方回滚事务
  • 如果事务被动方已经消费了消息,事务主动方需要回滚事务的话,需要发消息通知事务主动方进行回滚事务。

优点

  • 从应用设计开发的角度实现了消息数据的可靠性,消息数据的可靠性不依赖于消息中间件,弱化了对 MQ 中间件特性的依赖。
  • 方案轻量,容易实现。

缺点

  • 与具体的业务场景绑定,耦合性强,不可公用。
  • 消息数据与业务数据同库,占用业务系统资源。
  • 业务系统在使用关系型数据库的情况下,消息服务性能会受到关系型数据库并发性能的局限。

MQ事务方案(可靠消息事务)

基于 MQ 的分布式事务方案其实是对本地消息表的封装,将本地消息表基于 MQ 内部,其他方面的协议基本与本地消息表一致。

MQ事务方案整体流程和本地消息表的流程很相似,如下图:

MQ事务方案

从上图可以看出和本地消息表方案唯一不同就是将本地消息表存在了MQ内部,而不是业务数据库中。

那么MQ内部的处理尤为重要,下面主要基于 RocketMQ 4.3 之后的版本介绍 MQ 的分布式事务方案。

在本地消息表方案中,保证事务主动方发写业务表数据和写消息表数据的一致性是基于数据库事务,RocketMQ 的事务消息相对于普通 MQ提供了 2PC 的提交接口,方案如下:

正常情况:事务主动方发消息

事务主动方发消息

这种情况下,事务主动方服务正常,没有发生故障,发消息流程如下:

  • 步骤①:发送方向 MQ 服务端(MQ Server)发送 half 消息。
  • 步骤②:MQ Server 将消息持久化成功之后,向发送方 ack 确认消息已经发送成功。
  • 步骤③:发送方开始执行本地事务逻辑。
  • 步骤④:发送方根据本地事务执行结果向 MQ Server 提交二次确认(commit 或是 rollback)。
  • 步骤⑤:MQ Server 收到 commit 状态则将半消息标记为可投递,订阅方最终将收到该消息;MQ Server 收到 rollback 状态则删除半消息,订阅方将不会接受该消息。

异常情况:事务主动方消息恢复

事务主动方消息恢复

在断网或者应用重启等异常情况下,图中 4 提交的二次确认超时未到达 MQ Server,此时处理逻辑如下:

  • 步骤⑤:MQ Server 对该消息发起消息回查。
  • 步骤⑥:发送方收到消息回查后,需要检查对应消息的本地事务执行的最终结果。
  • 步骤⑦:发送方根据检查得到的本地事务的最终状态再次提交二次确认。
  • 步骤⑧:MQ Server基于 commit/rollback 对消息进行投递或者删除。

优点

相比本地消息表方案,MQ 事务方案优点是:

  • 消息数据独立存储 ,降低业务系统与消息系统之间的耦合。
  • 吞吐量大于使用本地消息表方案。

缺点

  • 一次消息发送需要两次网络请求(half 消息 + commit/rollback 消息) 。
  • 业务处理服务需要实现消息状态回查接口。

最大努力通知

最大努力通知也称为定期校对,是对MQ事务方案的进一步优化。它在事务主动方增加了消息校对的接口,如果事务被动方没有接收到消息,此时可以调用事务主动方提供的消息校对的接口主动获取。

最大努力通知的整体流程如下图:

最大努力通知

在可靠消息事务中,事务主动方需要将消息发送出去,并且消息接收方成功接收,这种可靠性发送是由事务主动方保证的;

但是最大努力通知,事务主动方尽最大努力(重试,轮询….)将事务发送给事务接收方,但是仍然存在消息接收不到,此时需要事务被动方主动调用事务主动方的消息校对接口查询业务消息并消费,这种通知的可靠性是由事务被动方保证的。

最大努力通知适用于业务通知类型,例如微信交易的结果,就是通过最大努力通知方式通知各个商户,既有回调通知,也有交易查询接口。

Saga 事务

Saga 事务源于 1987 年普林斯顿大学的 Hecto 和 Kenneth 发表的如何处理 long lived transaction(长活事务)论文。

Saga 事务核心思想是将长事务拆分为多个本地短事务,由 Saga 事务协调器协调,如果正常结束那就正常完成,如果某个步骤失败,则根据相反顺序一次调用补偿操作。

Saga 事务基本协议如下:

  • 每个 Saga 事务由一系列幂等的有序子事务(sub-transaction) Ti 组成。
  • 每个 Ti 都有对应的幂等补偿动作 Ci,补偿动作用于撤销 Ti 造成的结果。

TCC事务补偿机制有一个预留(Try)动作,相当于先报存一个草稿,然后才提交;Saga事务没有预留动作,直接提交。

对于事务异常,Saga提供了两种恢复策略,分别如下:

向后恢复(backward recovery)

在执行事务失败时,补偿所有已完成的事务,是“一退到底”的方式。如下图:

向后恢复

从上图可知事务执行到了支付事务T3,但是失败了,因此事务回滚需要从C3,C2,C1依次进行回滚补偿。

对应的执行顺序为:T1,T2,T3,C3,C2,C1

这种做法的效果是撤销掉之前所有成功的子事务,使得整个 Saga 的执行结果撤销。

向前恢复(forward recovery)

也称之为:勇往直前,对于执行不通过的事务,会尝试重试事务,这里有一个假设就是每个子事务最终都会成功。

流程如下图:

向前恢复

适用于必须要成功的场景,事务失败了重试,不需要补偿。

Saga事务有两种不同的实现方式,分别如下:

  • 命令协调(Order Orchestrator)
  • 事件编排(Event Choreographyo)

命令协调

中央协调器(Orchestrator,简称 OSO)以命令/回复的方式与每项服务进行通信,全权负责告诉每个参与者该做什么以及什么时候该做什么。整体流程如下图:

命令协调

上图步骤如下:

  • 事务发起方的主业务逻辑请求 OSO 服务开启订单事务
  • OSO 向库存服务请求扣减库存,库存服务回复处理结果。
  • OSO 向订单服务请求创建订单,订单服务回复创建结果。
  • OSO 向支付服务请求支付,支付服务回复处理结果。
  • 主业务逻辑接收并处理 OSO 事务处理结果回复。

中央协调器必须事先知道执行整个订单事务所需的流程(例如通过读取配置)。如果有任何失败,它还负责通过向每个参与者发送命令来撤销之前的操作来协调分布式的回滚。

基于中央协调器协调一切时,回滚要容易得多,因为协调器默认是执行正向流程,回滚时只要执行反向流程即可。

事件编排

没有中央协调器(没有单点风险)时,每个服务产生并观察其他服务的事件,并决定是否应采取行动。

在事件编排方法中,第一个服务执行一个事务,然后发布一个事件。该事件被一个或多个服务进行监听,这些服务再执行本地事务并发布(或不发布)新的事件。

当最后一个服务执行本地事务并且不发布任何事件时,意味着分布式事务结束,或者它发布的事件没有被任何 Saga 参与者听到都意味着事务结束。

事件编排

上图步骤如下:

  • 事务发起方的主业务逻辑发布开始订单事件。
  • 库存服务监听开始订单事件,扣减库存,并发布库存已扣减事件。
  • 订单服务监听库存已扣减事件,创建订单,并发布订单已创建事件。
  • 支付服务监听订单已创建事件,进行支付,并发布订单已支付事件。
  • 主业务逻辑监听订单已支付事件并处理。

事件/编排是实现 Saga 模式的自然方式,它很简单,容易理解,不需要太多的代码来构建。如果事务涉及 2 至 4 个步骤,则可能是非常合适的。

优点

命令协调设计的优点如下:

  • 服务之间关系简单,避免服务之间的循环依赖关系,因为 Saga 协调器会调用 Saga 参与者,但参与者不会调用协调器。
  • 程序开发简单,只需要执行命令/回复(其实回复消息也是一种事件消息),降低参与者的复杂性。
  • 易维护扩展,在添加新步骤时,事务复杂性保持线性,回滚更容易管理,更容易实施和测试。

事件/编排设计优点如下:

  • 避免中央协调器单点故障风险。
  • 当涉及的步骤较少服务开发简单,容易实现。

缺点

命令协调设计缺点如下:

  • 中央协调器容易处理逻辑容易过于复杂,导致难以维护。
  • 存在协调器单点故障风险。

事件/编排设计缺点如下:

  • 服务之间存在循环依赖的风险。
  • 当涉及的步骤较多,服务间关系混乱,难以追踪调测。

由于 Saga 模型中没有 Prepare 阶段,因此事务间不能保证隔离性。

当多个 Saga 事务操作同一资源时,就会产生更新丢失、脏数据读取等问题,这时需要在业务层控制并发,例如:在应用层面加锁,或者应用层面预先冻结资源。

总结

总结一下各个方案的常见的使用场景:

  • 2PC/3PC:依赖于数据库,能够很好的提供强一致性和强事务性,但相对来说延迟比较高,比较适合传统的单体应用,在同一个方法中存在跨库操作的情况,不适合高并发和高性能要求的场景。
  • TCC:适用于执行时间确定且较短,实时性要求高,对数据一致性要求高,比如互联网金融企业最核心的三个服务:交易、支付、账务。
  • 本地消息表/MQ 事务:都适用于事务中参与方支持操作幂等,对一致性要求不高,业务上能容忍数据不一致到一个人工检查周期,事务涉及的参与方、参与环节较少,业务上有对账/校验系统兜底。
  • Saga 事务:由于 Saga 事务不能保证隔离性,需要在业务层控制并发,适合于业务场景事务并发操作同一资源较少的情况。 Saga 相比缺少预提交动作,导致补偿动作的实现比较麻烦,例如业务是发送短信,补偿动作则得再发送一次短信说明撤销,用户体验比较差。Saga 事务较适用于补偿动作容易处理的场景。

什么是Seata?

上面讲了这么多的分布式事务的理论知识,都没看到一个落地的实现,这不是吹牛逼吗?

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

  • 对业务无侵入:即减少技术架构上的微服务化所带来的分布式事务问题对业务的侵入
  • 高性能:减少分布式事务解决方案所带来的性能消耗

官方文档:seata.io/zh-cn/index…

seata的几种术语:

  • TC(Transaction Coordinator):事务协调者。管理全局的分支事务的状态,用于全局性事务的提交和回滚。
  • TM(Transaction Manager):事务管理者。用于开启、提交或回滚事务。
  • RM(Resource Manager):资源管理器。用于分支事务上的资源管理,向 TC 注册分支事务,上报分支事务的状态,接收 TC 的命令来提交或者回滚分支事务。

AT模式

seata目前支持多种事务模式,分别有AT、TCC、SAGA 和 XA ,文章篇幅有限,今天只讲常用的AT模式。

AT模式的特点就是对业务无入侵式,整体机制分二阶段提交(2PC)

  • 一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。
  • 二阶段:
    1. 提交异步化,非常快速地完成
    2. 回滚通过一阶段的回滚日志进行反向补偿。

在 AT 模式下,用户只需关注自己的业务SQL,用户的业务SQL 作为一阶段,Seata 框架会自动生成事务的二阶段提交和回滚操作。

一个典型的分布式事务过程:

  • TM 向 TC 申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的 XID;
  • XID 在微服务调用链路的上下文中传播;
  • RM 向 TC 注册分支事务,将其纳入 XID 对应全局事务的管辖;
  • TM 向 TC 发起针对 XID 的全局提交或回滚决议;
  • TC 调度 XID 下管辖的全部分支事务完成提交或回滚请求。

搭建Seata TC协调者

seata的协调者其实就是阿里开源的一个服务,我们只需要下载并且启动它。

下载地址:seata.io/zh-cn/blog/…

陈某下载的版本是1.3.0 ,各位最好和我版本一致,这样不会出现莫名的BUG。

下载完成后,直接解压即可。但是此时还不能直接运行,还需要做一些配置。

创建TC所需要的表

TC运行需要将事务的信息保存在数据库,因此需要创建一些表,找到seata-1.3.0源码的script\server\db这个目录,将会看到以下SQL文件:

陈某使用的是Mysql数据库,因此直接运行mysql.sql这个文件中的sql语句,创建的三张表如下图:

修改TC的注册中心

找到seata-server-1.3.0\seata\conf这个目录,其中有一个registry.conf文件,其中配置了TC的注册中心和配置中心。

默认的注册中心是file形式,实际使用中肯定不能使用,需要改成Nacos形式,改动的地方如下图:

需要改动的地方如下:

  • type:改成nacos,表示使用nacos作为注册中心
  • application:服务的名称
  • serverAddr:nacos的地址
  • group:分组
  • namespace:命名空间
  • username:用户名
  • password:密码

最后这份文件都会放在项目源码的根目录下,源码下载方式见文末

修改TC的配置中心

TC的配置中心默认使用的也是file形式,当然要是用nacos作为配置中心了。

直接修改registry.conf文件,需要改动的地方如下图:

需要改动的地方如下:

  • type:改成nacos,表示使用nacos作为配置中心
  • serverAddr:nacos的地址
  • group:分组
  • namespace:命名空间
  • username:用户名
  • password:密码

上述配置修改好之后,在TC启动的时候将会自动读取nacos的配置。

那么问题来了:TC需要存储到Nacos中的配置都哪些,如何推送过去?

在seata-1.3.0\script\config-center中有一个config.txt文件,其中就是TC所需要的全部配置。

在seata-1.3.0\script\config-center\nacos中有一个脚本nacos-config.sh则是将config.txt中的全部配置自动推送到nacos中,运行下面命令(windows可以使用git bash运行):

1
2
shell复制代码# -h 主机,你可以使用localhost,-p 端口号 你可以使用8848,-t 命名空间ID,-u 用户名,-p 密码
$ sh nacos-config.sh -h 127.0.0.1 -p 8080 -g SEATA_GROUP -t 7a7581ef-433d-46f3-93f9-5fdc18239c65 -u nacos -w nacos

推送成功则可以在Nacos中查询到所有的配置,如下图:

修改TC的数据库连接信息

TC是需要使用数据库存储事务信息的,那么如何修改相关配置呢?

上一节的内容已经将所有的配置信息都推送到了Nacos中,TC启动时会从Nacos中读取,因此我们修改也需要在Nacos中修改。

需要修改的配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
properties复制代码## 采用db的存储形式
store.mode=db
## druid数据源
store.db.datasource=druid
## mysql数据库
store.db.dbType=mysql
## mysql驱动
store.db.driverClassName=com.mysql.jdbc.Driver
## TC的数据库url
store.db.url=jdbc:mysql://127.0.0.1:3306/seata_server?useUnicode=true
## 用户名
store.db.user=root
## 密码
store.db.password=Nov2014

在nacos中搜索上述的配置,直接修改其中的值,比如修改store.mode,如下图:

当然Seata还支持Redis作为TC的数据库,只需要改动以下配置即可:

1
2
3
4
properties复制代码store.mode=redis
store.redis.host=127.0.0.1
store.redis.port=6379
store.redis.password=123456

启动TC

按照上述步骤全部配置成功后,则可以启动TC,在seata-server-1.3.0\seata\bin目录下直接点击seata-server.bat(windows)运行。

启动成功后,在Nacos的服务列表中则可以看到TC已经注册进入,如下图:

至此,Seata的TC就启动完成了…………

Seata客户端搭建(RM)

上述已经将Seata的服务端(TC)搭建完成了,下面就以电商系统为例介绍一下如何编码实现分布式事务。

用户购买商品的业务逻辑。整个业务逻辑由3个微服务提供支持:

  • 仓储服务:对给定的商品扣除仓储数量。
  • 订单服务:根据采购需求创建订单。
  • 帐户服务:从用户帐户中扣除余额。

需要了解的知识:Nacos和openFeign,有不清楚的可以看我的前两章教程,如下:

  • 五十五张图告诉你微服务的灵魂摆渡者Nacos究竟有多强?
  • openFeign夺命连环9问,这谁受得了?

仓储服务搭建

陈某整个教程使用的都是同一个聚合项目,关于Spring Cloud版本有不清楚的可以看我第一篇文章的说明。

添加依赖

新建一个seata-storage9020项目,新增依赖如下:

由于使用的springCloud Alibaba依赖版本是2.2.1.RELEASE,其中自带的seata版本是1.1.0,但是我们Seata服务端使用的版本是1.3.0,因此需要排除原有的依赖,重新添加1.3.0的依赖。

注意:seata客户端的依赖版本必须要和服务端一致。

创建数据库

创建一个数据库seata-storage,其中新建两个表:

  • storage:库存的业务表,SQL如下:
1
2
3
4
5
6
7
8
9
10
sql复制代码CREATE TABLE `storage`  (
`id` bigint(11) NOT NULL AUTO_INCREMENT,
`name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
`num` bigint(11) NULL DEFAULT NULL COMMENT '数量',
`create_time` datetime(0) NULL DEFAULT NULL,
`price` bigint(10) NULL DEFAULT NULL COMMENT '单价,单位分',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Compact;

INSERT INTO `storage` VALUES (1, '码猿技术专栏', 1000, '2021-10-15 22:32:40', 100);
  • undo_log:回滚日志表,这是Seata要求必须有的,每个业务库都应该创建一个,SQL如下:
1
2
3
4
5
6
7
8
9
10
sql复制代码CREATE TABLE `undo_log`  (
`branch_id` bigint(20) NOT NULL COMMENT 'branch transaction id',
`xid` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT 'global transaction id',
`context` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT 'undo_log context,such as serialization',
`rollback_info` longblob NOT NULL COMMENT 'rollback info',
`log_status` int(11) NOT NULL COMMENT '0:normal status,1:defense status',
`log_created` datetime(6) NOT NULL COMMENT 'create datetime',
`log_modified` datetime(6) NOT NULL COMMENT 'modify datetime',
UNIQUE INDEX `ux_undo_log`(`xid`, `branch_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = 'AT transaction mode undo table' ROW_FORMAT = Compact;

配置seata相关配置

对于Nacos、Mysql数据源等相关信息就省略了,项目源码中都有。主要讲一下seata如何配置,详细配置如下:

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
yaml复制代码spring:
application:
## 指定服务名称,在nacos中的名字
name: seata-storage
## 客户端seata的相关配置
seata:
## 是否开启seata,默认true
enabled: true
application-id: ${spring.application.name}
## seata事务组的名称,一定要和config.tx(nacos)中配置的相同
tx-service-group: ${spring.application.name}-tx-group
## 配置中心的配置
config:
## 使用类型nacos
type: nacos
## nacos作为配置中心的相关配置,需要和server在同一个注册中心下
nacos:
## 命名空间,需要server端(registry和config)、nacos配置client端(registry和config)保持一致
namespace: 7a7581ef-433d-46f3-93f9-5fdc18239c65
## 地址
server-addr: localhost:8848
## 组, 需要server端(registry和config)、nacos配置client端(registry和config)保持一致
group: SEATA_GROUP
## 用户名和密码
username: nacos
password: nacos
registry:
type: nacos
nacos:
## 这里的名字一定要和seata服务端中的名称相同,默认是seata-server
application: seata-server
## 需要server端(registry和config)、nacos配置client端(registry和config)保持一致
group: SEATA_GROUP
namespace: 7a7581ef-433d-46f3-93f9-5fdc18239c65
username: nacos
password: nacos
server-addr: localhost:8848

以上配置注释已经很清楚,这里着重强调以下几点:

  • 客户端seata中的nacos相关配置要和服务端相同,比如地址、命名空间……….
  • tx-service-group:这个属性一定要注意,这个一定要和服务端的配置一致,否则不生效;比如上述配置中的,就要在nacos中新增一个配置service.vgroupMapping.seata-storage-tx-group=default,如下图:

注意:seata-storage-tx-group仅仅是后缀,要记得添加配置的时候要加上前缀service.vgroupMapping.

扣减库存的接口

逻辑很简单,这里仅仅是做了减库存的操作,代码如下:

这里的接口并没有不同,还是使用@Transactional开启了本地事务,并没有涉及到分布式事务。

到这里仓储服务搭建好了…………..

账户服务搭建

搭建完了仓储服务,账户服务搭建很类似了。

添加依赖

新建一个seata-account9021服务,这里的依赖和仓储服务的依赖相同,直接复制

创建数据库

创建一个seata-account数据库,其中新建了两个表:

  • account:账户业务表,SQL如下:
1
2
3
4
5
6
7
8
9
sql复制代码CREATE TABLE `account`  (
`id` bigint(11) NOT NULL,
`user_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '用户userId',
`money` bigint(11) NULL DEFAULT NULL COMMENT '余额,单位分',
`create_time` datetime(0) NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Compact;

INSERT INTO `account` VALUES (1, 'abc123', 1000, '2021-10-19 17:49:53');
  • undo_log:回滚日志表,同仓储服务

配置seata相关配置

Seata相关配置和仓储服务相同,只不过需要在nacos中添加一个service.vgroupMapping.seata-account-tx-group=default,如下图:

扣减余额的接口

具体逻辑自己完善,这里我直接扣减余额,代码如下:

依然没有涉及到分布式事务,还是使用@Transactional开启了本地事务,是不是很爽…………

订单服务搭建(TM)

这里为了节省篇幅,陈某直接使用订单服务作为TM,下单、减库存、扣款整个流程都在订单服务中实现。

添加依赖

新建一个seata-order9022服务,这里需要添加的依赖如下:

  • Nacos服务发现的依赖
  • seata的依赖
  • openFeign的依赖,由于要调用账户、仓储的微服务,因此需要额外添加一个openFeign的依赖

创建数据库

新建一个seata_order数据库,其中新建两个表,如下:

  • t_order:订单的业务表
1
2
3
4
5
6
7
8
9
sql复制代码CREATE TABLE `t_order`  (
`id` bigint(11) NOT NULL AUTO_INCREMENT,
`product_id` bigint(11) NULL DEFAULT NULL COMMENT '商品Id',
`num` bigint(11) NULL DEFAULT NULL COMMENT '数量',
`user_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '用户唯一Id',
`create_time` datetime(0) NULL DEFAULT NULL,
`status` int(1) NULL DEFAULT NULL COMMENT '订单状态 1 未付款 2 已付款 3 已完成',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 7 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Compact;
  • undo_log:回滚日志表,同仓储服务

配置和seata相关配置

Seata相关配置和仓储服务相同,只不过需要在nacos中添加一个service.vgroupMapping.seata-order-tx-group=default,如下图:

扣减库存的接口

这里需要通过openFeign调用仓储服务的接口进行扣减库存,接口如下:

以上只是简单的通过openFeign调用,更细致的配置,比如降级,自己完善………

扣减余额的接口

这里仍然是通过openFeign调用账户服务的接口进行扣减余额,接口如下:

创建订单的接口

下订单的接口就是一个事务发起方,作为TM,需要发起一个全局事务,详细代码如下图:

有什么不同?不同之处就是使用了@GlobalTransactional而不是@Transactional。

@GlobalTransactional是Seata提供的,用于开启才能全局事务,只在TM中标注即可生效。

测试

分别启动seata-account9021、seata-storage9020、seata-order9022,如下图:

下面调用下单接口,如下图:

从控制台输出的日志可以看出,流程未出现任何异常,事务已经提交,如下图:

果然,查看订单、余额、库存表,数据也都是正确的。

但是,这仅仅是流程没问题,并不能说明分布式事务已经配置成功了,因此需要手动造个异常。

在扣减余额的接口睡眠2秒钟,因为openFeign的超时时间默认是1秒,这样肯定是超时异常了,如下图:

此时,调用创建订单的接口,控制台日志输出如下图:

发现在扣减余额处理中超时了,导致了异常…….

此时,看下库存的数据有没有扣减,很高兴,库存没有扣减成功,说明事务已经回滚了,分布式事务成功了。

总结

Seata客户端创建很简单,需要注意以下几点内容:

  • seata客户端的版本需要和服务端保持一致
  • 每个服务的数据库都要创建一个undo_log回滚日志表
  • 客户端指定的事务分组名称要和Nacos相同,比如service.vgroupMapping.seata-account-tx-group=default
    • 前缀:service.vgroupMapping.
    • 后缀:{自定义}

项目源码已经上传,关注公众号码猿技术专栏回复关键词9528获取!

AT模式原理分析

AT模式最大的优点就是对业务代码无侵入,一切都像在写单体业务逻辑一样。

TC相关的三张表:

  • global_table:全局事务表,每当有一个全局事务发起后,就会在该表中记录全局事务的ID
  • branch_table:分支事务表,记录每一个分支事务的ID,分支事务操作的哪个数据库等信息
  • lock_table:全局锁

一阶段步骤

  1. TM:seata-order.create()方法执行时,由于该方法具有@GlobalTranscational标志,该TM会向TC发起全局事务,生成XID(全局锁)
  2. RM:StorageService.deduct():写表,UNDO_LOG记录回滚日志(Branch ID),通知TC操作结果
  3. RM:AccountService.deduct():写表,UNDO_LOG记录回滚日志(Branch ID),通知TC操作结果
  4. RM:OrderService.create():写表,UNDO_LOG记录回滚日志(Branch ID),通知TC操作结果

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”,如果两份数据完全一致就说明没有脏写,可以还原业务数据,如果不一致就说明有脏写,出现脏写就需要转人工处理。

AT 模式的一阶段、二阶段提交和回滚均由 Seata 框架自动生成,用户只需编写业务 SQL,便能轻松接入分布式事务,AT 模式是一种对业务无任何侵入的分布式事务解决方案。

总结

本文介绍了七种分布式事务解决方案,以及阿里开源的Seata,从入门到实现,文中如有错误之处,欢迎留言指正。

本文只介绍了Seata的AT模式,其实Seata还支持TCC、Saga事务模式,关于这一部分内容和Seata源码分析会在下期文章中介绍。

作者码字不易,在看、收藏、转发一波,谢谢支持!

案例源码已经上传,关注公号【码猿技术专栏】,回复关键词【9528】获取!

本文转载自: 掘金

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

三分钟极速体验:Java版人脸检测

发表于 2021-10-25

欢迎访问我的GitHub

github.com/zq2599/blog…

内容:所有原创文章分类汇总及配套源码,涉及Java、Docker、Kubernetes、DevOPS等;

本篇概览

  • 检测照片中的人脸,用Java可以实现吗?
  • 当然可以,今天咱们用最少的时间、最简单的操作来体验这个实用的功能,当您提交一张带有人脸的照片后,会看到下图效果,所有人脸都被识别到并被框选出来了:

在这里插入图片描述

  • 本篇以体验为主,不涉及具体的开发,后面还会有文章介绍完整的开发过程(包括源码)

风险提前告知

  • 为了简化操作,接下来会用到docker,对应的镜像体积巨大,达到了恐怖的4.69G,建议您为自己的docker做好加速配置,可以减少下载等待时间;
  • 由于opencv体积庞大,再加上javacv的依赖库也不小,这才导致超大镜像的出现,还望您多多海涵,标题中的《三分钟极速体验》是要去掉镜像的等待时间的,您要是觉得欣宸的标题起得很无耻,我觉得您是对的…

环境信息

  • 为了简化体验过程,接下来会用到docker,推荐的环境信息如下:
  • 操作系统:Ubuntu 16.04.1 LTS 服务器版(MacBook Pro也可以,版本是11.2.3,macOS Big Sur)
  • docker:20.10.2 Community
  • 为了加快docker镜像的下载速度,建议您提前做好docker加速配置
  • 文章标题号称三分钟极速体验,没时间说太多,准备好环境就火速动手啦

部署

  • 新建名为images的目录,用于存储处理后的文件,我这里完整路径是/root/temp/202107/17/images
  • 新建名为model的目录,用于存储稍后要下载的模型文件,我这里完整路径是/root/temp/202107/17/model
  • 下载训练好的模型文件,我准备了两个下载地址,您任选一个即可,一个是csdn的(无需积分):
  1. download.csdn.net/download/bo…
  2. raw.githubusercontent.com/zq2599/blog…
  • 上述文件下载下来是个压缩包,请先解压,再将文件haarcascade_frontalface_default.xml放入model目录(model里放的必须是解压后的文件)
  • 执行以下命令,会先下载docker镜像文件再创建容器:
1
2
3
4
5
6
shell复制代码docker run \
--rm \
-p 18080:8080 \
-v /root/temp/202107/17/images:/app/images \
-v /root/temp/202107/17/model:/app/model \
bolingcavalry/facedetect:0.0.1
  • 部署完成,开始体验

体验

  • 浏览器访问http://localhost:18080,这里的localhost请改成docker宿主机IP(要关闭防火墙!),可以见到操作页面,如下图(欣宸的前端开发水平渣到令人发指,果然不是空穴来风):

在这里插入图片描述

  • 找一张有人脸的图片(我在百度图片随机找的),点击上图的选取图片按钮进行上传,至于周围检测数量那里先保持默认值32不要动
  • 点击提交按钮后,页面会显示检测结果,如下图,人脸被准确的框选出来了:

在这里插入图片描述

  • 再试试多人的,如下图,居然一个人脸都没有检测到:

在这里插入图片描述

  • 把周围检测数量的值调低些,改成4再试,如下图,这次成功了,八张人脸全部检测到:

在这里插入图片描述

  • 至此,Java版人脸检测的体验已经完成,一分钟概览,一分钟部署,一分钟体验,咱们足够高效(下载超大镜像的时间不能算,不敢算…)
  • 此刻您应该能感受到Java在人脸识别领域的魅力了,聪明的您当然会有很多疑问,例如:
  1. 用了啥框架?
  2. 写了啥代码?
  3. 运行环境好不好配置?只要jar依赖吗?还需要其他操作吗?
  • 这些疑问在下面这两篇文章中完全揭秘,然后您也能轻易做出集成了人脸识别的SpringBoot应用了:
  1. Java版人脸检测详解上篇:运行环境的Docker镜像(CentOS+JDK+OpenCV)
  2. Java版人脸检测详解下篇:编码
  • 顺便剧透一下:设置运行环境很麻烦,所幸欣宸已解决此问题,并成功封装为基础镜像,咱们可以专注的开发应用,对运行环境再也无需处理了

你不孤单,欣宸原创一路相伴

  1. Java系列
  2. Spring系列
  3. Docker系列
  4. kubernetes系列
  5. 数据库+中间件系列
  6. DevOps系列

欢迎关注公众号:程序员欣宸

微信搜索「程序员欣宸」,我是欣宸,期待与您一同畅游Java世界…
github.com/zq2599/blog…

本文转载自: 掘金

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

程序员必备小知识系列--个人网站功能开发与性能优化经历(2)

发表于 2021-10-25

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

自己搭建了一个基于SpringBoot+Spring Security+MyBatis+MySQL+Redis+Thymeleaf的博客网站。上线个人云服务器后,发现服务器访问慢。个人服务器是1核2G的,1M宽带,虽然服务器是低配的,但是可以通过优化代码,中间件等手段,来提升性能。我会讲解个人网站功能的开发与一些性能优化的经历。

这篇主要讲CDN文章缓存

众所周知,一个网站,打开速度的快慢会影响用户的使用感觉,如果一个网站打开的越慢,越多的用户会觉得不耐烦。而影响网站打开的速度,其中占很大的一个层面,那么就是前端。
在这里插入图片描述

前端优化的方法有很多:

一、CDN

摘自百度百科

1
xml复制代码CDN的全称是Content Delivery Network,即内容分发网络。CDN是构建在现有网络基础之上的智能虚拟网络,依靠部署在各地的边缘服务器,通过中心平台的负载均衡、内容分发、调度等功能模块,使用户就近获取所需内容,降低网络拥塞,提高用户访问响应速度和命中率。CDN的关键技术主要有内容存储和分发技术。

可以使用CDN提高网站的访问速度,另外也可以缓解服务器的压力。
最常用的CDN网站:www.bootcdn.cn/
可以使用这里的前端文件代替自己本地的文件,也就是说不用访问自己本地静态资源,访问其他网站的静态资源,减轻自己服务器前端加载的速度
在这里插入图片描述

1
2
html复制代码<script src="js/lightgallery/lightgallery.js"></script>
<script src="https://cdn.staticfile.org/lightgallery-js/0.0.2/js/lightgallery.js"></script>

上面的是自己本地的静态文件,改成,CDN的地址,这样自己网站加载前端资源的话,就直接加载CDN里的静态文件了。

二、使用百度云,阿里云,腾讯云等等的CDN进行加速

console.cloud.tencent.com/cdn/domains
我这里用的是腾讯云在这里插入图片描述

console.dnspod.cn/dns/xuluowu…
在这里插入图片描述
具体操作指南看这里:官方文档已经提供了cloud.tencent.com/document/pr…

三、前端代码进行优化

1、js,script代码尽量写在文件末端,写在前面的话,加载会提前加载这些js,script代码,也会延迟时间
在这里插入图片描述
引入的js,css等文件都放在文件末端了

1
2
3
4
5
6
7
8
9
10
11
12
html复制代码<script src="js/all.js"></script>
<script src="js/show.js"></script>
<script src="js/comment.js"></script>
<!--放大图片插件-->
<script src="js/lightgallery/picturefill.min.js"></script>
<script src="js/lightgallery/lg-pager.js"></script>
<script src="js/lightgallery/lg-autoplay.js"></script>
<script src="js/lightgallery/lg-fullscreen.js"></script>
<script src="js/lightgallery/lg-zoom.js"></script>
<!--分享功能插件-->
<link href="https://cdn.bootcss.com/social-share.js/1.0.16/css/share.min.css" rel="stylesheet">
<script src="https://cdn.bootcss.com/social-share.js/1.0.16/js/social-share.min.js"></script>

2、如果大部分页面都有要加载相同的东西,比如scirpt引入的东西,可以把这些相同的抽出来一个公共页面,其他页面想要用的,引入这个页面即可。
比如底部页面,很多页面都会有的样式,抽出来,一起引用。
在这里插入图片描述

本文转载自: 掘金

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

如果你要去创业公司做CTO

发表于 2021-10-25

不少朋友在大公司工作了一些年后,会收到创业公司做CTO的offer,心里诚惶诚恐。一方面工作进入了一种缓慢重复的节奏,失去了激情,另一方面也同时在担心自己能不能适应all in的创业的要求。

纠结之下,在决定去创业公司之前,请一定要提前调整好以下几个思维认知:

1、关注创始人基因和格局

对于技术人来说,选择创业公司都会首先去关注创业项目、会关注行业、会关注技术、会关注工程师文化、会关注自我实现等等。但常常忽略掉创始人的基因和格局,而这通常决定了以后的创业道路是hard模式还是地狱模式(不好意思,创业没有easy模式)。

有些资源型的创始人,他对于CTO的期望就在于满足需求上线,在被告知研发难度以后,张口第一句话永远都是“为什么BAT可以?”;或者当需要向老板争取资源和开发时间进行优化及开发一些非功能性需求时,沟通成本就会无限提高。产品做出来之前你很重要,后期依靠运营和销售进入市场时,技术的作用就是确保不出问题,平时看不到价值,发现的时候就是宕机或者出问题的时候,好像每次赢得利润都是商务和运营的努力的结果。正如冯大辉所说的: “技术在短期内总会被高估,但长期内,总会被低估。”

画外音:有没有被挑战过?为什么BAT可以?

2、创业公司CTO是对人性和心力的考验

通常在大公司的时候,做好自己的技术领域就可以了,但到了小公司,能力要求掌握的越多越好,如果一无所知,木桶原理的短板会让自己和公司都很被动。我们常常说,创业公司的CTO,更大多数可能是顶着CTO的名头干着技术组长兼打杂的事情。

团队没有人能做的事情,就要自己去找资源来解决。从技术的角度总结,就是要学会在保证业务连续性的前提下,用有限的资源完成不断变化的需求。 这其中,核心的职责包括:

带团队。 没有大公司品牌光环的创业公司,项目永远忙不过来,招聘却永远处于被面试的状态。找不到90分的人才,70分的人不理解为什么公司没有年终奖,为什么过节连个礼品也不发。尤其是还不知道要主导团队文化的CTO,还没有建立现有人才的培养激励制度的团队,是件更可怕的事情。CTO 不应该只局限于技术,应该让团队成员的技术劳动体现出应有的价值,把技术和产品变成一家公司的发动机。

定战略。 我们希望CTO拥有强大的战略和思维,对行业格局、技术方向上有前瞻性,知道在什么时间点投入怎样的资源强化哪块技术,有优秀的商业/技术平衡能力,了解业务,用技术战略去支持公司战略。在创业公司做CTO,日常是需要救火的,但切莫沉浸在救火中,而忘了修炼。

拿结果。 相信从技术到管理的时候,很多人都已经意识到要以结果为导向,无论是技术方向的选择,还是技术实现和方式的选择,都要为最终的结果负责。除此之外,CTO跟所有人一样,都知道创业维艰,向死而生。但是却需要在大家为功能需求焦头烂额的时候,为了避免项目上线后死去,同时克服自己在沟通层面的不擅长,主动提出非功能性需求。这才是真正的对结果负责。

画外音:有没有怀疑过,自己到底是不是一个技术人?

3、永远记得自我修炼

技术人员最好的发展,应该分为四个阶段:学习、沉淀、分享和影响力。 除了技术知识的学习和了解,技术人的学习还应该包括前面无数次提到的,协调沟通能力,在面对不理解技术的各种O的时候,更是需要道与术的修炼。学习沉淀之后,还要把自己的东西价值最大化的分享和表达出来,用个人魅力,去主导团队文化,用自身影响力,去提升公司影响力。

学习是一件持续的事情,能力越高责任越大,CTO更要不断为自己充电,这是一个技术人最好的时代,同时也是对技术人要求更高的时代。

在这个时代,认知升级远比知识升级更重要。 能意识到这一点,在接受创业公司offer之前,在决定自己人生的一次all in之前,你可能需要系统化提升一下CTO应该具备的能力,不妨来体验一下 【CTO训练营】 的课程,听听CTO导师们对于CTO的理解,并与一帮志同道合的人,来探讨在工作中遇到的难题与困惑。

CTO训练营——技术管理者的MBA, 51CTO旗下面向技术管理者的平台之一,致力于为行业中技术领袖提供知识学习和价值成长,强调技术管理者的“技术性视野”和“商业性视野”,提升技术决策能力、商业性知识和领导能力,由业界资深技术高管、技术型创始人、投资人等共同参与到课程内容的设计及教授中,帮助中国最具潜力的技术管理者成为未来技术领域的领袖。

CTO训练营第五季,报名采用【申请审核】机制,欢迎扫码申请。

【我是福利】申请填表同学可免费获得CTO训练营往期【变革时代的自我进化论】模块的视频观看权,价值880元。\

点击【阅读原文】,一起重新定义CTO!

本文转载自: 掘金

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

系统化全方位监控告警,这一篇足矣

发表于 2021-10-25

Q1花了较大的篇幅,系统化讲述了监控与告警体系的建立,本文稍作总结。如果恰巧你正在搭建自动化监控与告警平台,或许,细读这一系列文章就够了。

一《监控告警,集群信息管理先行》

  • 什么是集群信息管理
  • 使用配置文件进行集群信息管理
  • 使用服务进行集群信息管理
  • 使用配置中心进行集群信息管理

二《监控告警,员工信息管理,分级告警策略》

  • 使用配置文件进行员工信息管理
  • 使用服务进行员工信息管理
  • 常见分级告警策略:告警收敛,分时告警,逐层上报,黑白跳动

三《多维度立体化监控》

  • 什么是多维度立体化监控
  • 立体化监控有哪些维度
  1. 操作系统+进程+端口
  2. http状态码
  3. 服务存活性
  4. 接口处理时间
  5. http接口
  6. log监控
  7. 用户视角
  • 如何快速实施“1.操作系统+进程+端口”的监控
  • 如何快速实施“2.http状态码”的监控
  • 如何快速实施“3.服务存活性”的监控
  • 如何快速实施“4.接口处理时间”的监控

四《一分钟搞定“http接口”监控框架》

  • 常见http监控玩法
  • 常见http监控的弊端
  • 通用可扩展http监控平台架构
  • 简版http监控框架架构
  • 100行伪代码搞定http监控

五《一分钟搞定“log”监控框架》

  • 什么是日志监控
  • 日志分级,错误日志监控,正常日志监控
  • 日志与目录规范
  • 集中式日志监控及实现方案
  • 分散式日志监控及实现方案
  • 90行伪代码搞定log监控

六《一分钟搞定“用户视角”监控》

  • 什么是用户视角的监控
  • 机房布点法
  • 端上报法
  • 第三方监控法

希望这六篇文章,对大家建立体系化的监控告警平台能有所帮助,告别背锅的时光。

调研一,下列场景大家是否经常遇到:

  • 用户和老板总是比研发先发现线上问题
  • 总是需要凌晨起床处理线上问题
  • 短信一响就异常紧张

调研二,下列技术体系大家是否已经具备:

  • 集群信息管理
  • 员工信息管理
  • 分级告警策略
  • 多维度立体化监控
  1. 操作系统+进程+端口
  2. http状态码
  3. 服务存活性
  4. 接口处理时间
  5. http接口
  6. log监控
  7. 用户视角

求帮转。

本文转载自: 掘金

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

探索原味 BFF 模式

发表于 2021-10-24

BFF — Backends For Frontends 。在后来的学习和工作经验累积中逐步的加深了对 BFF 的理解,这个模式它具有更加确切的一些使用场景以及解决的是一些更加特定的问题。本篇小作文中,从找到 BFF 的起源开始,回到 BFF 模式诞生的大背景下,去探索那个特殊环境下遇到了什么样的问题催生出了这个在微服务系统中出镜率极高的模式。

寻找源头

首先,从技术雷达(www.thoughtworks.com/radar/techn… BFF 的条目上,可以找到一些蛛丝马迹。条目的发布时间是 2015 年 11 月10 日。接着,我们在谷歌搜索关键字Backend for Frontend 以及将时间范围限定在 2015 年 1 月 1 日到 2015 年 11 月 10 日。

接下来,对比搜索结果的时间,我找到最早出现 Backend for Frontend 词条的文章(philcalcado.com/2015/09/18/…

文中提到,BFF 这个名字是被当时团队 TechLeader Nick Fisher(twitter.com/spadgos)提出的… BFF 模式是在 SoundCloud(zh.wikipedia.org/zh-hans/Sou… 首次出现。接着,让我们一起回到BFF 第一次发挥威力的现场吧。

问题初现

为了更有条理的了解当年SoundCloud 遇到了什么样的挑战,我通过分类分项的方式来列举情况和分析。

  • 背景:
+ SoundCloud([blog.sellfy.com/make-more-m…](https://blog.sellfy.com/make-more-money-on-soundcloud/%EF%BC%89)
主要是通过付费订阅与广告进行盈利(也就是说,越多的曝光渠道,会给SoundCloud 带来更多的盈利机会)
+ SoundCloud 是一个单体系统,通过暴露共享 API 的方式为 Web 客户端、Android 和 iOS 应用程序以及互联网、合作伙伴等提供服务。这些共享 API 随着功能和特性一起增长,最终变成了平台与客户端之间的集成点。
+ 将 2007 年开始运行的 SoundCloud 从单体模式转变至微服务模式 (具体改造过程:[philcalcado.com/2015/09/08/…](https://philcalcado.com/2015/09/08/how_we_ended_up_with_microservices.html%EF%BC%89%E3%80%82)
此时,单体服务已经被拆分为多个微服务。
+ 支持在 IOS 平台上新增的应用程序(原来的产品主要是在 Web 端提供服务)
![Public.png](https://gitee.com/songjianzaina/juejin_p9/raw/master/img/48d28fd703ef60ddfb26273f42285f5184b62158cc5564132b66111702a1478e)
  • 主要动机:
+ 减少产品发布上线的时间
+ 支持 IOS 平台的新应用程序,隔离其不同用户体验和交互模式带来的风险
  • 挑战:
+ 为了让第三方开发人员能更自由的集成,需要设计出不对数据的使用方式做出任何假设的 API。 所以,即便是呈现很简单的体验,也需要许多的 HTTP API 来提供服务。导致了构建一个简单的页面的数据需要上百个 API 请求来获取。
+ 每次团队需要更改现有API 时,都需要确保更改不会破坏我们现有的任何客户端(包括重要的第三方集成)。每当添加新内容,都必须投入大量时间来确保新功能不会过度用于特定客户端,以便所有客户都可以轻松使用新功能。 所有这些协调使日常工作变得比应有的困难得多,也就导致了新功能发布缓慢。
+ 开始准备开发新 iOS 应用程序, 新平台上的应用程序的用户体验会全部被重塑通过分析上面的各种情况,可以得出当时SoundCloud 后端团队面对的如下几个问题:
  • 问题一:难以为每个第三方客户提供合适粒度的 API,导致了 API 数据粒度过细,想完成一个业务服务需要请求的 API 太多。
  • 问题二:现有的对外 API 与消费方耦合严重,维护成本高,时间长,新功能发布缓慢。
  • 问题三:IOS平台新客户端更进了用户体验和交互方式,需要隔离新App带来的风险,并且还要找到与多个客户端团队更好的合作方式。

看上去,这三个问题在通常后端团队在完成微服务改造中往往也会遇到。让我们一起看看,他们是如何一步步见招拆招解决问题,并且找到《BFF》这个内功心法的。

Backends For Frontends 初成

SoundCloud 后端团队在面对上面复杂的问题时显得非常的专业。他们一开始便决定了,通过对不同客户端提供不同的 API 来各个击破。避免重蹈覆辙,多个客户端依赖同一套 API,继而耦合需求重合,无法进行灵活变更。同时,后端 Tech Lead Nick Fisher 创造了 BFF 这个缩写,也在团队内通过投票被认可。BFF 终于出现在技术世界的舞台之上,当时的架构如下图所示。
ServceB.png

你会发现与现在使用 BFF 模式的架构图有些细微的差别。没错,现在 BFF 还没真正的进化到成熟模式。接下来让我们看看它是在什么契机之下发生了进化。

后续的的研发中客户端团队意识到。由于他们拥有 API, 他们可以提取对不同服务进行多次调用的所有逻辑,并将它们混合到后端的用户配置文件中。这将简化代码并提高性能。不是对后端服务多次不同的调用,而是客户端对单个资源简单请求,例如:

  • GET /user-profile/123.json

当后端团队进一步试验这个方式时,他们发现了自己在 BFF 中编写了很多 Presentation Model。 在这个时候,他们突然意识到 BFF 不只是被客户端使用的 API ,而是 BFF 本身就是申请的一部分。BFF 新的形态出现了,具体如下图所示:

Moosle.png

滴答滴答,时钟在前进,SoundCloud 的 BFF 也在持续增加。这时,他们已经在生产环境同时维护着 5 个 BFF 了。为了进一步提高生产力,减少不必要的重复。 User Profile 从每个不同的微服务中抽取出来,变成一个独立的在 service 与 BFF 之间的应用服务。随着时间的推移,他们发现了越来越多这样的情况。现在 SoundCloud 的 BFF 显然随着时间的推移而增长,这种横向增长再不会引起任何问题了。最终,BFF 模式的架构变成下图:

Serviee A.png

结论

BFF 模式为我们在使用微服务时,针对多客户端开发的问题,提供了一种很好的方法。使我们在构建面向客户端的后端团队能够掌控自己的命运。 这种自主性对于快速迭代客户端应用程序并快速提供良好的体验至关重要。 通过支持持续的演进和变化,通过这种模式可以将消费者的行为限制在一个可控范围,会使他们变的更容易合作和改变。并且,更好满足不同客户端不同的特性需求。如果,在系统建立一开始便希望所有东西都是可复用的,会给系统的维护和协调带来巨大的挑战。特别是在面对需要维护多个客户端或消费者的场景下。在考虑通用用法之前,请专注于您的功能和特定用例。再在了解情况的基础上逐步进行低成本和合理的通用和复用。

本文转载自: 掘金

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

springboot 单元测试实践3 新建http请求接口

发表于 2021-10-24

本文重点来写点代码,对spring boot中http接口的返回进行单测。我们在springboot单元测试实践1中,已经写了如何mockcontroller的请求,测试controller中的逻辑,并简单针对返回体response进行了简单的测试。

在实际的生产环境中,spring boot对外提供的http接口的返回,根据具体的业务场景可能是个很复杂的数据结构。目前,大多数前后端分离的项目,返回的内容都是一整段的json格式的数据。

所以,我们对response做测试,大部分就是在针对response中返回的json做校验,json中有可能返回的是array,map等嵌套的复杂结构。

新建http请求接口

在前文的DemoController中,新建几个http的请求接口

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
typescript复制代码import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api")
@Slf4j
public class DemoController {

@GetMapping("/log")
public String log() {
log.info("test print log");
return "log";
}

@GetMapping("/list")
public List<String> stringList() {

List<String> list = new ArrayList<>();
list.add("one");
list.add("two");
list.add("three");
return list;
}

@GetMapping("/map")
public Object mapList() {
List<String> list = new ArrayList<>();
list.add("one");
list.add("two");
list.add("three");
return Map.of("list", list);
}

@GetMapping("/result")
public Object resultList() {
Result result1 = new Result();
result1.setKey("key1");
Result result2 = new Result();
result2.setKey("key2");
return List.of(result1, result2);
}

@Getter
@Setter
private class Result {
private String key;
private String value;
}
}

内容中新增了/api/list 、 /api/map、 /api/result 三个接口

/api/list 的单测

/api/list 主要是用来模拟直接返回一个list的列表,也就是说实际的response是下面这个样子的

1
css复制代码    ["one", "two", "three"]

针对 /api/list,写了下面的测试case

1
2
3
4
5
6
7
8
less复制代码@Test
void stringList() throws Exception {
this.mockMvc
.perform(MockMvcRequestBuilders.get("/api/list"))
.andDo(print())
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("$", Matchers.iterableWithSize(3)));
}

.andDo(print()) 实际上就是用来在控制台打印具体的response内容,这样方便比对response的内容进行针对性的测试

jsonPath方法是用来将response按照json的格式来进行解析,”$”符号代表着json的根对象,因为我们返回的是个list,所以根对象实际指的就是当前返回的array数组,这里我们期望返回的array列表的大小是3

/api/map 的单测

/api/map 主要是用来模拟直接返回一个map,也就是说实际的response是下面的这个样子的, 这里我们直接使用andDo(print())的控制台的打印来检查返回内容

image.png

针对 /api/map, 写了下面的测试case

1
2
3
4
5
6
7
8
9
10
11
less复制代码@Test
void mapList() throws Exception {
this.mockMvc
.perform(MockMvcRequestBuilders.get("/api/map"))
.andDo(print())
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("$").isMap())
.andExpect(MockMvcResultMatchers.jsonPath("$", Matchers.hasKey("list")))
.andExpect(MockMvcResultMatchers.jsonPath("$.list", Matchers.iterableWithSize(3)))
.andExpect(MockMvcResultMatchers.jsonPath("$.list[0]", Matchers.is("one")));
}

首先,我们测试jsonPath拿到的根对象的内容是不是一个map,再来检查map返回的内容中是不是有一个list的key,再来检查list返回的结果的大小是不是3个,最后来获取list中第一个对象的内容

/api/result接口的单测

/api/result 主要是用来模拟直接返回一个对象的列表,我们直接使用andDo(print())的控制台的打印来检查返回内容

image.png

针对 /api/result,写了下面的测试case

1
2
3
4
5
6
7
8
9
10
11
12
13
less复制代码@Test
void resultList() throws Exception {
this.mockMvc
.perform(MockMvcRequestBuilders.get("/api/result"))
.andDo(print())
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("$").isArray())
.andExpect(MockMvcResultMatchers.jsonPath("$", Matchers.iterableWithSize(2)))
.andExpect(
MockMvcResultMatchers.jsonPath(
"$", Matchers.everyItem(Matchers.allOf(Matchers.hasKey("key")))))
.andExpect(MockMvcResultMatchers.jsonPath("$[0].key", Matchers.is("key1")));
}

我们先测试jsonPath拿到的根对象是不是一个array数组,再检查列表中的大小是不是2,再检查列表中所有的对象,都包含key这个字段,最后检查列表中的第一个对象的key字段对应的值为key1

好了,针对json的测试目前也就这么多,更复杂的测试,可以多去看看MockMvcResultMatchers中提供的api和Matchers中提供的校验方法。

就到这里了,如果有复杂的场景,也可以在评论里写出来,一起来玩转业务场景的各种单测case

本文转载自: 掘金

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

1…471472473…956

开发者博客

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