2024年11月

一.  新建一个自由风格的软件项目

二. General配置(参数化构建)

1. 用来选择部署的服务器(我这里只添加了一个,如果需要添加多个,一行一个就可以了)

2. 选择不同的环境变量

三、源码管理

1. 填写Github项目地址,选择Credential(Credential需要自己新建,根据自己情况添加即可)。

2. 在Branches to buid下边填写要拉取的远程分支(如图)

四、Buid Steps

1. 增加构建步骤-Send files or execute commands over SSH

(1)选择Name之前,需要提前添加需要ssh的服务器的主机信息(当前使用windows服务器演示),包括地址,用户名,密码,例如:

在Dashboard-系统管理-系统配置-Publish over SSH-新增

Name随便填,能用来分辨即可,Hostname即ip地址,Username就是ssh的用户名,如果ssh需要密码,则再“高级”中勾选 "Use password authentication, or use a different key",然后填写密码。

最后,在右下角点击“Test Configuration”,如果通过,则继续进行下一步。否则需要重复检查您自己的配置直到通过。

(2)继续回到添加构建步骤,如下图,Name选择刚刚添加的ssh服务器信息,然后选择“高级”,勾选Verbos output in console(从Jenkins控制台输出详细步骤),勾选Label,Label则填写参数化构建过程中的SSH_SERVER中的选项列表的选项对应(一个选项        对应一个ssh步骤模块)。

(3)Transfers配置

Exec command下边填写命令

d: &&
if not exist mt4-manager md mt4-manager &&
mkdir D:\\mt4-manager\\$MANAGER &&
cd "mt4-manager" &&
if not exist publish md publish &&
cd "d:\\mt4-manager\\publish" &&
if not exist $MANAGER md $MANAGER

释义:发送cmd命令

d: ->跳转到D盘

    if not exist mt4-manager md mt4-manager -> 如果不存在mt4-manager文件夹则创建
    mkdir D:\\mt4-manager\\$MANAGER -> 创建动态名称目录($MANAGER对应参数化构建过程中的MANAGER对应的选项)
    cd "mt4-manager" -> 跳转到mt4-manager目录
后边的命令同以上


“高级”菜单中,Exec timeout修改为0,并且勾选Exec in pty。

2. 在同模块下,添加Transfer Set子模块

在Exec command下边填写以下命令:

pm2 delete "mt4-manager-api-$MANAGER"

3. 添加构建步骤-执行shell

输入命令(拷贝Jenkins服务器上的代码到ssh服务器上对应的目录):

scp -r ../mt4-manager-api mt4@192.168.0.130:d:\\mt4-manager\\$MANAGER

4. 再次添加构建步骤-Send files or execute commands over SSH

配置跟刚刚配置的ssh模块一样,但是Exec command输入为:

d: &&
cd "D:\mt4-manager\$MANAGER\manager-api-v2" &&
dotnet publish -c Release -o D:\mt4-manager\publish\$MANAGER &&
cd "D:\mt4-manager\publish\$MANAGER" &&
powershell -Command "(Get-Content appsettings.json -Raw) -replace 'devDemo','$MANAGER' | Set-Content appsettings.json" &&
pm2 start "manager-api-v2.exe" --name "manager-api-$MANAGER" &&
echo start success

别忘了“高级”菜单中的配置

释义:

进入d盘,进入manager-api-v2文件夹(项目根目录),

发布项目到指定文件夹

进入发布后的文件夹项目根目录

使用powershell命令,将appsettings.json中配置的环境变量值,由devDemo替换为动态参数值$MANAGER,对应参数化构建中的$MANAGER中的选项

使用pm2命令运行.net core程序,名称为manager-api-(动态参数)

输出 start success

到此,整个的配置过程就完成了。查看结果

如有错误,请大佬指正!

MySQL--DAY04

索引

定义

索引是在数据库
表的字段
上添加的,是为了
提高查询效率
存在的一种机制。

一张表的一个字段可以添加一个索引,当然,多个字段联合起来也可以添加索引。

索引相当于一本书的目录,是为了
缩小扫描范围
而存在的一种机制。

对于一本字典来说,查找某个汉字有两种方式:

  • 第一种方式:一页一页挨着找,直到找到为止,这种查找方式属于全字典扫描。效率比较低。
  • 第二种方式:先通过目录(索引)去定位一个大概的位置,然后直接定位到这个位置,做局域性扫描,缩小扫描的范围,快速的查找。这种查找方式属于通过索引检索,效率较高。
	t_user
	id(idIndex)	name(nameIndex)	email(emailIndex)		address  (emailAddressIndex)
	----------------------------------------------------------------------------------
	1				zhangsan...
	2				lisi
	3				wangwu
	4				zhaoliu
	5				hanmeimei
	6				jack

	select * from t_user where name = 'jack';

以上的这条SQL语句会去name字段上扫描,为什么?

因为查询条件是:name='jack'

如果name字段上没有添加索引(目录),或者说没有给name字段创建索引,

MySQL会进行全扫描,会将name字段上的每一个值都比对一遍。效率比较低。

MySQL在查询方面主要就是两种方式:

第一种方式:全表扫描

第二种方式:根据索引检索。

注意:

在实际中,汉语字典前面的目录是
排序
的,按照a b c d e f....排序,

为什么排序呢?因为只有排序了才会有区间查找这一说!(缩小扫描范围

其实就是扫描某个区间罢了!)

在mysql数据库当中
索引也是需要排序
的,并且这个索引的排序和TreeSet

数据结构相同。TreeSet(TreeMap)底层是一个自平衡的二叉树!在mysql

当中索引是一个
B-Tree
数据结构。

遵循左小右大原则存放。采用中序遍历方式遍历取数据。

原理

假设有一张用户表:t_user

	id(PK)					name						每一行记录在硬盘上都有物理存储编号
	----------------------------------------------------------------------------------
	100						zhangsan					0x1111
	120						lisi						0x2222
	99						wangwu						0x8888
	88						zhaoliu						0x9999
	101						jack						0x6666
	55						lucy						0x5555
	130						tom							0x7777

提醒1:在任何数据库当中
主键
上都会
自动添加索引对象
,id字段上自动有索引,因为id是PK。另外在mysql当中,一个字段上如果有
unique约束
的话,也会
自动创建索引对象

提醒2:在任何数据库当中,任何一张表的
任何一条记录
在硬盘存储上都有一个硬盘的
物理存储编号

提醒3:在mysql当中,索引是一个
单独的对象
,不同的存储引擎以不同的形式存在,在MyISAM存储引擎中,索引存储在一个.MYI文件中。在InnoDB存储引擎中索引存储在一个逻辑名称叫做tablespace的当中。在MEMORY存储引擎当中索引被存储在内存当中。不管索引存储在哪里,索引在mysql当中都是一个

的形式存在。(自平衡二叉树:
B-Tree

在mysql当中,主键上,以及unique字段上都会自动添加索引的!!!!

什么条件下,我们会考虑给字段添加索引呢?

  • 条件1:数据量庞大(到底有多么庞大算庞大,这个需要测试,因为每一个硬件环境不同)
  • 条件2:该字段经常出现在where的后面,以条件的形式存在,也就是说这个字段总是被扫描。
  • 条件3:该字段很少的DML(insert delete update)操作。(因为DML之后,索引需要重新排序。)

建议不要随意添加索引,因为索引也是需要维护的,太多的话反而会降低系统的性能。

建议通过主键查询,建议通过unique约束的字段进行查询,效率是比较高的。

创建和删除索引

创建索引:

mysql> create index emp_ename_index on emp(ename);

给emp表的ename字段添加索引,起名:emp_ename_index

删除索引:

mysql> drop index emp_ename_index on emp;

将emp表上的emp_ename_index索引对象删除。

查看SQL语句是否使用索引

	mysql> explain select * from emp where ename = 'KING';
	+----+-------------+-------+------+---------------+------+---------+------+------+-------------+
	| id | select_type | table | type | possible_keys | key  | key_len | ref  | rows | Extra       |
	+----+-------------+-------+------+---------------+------+---------+------+------+-------------+
	|  1 | SIMPLE      | emp   | ALL  | NULL          | NULL | NULL    | NULL |   14 | Using where |
	+----+-------------+-------+------+---------------+------+---------+------+------+-------------+

扫描14条记录:说明没有使用索引。type=ALL

	mysql> create index emp_ename_index on emp(ename);

	mysql> explain select * from emp where ename = 'KING';
	+----+-------------+-------+------+-----------------+-----------------+---------+-------+------+-------------+
	| id | select_type | table | type | possible_keys   | key             | key_len | ref   | rows | Extra       |
	+----+-------------+-------+------+-----------------+-----------------+---------+-------+------+-------------+
	|  1 | SIMPLE      | emp   | ref  | emp_ename_index | emp_ename_index | 33      | const |    1 | Using where |
	+----+-------------+-------+------+-----------------+-----------------+---------+-------+------+-------------+

索引失效

索引什么时候失效?

失效的第1种情况

select * from emp where ename like '%T';

ename上即使添加了索引,也不会走索引,为什么?

原因是因为模糊匹配当中以“%”开头了!

尽量避免模糊查询的时候以“%”开始。

这是一种优化的手段/策略。

		mysql> explain select * from emp where ename like '%T';
		+----+-------------+-------+------+---------------+------+---------+------+------+-------------+
		| id | select_type | table | type | possible_keys | key  | key_len | ref  | rows | Extra       |
		+----+-------------+-------+------+---------------+------+---------+------+------+-------------+
		|  1 | SIMPLE      | emp   | ALL  | NULL          | NULL | NULL    | NULL |   14 | Using where |
		+----+-------------+-------+------+---------------+------+---------+------+------+-------------+

失效的第2种情况

使用or的时候会失效,如果使用or那么要求
or两边的条件字段都要有索引
,才会走索引,如果其中一边有一个字段没有索引,那么另一个字段上的索引也会实现。所以这就是为什么不建议使用or的原因。

mysql> explain select * from emp where ename = 'KING' or job = 'MANAGER';
	+----+-------------+-------+------+-----------------+------+---------+------+------+-------------+
	| id | select_type | table | type | possible_keys   | key  | key_len | ref  | rows | Extra       |
	+----+-------------+-------+------+-----------------+------+---------+------+------+-------------+
	|  1 | SIMPLE      | emp   | ALL  | emp_ename_index | NULL | NULL    | NULL |   14 | Using where |
	+----+-------------+-------+------+-----------------+------+---------+------+------+-------------+

失效的第3种情况

使用复合索引的时候,没有使用左侧的列查找,索引失效

什么是复合索引?

两个字段,或者更多的字段联合起来添加一个索引,叫做复合索引。

		create index emp_job_sal_index on emp(job,sal);
		
		mysql> explain select * from emp where job = 'MANAGER';
		+----+-------------+-------+------+-------------------+-------------------+---------+-------+------+-------------+
		| id | select_type | table | type | possible_keys     | key               | key_len | ref   | rows | Extra       |
		+----+-------------+-------+------+-------------------+-------------------+---------+-------+------+-------------+
		|  1 | SIMPLE      | emp   | ref  | emp_job_sal_index | emp_job_sal_index | 30      | const |    3 | Using where |
		+----+-------------+-------+------+-------------------+-------------------+---------+-------+------+-------------+
		
		mysql> explain select * from emp where sal = 800;
		+----+-------------+-------+------+---------------+------+---------+------+------+-------------+
		| id | select_type | table | type | possible_keys | key  | key_len | ref  | rows | Extra       |
		+----+-------------+-------+------+---------------+------+---------+------+------+-------------+
		|  1 | SIMPLE      | emp   | ALL  | NULL          | NULL | NULL    | NULL |   14 | Using where |
		+----+-------------+-------+------+---------------+------+---------+------+------+-------------+

失效的第4种情况

在where当中索引列参加了运算,索引失效。

		mysql> create index emp_sal_index on emp(sal);

		explain select * from emp where sal = 800;
		+----+-------------+-------+------+---------------+---------------+---------+-------+------+-------------+
		| id | select_type | table | type | possible_keys | key           | key_len | ref   | rows | Extra       |
		+----+-------------+-------+------+---------------+---------------+---------+-------+------+-------------+
		|  1 | SIMPLE      | emp   | ref  | emp_sal_index | emp_sal_index | 9       | const |    1 | Using where |
		+----+-------------+-------+------+---------------+---------------+---------+-------+------+-------------+

		mysql> explain select * from emp where sal+1 = 800;
		+----+-------------+-------+------+---------------+------+---------+------+------+-------------+
		| id | select_type | table | type | possible_keys | key  | key_len | ref  | rows | Extra       |
		+----+-------------+-------+------+---------------+------+---------+------+------+-------------+
		|  1 | SIMPLE      | emp   | ALL  | NULL          | NULL | NULL    | NULL |   14 | Using where |
		+----+-------------+-------+------+---------------+------+---------+------+------+-------------+

失效的第5种情况

在where当中索引列使用了函数

		explain select * from emp where lower(ename) = 'smith';
		+----+-------------+-------+------+---------------+------+---------+------+------+-------------+
		| id | select_type | table | type | possible_keys | key  | key_len | ref  | rows | Extra       |
		+----+-------------+-------+------+---------------+------+---------+------+------+-------------+
		|  1 | SIMPLE      | emp   | ALL  | NULL          | NULL | NULL    | NULL |   14 | Using where |
		+----+-------------+-------+------+---------------+------+---------+------+------+-------------+

失效的第...种情况

索引分类

  1. 单一索引:一个字段上添加索引。
  2. 复合索引:两个字段或者更多的字段上添加索引。
  3. 主键索引:主键上添加索引。
  4. 唯一性索引:具有unique约束的字段上添加索引。
    .....

注意:唯一性比较弱的字段上添加索引用处不大。

视图

什么是视图?

view:站在不同的角度去看待同一份数据。

创建和删除视图对象

	表复制:
	mysql> create table dept2 as select * from dept;

	dept2表中的数据:
	mysql> select * from dept2;
	+--------+------------+----------+
	| DEPTNO | DNAME      | LOC      |
	+--------+------------+----------+
	|     10 | ACCOUNTING | NEW YORK |
	|     20 | RESEARCH   | DALLAS   |
	|     30 | SALES      | CHICAGO  |
	|     40 | OPERATIONS | BOSTON   |
	+--------+------------+----------+

创建视图对象:

	create view dept2_view as select * from dept2;

删除视图对象:

	drop view dept2_view;	

注意:只有DQL语句才能以view的形式创建。

create view view_name as // 这里的语句必须是DQL语句;

视图的作用

我们可以面向视图对象进行增删改查,对视图对象的增删改查,会导致
原表被操作

(视图的特点:通过对视图的操作,会影响到原表数据。)

	//面向视图查询
	select * from dept2_view; 

	// 面向视图插入
	insert into dept2_view(deptno,dname,loc) values(60,'SALES', 'BEIJING');

	// 查询原表数据
	mysql> select * from dept2;
	+--------+------------+----------+
	| DEPTNO | DNAME      | LOC      |
	+--------+------------+----------+
	|     10 | ACCOUNTING | NEW YORK |
	|     20 | RESEARCH   | DALLAS   |
	|     30 | SALES      | CHICAGO  |
	|     40 | OPERATIONS | BOSTON   |
	|     60 | SALES      | BEIJING  |
	+--------+------------+----------+

	// 面向视图删除
	mysql> delete from dept2_view;

	// 查询原表数据
	mysql> select * from dept2;
	Empty set (0.00 sec)
	

	// 创建视图对象
	create view 
		emp_dept_view
	as
		select 
			e.ename,e.sal,d.dname
		from
			emp e
		join
			dept d
		on
			e.deptno = d.deptno;

	// 查询视图对象
	mysql> select * from emp_dept_view;
	+--------+---------+------------+
	| ename  | sal     | dname      |
	+--------+---------+------------+
	| CLARK  | 2450.00 | ACCOUNTING |
	| KING   | 5000.00 | ACCOUNTING |
	| MILLER | 1300.00 | ACCOUNTING |
	| SMITH  |  800.00 | RESEARCH   |
	| JONES  | 2975.00 | RESEARCH   |
	| SCOTT  | 3000.00 | RESEARCH   |
	| ADAMS  | 1100.00 | RESEARCH   |
	| FORD   | 3000.00 | RESEARCH   |
	| ALLEN  | 1600.00 | SALES      |
	| WARD   | 1250.00 | SALES      |
	| MARTIN | 1250.00 | SALES      |
	| BLAKE  | 2850.00 | SALES      |
	| TURNER | 1500.00 | SALES      |
	| JAMES  |  950.00 | SALES      |
	+--------+---------+------------+

	// 面向视图更新
	update emp_dept_view set sal = 1000 where dname = 'ACCOUNTING';

	// 原表数据被更新
	mysql> select * from emp;
	+-------+--------+-----------+------+------------+---------+---------+--------+
	| EMPNO | ENAME  | JOB       | MGR  | HIREDATE   | SAL     | COMM    | DEPTNO |
	+-------+--------+-----------+------+------------+---------+---------+--------+
	|  7369 | SMITH  | CLERK     | 7902 | 1980-12-17 |  800.00 |    NULL |     20 |
	|  7499 | ALLEN  | SALESMAN  | 7698 | 1981-02-20 | 1600.00 |  300.00 |     30 |
	|  7521 | WARD   | SALESMAN  | 7698 | 1981-02-22 | 1250.00 |  500.00 |     30 |
	|  7566 | JONES  | MANAGER   | 7839 | 1981-04-02 | 2975.00 |    NULL |     20 |
	|  7654 | MARTIN | SALESMAN  | 7698 | 1981-09-28 | 1250.00 | 1400.00 |     30 |
	|  7698 | BLAKE  | MANAGER   | 7839 | 1981-05-01 | 2850.00 |    NULL |     30 |
	|  7782 | CLARK  | MANAGER   | 7839 | 1981-06-09 | 1000.00 |    NULL |     10 |
	|  7788 | SCOTT  | ANALYST   | 7566 | 1987-04-19 | 3000.00 |    NULL |     20 |
	|  7839 | KING   | PRESIDENT | NULL | 1981-11-17 | 1000.00 |    NULL |     10 |
	|  7844 | TURNER | SALESMAN  | 7698 | 1981-09-08 | 1500.00 |    0.00 |     30 |
	|  7876 | ADAMS  | CLERK     | 7788 | 1987-05-23 | 1100.00 |    NULL |     20 |
	|  7900 | JAMES  | CLERK     | 7698 | 1981-12-03 |  950.00 |    NULL |     30 |
	|  7902 | FORD   | ANALYST   | 7566 | 1981-12-03 | 3000.00 |    NULL |     20 |
	|  7934 | MILLER | CLERK     | 7782 | 1982-01-23 | 1000.00 |    NULL |     10 |
	+-------+--------+-----------+------+------------+---------+---------+--------+

实际开发中的作用?

《方便,简化开发,利于维护》

		create view 
			emp_dept_view
		as
			select 
				e.ename,e.sal,d.dname
			from
				emp e
			join
				dept d
			on
				e.deptno = d.deptno;

假设有一条非常复杂的SQL语句,而这条SQL语句需要在不同的位置上反复使用。

每一次使用这个sql语句的时候都需要重新编写,很长,很麻烦,怎么办

可以把这条复杂的SQL语句以视图对象的形式新建。

在需要编写这条SQL语句的位置直接使用视图对象,可以大大简化开发。

并且
利于后期的维护
,因为修改的时候也只需要修改一个位置就行,只需要修改视图对象所映射的SQL语句。

我们以后面向视图开发的时候,使用视图的时候可以像使用table一样。

可以对视图进行增删改查等操作。视图不是在内存当中,视图对象也是存储在
硬盘
上的,不会消失。

再提醒一下

视图对应的语句
只能是DQL语句
。但是视图对象创建完成之后,可以对视图进行增删改查等操作。

小插曲:

增删改查,又叫做:CRUD。

CRUD是在公司中程序员之间沟通的术语。一般我们很少说增删改查。

一般都说CRUD。

C:Create(增)
R:Retrive(查:检索)
U:Update(改)
D:Delete(删)

DBA常用命令?

重点掌握:
数据的导入和导出

数据的备份

其它命令了解一下即可。

数据导出

注意:在windows的dos命令窗口中:

mysqldump study>D:\study.sql -uroot -p123456

可以导出指定的表吗?

mysqldump study emp>D:\study.sql -uroot -p123456

数据导入

注意:需要先登录到mysql数据库服务器上。

然后创建数据库:

create database study;

使用数据库:

use study

然后初始化数据库:source D:\study.sql

数据库设计三范式

什么是数据库设计三范式?

数据库表的
设计依据
。教你怎么进行数据库表的设计。

数据库设计三范式有几类?

3个。

  1. 第一范式:要求任何一张表必须有主键,每一个字段原子性不可再分。
  2. 第二范式:建立在第一范式的基础之上,要求所有非主键字段完全依赖主键,不要产生部分依赖。
  3. 第三范式:建立在第二范式的基础之上,要求所有非主键字段直接依赖主键,不要产生传递依赖。

声明:三范式是面试官经常问的,所以一定要熟记在心!

设计数据库表的时候,按照以上的范式进行,可以
避免表中数据的冗余,空间的浪费

第一范式

最核心,最重要的范式,所有表的设计都需要满足。

必须有主键
,并且每一个字段都是
原子性
不可再分。

	学生编号 	学生姓名 			联系方式
	---------------------------------------------------
	1001		张三			zs@gmail.com,1359999999
	1002		李四			ls@gmail.com,13699999999
	1001		王五			ww@163.net,13488888888

以上是学生表,满足第一范式吗?

不满足,第一:没有主键。第二:联系方式可以分为邮箱地址和电话

	学生编号(pk) 		学生姓名		邮箱地址				联系电话
	--------------------------------------------------------------------
	1001				张三			zs@gmail.com		1359999999
	1002				李四			ls@gmail.com		13699999999
	1003				王五			ww@163.net			13488888888

第二范式

建立在第一范式的基础之上,要求
所有非主键字段必须完全依赖主键

不要产生部分依赖

学生编号 		学生姓名 	教师编号 	教师姓名
----------------------------------------------------
1001			张三		001			王老师
1002			李四		002			赵老师
1003			王五		001			王老师
1001			张三		002			赵老师

这张表描述了学生和老师的关系:(1个学生可能有多个老师,1个老师有多个学生)

这是非常典型的:
多对多
关系!

分析以上的表是否满足第一范式?

不满足第一范式。

怎么满足第一范式呢?修改

	学生编号+教师编号(pk)			学生姓名  		教师姓名
	----------------------------------------------------
	1001			001				张三			王老师
	1002			002				李四			赵老师
	1003			001				王五			王老师
	1001			002				张三			赵老师

学生编号 教师编号,两个字段联合做主键,
复合主键
(PK: 学生编号+教师编号)

经过修改之后,以上的表满足了第一范式。但是满足第二范式吗?

不满足,“张三”依赖1001,“王老师”依赖001,显然产生了
部分依赖

产生部分依赖有什么缺点?

数据冗余
了。
空间浪费
了。“张三”重复了,“王老师”重复了。

为了让以上的表满足第二范式,你需要这样设计:

使用三张表来表示多对多的关系!!!!

		学生表
		学生编号(pk)			学生名字
		------------------------------------
		1001					张三
		1002					李四
		1003					王五
		
		教师表
		教师编号(pk)		教师姓名
		--------------------------------------
		001					王老师
		002					赵老师

		学生教师关系表
		id(pk)					学生编号(fk)			教师编号(fk)
		-----------------------------------------------------------
		1						1001						001
		2						1002						002
		3						1003						001
		4						1001						002

背口诀:

多对多怎么设计?

多对多,三张表,关系表两个外键!!!!!!!!!!!!!!!

第三范式

第三范式建立在第二范式的基础之上

要求所有非主键字典必须直接依赖主键,不要产生
传递依赖

		学生编号(PK) 		学生姓名 班级编号  		班级名称
	---------------------------------------------------------
		1001				张三		01			一年一班
		1002				李四		02			一年二班
		1003				王五		03			一年三班
		1004				赵六		03			一年三班

以上表的设计是描述:班级和学生的关系。很显然是1对多关系!

一个教室中有多个学生。

分析以上表是否满足第一范式?

满足第一范式,有主键。

分析以上表是否满足第二范式?

满足第二范式,因为主键不是复合主键,没有产生部分依赖。主键是
单一主键

分析以上表是否满足第三范式?

第三范式要求:不要产生传递依赖!

一年一班依赖01,01依赖1001,产生了传递依赖。

不符合第三范式的要求。产生了数据的冗余。

那么应该怎么设计一对多呢?

		班级表:一
		班级编号(pk)					班级名称
		----------------------------------------
		01								一年一班
		02								一年二班
		03								一年三班

		学生表:多

		学生编号(PK) 		学生姓名 	班级编号(fk)
		-------------------------------------------
		1001				张三			01			
		1002				李四			02			
		1003				王五			03			
		1004				赵六			03		

背口诀:

一对多,两张表,多的表加外键!!!!!!!!!!!!

总结

一对多:一对多,两张表,多的表加外键!!!!!!!!!!!!

多对多:多对多,三张表,关系表两个外键!!!!!!!!!!!!!!!

一对一:一对一放到一张表中不就行了吗?为啥还要拆分表?

在实际的开发中,可能存在一张表字段太多,太庞大。这个时候要拆分表。

一对一怎么设计?

			没有拆分表之前:一张表
				t_user
				id		login_name		login_pwd		real_name		email				address........
				---------------------------------------------------------------------------
				1			zhangsan		123				张三				zhangsan@xxx
				2			lisi			123				李四				lisi@xxx
				...
			
			这种庞大的表建议拆分为两张:
				t_login 登录信息表
				id(pk)		login_name		login_pwd	
				---------------------------------
				1				zhangsan		123			
				2				lisi			123			

				t_user 用户详细信息表
				id(pk)		real_name		email				address........	login_id(fk+unique)
				-----------------------------------------------------------------------------------------
				100			张三				zhangsan@xxx								1
				200			李四				lisi@xxx										2

口诀:一对一,外键唯一!!!!!!!!!!

叮嘱

数据库设计三范式是
理论
上的。

实践和理论有的时候有偏差。

最终的目的都是为了
满足客户的需求
,有的时候会拿冗余换执行速度。

因为在sql当中,表和表之间连接次数越多,效率越低。(笛卡尔积)

有的时候可能会存在冗余,但是为了
减少表的连接次数
,这样做也是合理的,并且对于开发人员来说,sql语句的编写难度也会降低。

面试的时候把这句话说上:他就不会认为你是初级程序员了!

在各平台应用开发过程中,随着业务的功能增加,不免会涉及到非公开的
API
依赖,针对某些应用或厂商系统的适配,每个版本都需要投入精力去排查,
CoCollider
可以让我们的适配效率从几个星期提升到几小时即可完成。

项目已开源:

☞ Github:https://www.github.com/iofomo/fireyer ☜

☞ Github:https://www.github.com/iofomo/wing ☜

如果您也喜欢,别忘了给我们点个星。

01. 写在前面


Android
平台(后续会支持
iOS
平台)日常开发过程中,不免会接触到一些
Hidden
(或私有函数和类)的类,
JVM
层通过反射,
Native
则通过查找函数
symbol
来完成。特别是涉及系统
Framework
层和底层库较多的实现(特别是安全产品和系统工具类应用),成为每次系统和厂商
ROM
更新适配的重灾区,严重影响适配效率。

以往,开发同学要么先一个个自行排查代码后再提交测试,这个过程重复,耗时且容易遗漏。要么先让测试同学先进行用例覆盖测试,然后开发同学通过
BUG
来进行分析,这会导致整个团队效率更加低下。

由于我们虚拟化产品的特性,工程中涉及系统和应用适配的接口众多,工作量很大,主要有两部分:

  1. 如何快速寻找到系统新增的模块,感兴趣的可以查看我们的另一个开源项目
    【ASeeker】
    https://www.github.com/iofomo/aseeker
  2. 如何快速定位现有使用的
    API
    是否已发生变化,这就是
    【CoCollider】
    要解决的问题。

CoCollider
已经在我们内部使用,极大提升我们系统适配效率,原本需要几个星期的工作量,现在只需几小时便可适配完成,原理简单,堪称适配效率神器。
Android 15
发布在即,现在分享给大家。

CoCollider
同样适用于其他平台(如:iOS)和某些深度定制的应用版本适配。

02. 我们需要

对于系统的
API
,我们的需求是:

  1. 类的属性是否有变化。
  2. 类的成员属性是否有变化。
  3. 类的方法属性是否有变化。
  4. Native
    库和函数是否存在。

对于开发工程师的工作量,首次需要在源码中添加注释标签,这个工作不可省略,却一劳永逸。后续需要在每次涉及适配的代码处加上
@CoCollider
标签就行。

Java代码标签格式:

格式:
// @CoCollider {class name},{-/+}{field name/method name},...

范例:
# 仅查看类是否变更
// @CoCollider android.utils.Abc
# 仅查看类所有成员是否变更
// @CoCollider android.utils.Abc,-*
# 查看类和某成员或方法是否变更(支持多个)
// @CoCollider android.utils.Abc,-mFile
// @CoCollider android.utils.Abc,-mFile,mName
// @CoCollider android/utils/Abc,-mFile
// @CoCollider android.utils.Abc,+getFile,-mName
// @CoCollider android/utils/Abc,+getFile,+getName,-mName

# 支持缺省自动填充:(按照顺序依次填充)
Class.forName("android.utils.Abc");// @CoCollider
# 等同于:// @CoCollider android.utils.Abc

ReflectUtils.getStaticField("android.utils.Abc", "mName");// @CoCollider ,-
# 等同于:// @CoCollider android.utils.Abc,-mName

Native 代码标签格式:

格式:
// @CoCollider ~{lib name},{-/+}{field name/method name},...
  
范例:
# 查看方法是否变更
// @CoCollider ~libc.so,+open
// @CoCollider ~/system/lib/libc.so,+open
// @CoCollider ~/system/lib/libc.so,+open,+close

# 支持缺省自动填充:(按照顺序依次填充)
utils_dlsym("libc.so", "open");// @CoCollider ~,+
# 等同于:// @CoCollider ~libc.so,+open

utils_dlsym("open");// @CoCollider ~libc.so,+
# 等同于:// @CoCollider ~libc.so,+open

03. 配置运行

  1. 电脑下载配置
    wing

  2. 手机安装
    Fireyer
    ,或集成
    CoCollider
    模块的应用,链接
    adb

  3. 扫描代码:

    # 命令格式:扫描当前目录下的代码(默认支持 h/c/cpp/java/kt/aidl,也可以追加文件类型)
    $ wing -cocollider scan
    $ wing -cocollider scan /home/space
    # 或
    $ python ./cocollider.py scan
    $ python ./cocollider.py scan /home/space
    # 在当前目录输出结果
    >>> cocollider-scan-20241023-112044.txt
    
  4. 解析运行:

    # 命令格式:调用 Fireyer 应用
    $ wing -cocollider run /home/cocollider-scan-20241023-112044.txt
    # 或
    $ python ./cocollider.py run /home/cocollider-scan-20241023-112044.txt
    
    # 在当前目录输出结果
    >>> cocollider-run-20241023-112044.txt
    
  5. 使用对比工具查看结果即可快速定位变更内容。

04. 扫描结果

cocollider-scan-20241023-112044.txt
中的文件内容为:

############################################################
= android.utils.Abc

- mFile
> application/fireyer/test/test1.java,14

- mName3
> application/fireyer/test/test1.java,14

- mPath
> application/fireyer/test/test1.java,14

############################################################
= android.utils.Abc1

- mFile
> application/fireyer/test/test1.java,5

...

05. 运行结果

cocollider-run-20241023-112044.txt
中的文件内容(已对
class

lib
库,
function

field

method
排序,便于使用对比工具对比)

############################################################
~ libwilhelm.so
[OK]

+ _ZN7androidxx6BufferE
[OK], public final class libcore.io.Linux
> cmpt/xxx/jni/xxx/jni/src/xxx.cpp,362

+ _ZN7android1xx6BufferE
[OK], public final class libcore.io.Linux
> cmpt/xxx/jni/xxx/jni/src/xxx.cpp,350


############################################################
= ohos.abilityshell.HarmonyApplication
[Fail]

- applicationHandler
[Fail]
> scene/xxx/xxx.java,36


############################################################
= ohos.system.Parameters
[Fail]

+ nativeGet
[Fail]
> scene/xxx.cpp,250

...

06. 对比结果

分别在不同版本系统运行
python cocollider.py run
之后,使用对比工具(如:
Beyond Compare
)对内容进行比较查看,可以快速找到新增,修改和删除项,从而可以快速进行排查和修复。

前言

对于从事java开发工作的同学来说,spring的事务肯定再熟悉不过了。

在某些业务场景下,如果一个请求中,需要同时写入多张表的数据。为了保证操作的原子性(要么同时成功,要么同时失败),避免数据不一致的情况,我们一般都会用到spring事务。

确实,spring事务用起来贼爽,就用一个简单的注解:
@Transactional
,就能轻松搞定事务。我猜大部分小伙伴也是这样用的,而且一直用一直爽。

但如果你使用不当,它也会坑你于无形。

今天我们就一起聊聊,事务失效的一些场景,说不定你已经中招了。不信,让我们一起看看。

一 事务不生效

1.访问权限问题

众所周知,java的访问权限主要有四种:private、default、protected、public,它们的权限从左到右,依次变大。

但如果我们在开发过程中,把有某些事务方法,定义了错误的访问权限,就会导致事务功能出问题,例如:

@Service
public class UserService {
    
    @Transactional
    private void add(UserModel userModel) {
         saveData(userModel);
         updateData(userModel);
    }
}

我们可以看到add方法的访问权限被定义成了
private
,这样会导致事务失效,spring要求被代理方法必须是
public
的。

说白了,在
AbstractFallbackTransactionAttributeSource
类的
computeTransactionAttribute
方法中有个判断,如果目标方法不是public,则
TransactionAttribute
返回null,即不支持事务。

protected TransactionAttribute computeTransactionAttribute(Method method, @Nullable Class<?> targetClass) {
    // Don't allow no-public methods as required.
    if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
      return null;
    }

    // The method may be on an interface, but we need attributes from the target class.
    // If the target class is null, the method will be unchanged.
    Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass);

    // First try is the method in the target class.
    TransactionAttribute txAttr = findTransactionAttribute(specificMethod);
    if (txAttr != null) {
      return txAttr;
    }

    // Second try is the transaction attribute on the target class.
    txAttr = findTransactionAttribute(specificMethod.getDeclaringClass());
    if (txAttr != null && ClassUtils.isUserLevelMethod(method)) {
      return txAttr;
    }

    if (specificMethod != method) {
      // Fallback is to look at the original method.
      txAttr = findTransactionAttribute(method);
      if (txAttr != null) {
        return txAttr;
      }
      // Last fallback is the class of the original method.
      txAttr = findTransactionAttribute(method.getDeclaringClass());
      if (txAttr != null && ClassUtils.isUserLevelMethod(method)) {
        return txAttr;
      }
    }
    return null;
  }

也就是说,如果我们自定义的事务方法(即目标方法),它的访问权限不是
public
,而是private、default或protected的话,spring则不会提供事务功能。

2. 方法用final修饰

有时候,某个方法不想被子类重新,这时可以将该方法定义成final的。普通方法这样定义是没问题的,但如果将事务方法定义成final,例如:

@Service
public class UserService {

    @Transactional
    public final void add(UserModel userModel){
        saveData(userModel);
        updateData(userModel);
    }
}

我们可以看到add方法被定义成了
final
的,这样会导致事务失效。

为什么?

如果你看过spring事务的源码,可能会知道spring事务底层使用了aop,也就是通过jdk动态代理或者cglib,帮我们生成了代理类,在代理类中实现的事务功能。

但如果某个方法用final修饰了,那么在它的代理类中,就无法重写该方法,而添加事务功能。

注意:如果某个方法是static的,同样无法通过动态代理,变成事务方法。

3.方法内部调用

有时候我们需要在某个Service类的某个方法中,调用另外一个事务方法,比如:

@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;

    //@Transactional
    public void add(UserModel userModel) {
        userMapper.insertUser(userModel);
        updateStatus(userModel);
    }

    @Transactional
    public void updateStatus(UserModel userModel) {
        doSameThing();
    }
}

我们看到在事务方法add中,直接调用事务方法updateStatus。从前面介绍的内容可以知道,updateStatus方法拥有事务的能力是因为spring aop生成代理了对象,但是这种方法直接调用了this对象的方法,所以updateStatus方法不会生成事务。

由此可见,在同一个类中的方法直接内部调用,会导致事务失效。

那么问题来了,如果有些场景,确实想在同一个类的某个方法中,调用它自己的另外一个方法,该怎么办呢?

3.1 新加一个Service方法

这个方法非常简单,只需要新加一个Service方法,把@Transactional注解加到新Service方法上,把需要事务执行的代码移到新方法中。具体代码如下:

@Servcie
public class ServiceA {
   @Autowired
   prvate ServiceB serviceB;

   public void save(User user) {
         queryData1();
         queryData2();
         serviceB.doSave(user);
   }
 }

 @Servcie
 public class ServiceB {

    @Transactional(rollbackFor=Exception.class)
    public void doSave(User user) {
       addData1();
       updateData2();
    }

 }

3.2 在该Service类中注入自己

如果不想再新加一个Service类,在该Service类中注入自己也是一种选择。具体代码如下:

@Servcie
public class ServiceA {
   @Autowired
   prvate ServiceA serviceA;

   public void save(User user) {
         queryData1();
         queryData2();
         serviceA.doSave(user);
   }

   @Transactional(rollbackFor=Exception.class)
   public void doSave(User user) {
       addData1();
       updateData2();
    }
 }

可能有些人可能会有这样的疑问:这种做法会不会出现循环依赖问题?

答案:不会。

其实spring ioc内部的三级缓存保证了它,不会出现循环依赖问题。但有些坑,如果你想进一步了解循环依赖问题,可以看看我之前文章《
spring:我是如何解决循环依赖的?
》。

3.3 通过AopContent类

在该Service类中使用AopContext.currentProxy()获取代理对象

上面的方法2确实可以解决问题,但是代码看起来并不直观,还可以通过在该Service类中使用AOPProxy获取代理对象,实现相同的功能。具体代码如下:

@Servcie
public class ServiceA {

   public void save(User user) {
         queryData1();
         queryData2();
         ((ServiceA)AopContext.currentProxy()).doSave(user);
   }

   @Transactional(rollbackFor=Exception.class)
   public void doSave(User user) {
       addData1();
       updateData2();
    }
 }

4.未被spring管理

在我们平时开发过程中,有个细节很容易被忽略。即使用spring事务的前提是:对象要被spring管理,需要创建bean实例。

通常情况下,我们通过@Controller、@Service、@Component、@Repository等注解,可以自动实现bean实例化和依赖注入的功能。

当然创建bean实例的方法还有很多,有兴趣的小伙伴可以看看我之前写的另一篇文章《
@Autowired的这些骚操作,你都知道吗?

如果有一天,你匆匆忙忙的开发了一个Service类,但忘了加@Service注解,比如:

//@Service
public class UserService {

    @Transactional
    public void add(UserModel userModel) {
         saveData(userModel);
         updateData(userModel);
    }    
}

从上面的例子,我们可以看到UserService类没有加
@Service
注解,那么该类不会交给spring管理,所以它的add方法也不会生成事务。

5.多线程调用

在实际项目开发中,多线程的使用场景还是挺多的。如果spring事务用在多线程场景中,会有问题吗?

@Slf4j
@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;
    @Autowired
    private RoleService roleService;

    @Transactional
    public void add(UserModel userModel) throws Exception {
        userMapper.insertUser(userModel);
        new Thread(() -> {
            roleService.doOtherThing();
        }).start();
    }
}

@Service
public class RoleService {

    @Transactional
    public void doOtherThing() {
        System.out.println("保存role表数据");
    }
}

从上面的例子中,我们可以看到事务方法add中,调用了事务方法doOtherThing,但是事务方法doOtherThing是在另外一个线程中调用的。

这样会导致两个方法不在同一个线程中,获取到的数据库连接不一样,从而是两个不同的事务。如果想doOtherThing方法中抛了异常,add方法也回滚是不可能的。

如果看过spring事务源码的朋友,可能会知道spring的事务是通过数据库连接来实现的。当前线程中保存了一个map,key是数据源,value是数据库连接。

private static final ThreadLocal<Map<Object, Object>> resources =

  new NamedThreadLocal<>("Transactional resources");

我们说的同一个事务,其实是指同一个数据库连接,只有拥有同一个数据库连接才能同时提交和回滚。如果在不同的线程,拿到的数据库连接肯定是不一样的,所以是不同的事务。

6.表不支持事务

周所周知,在mysql5之前,默认的数据库引擎是
myisam

它的好处就不用多说了:索引文件和数据文件是分开存储的,对于查多写少的单表操作,性能比innodb更好。

有些老项目中,可能还在用它。

在创建表的时候,只需要把
ENGINE
参数设置成
MyISAM
即可:

CREATE TABLE `category` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `one_category` varchar(20) COLLATE utf8mb4_bin DEFAULT NULL,
  `two_category` varchar(20) COLLATE utf8mb4_bin DEFAULT NULL,
  `three_category` varchar(20) COLLATE utf8mb4_bin DEFAULT NULL,
  `four_category` varchar(20) COLLATE utf8mb4_bin DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin

myisam好用,但有个很致命的问题是:
不支持事务

如果只是单表操作还好,不会出现太大的问题。但如果需要跨多张表操作,由于其不支持事务,数据极有可能会出现不完整的情况。

此外,myisam还不支持行锁和外键。

所以在实际业务场景中,myisam使用的并不多。在mysql5以后,myisam已经逐渐退出了历史的舞台,取而代之的是innodb。

有时候我们在开发的过程中,发现某张表的事务一直都没有生效,那不一定是spring事务的锅,最好确认一下你使用的那张表,是否支持事务。

7.未开启事务

有时候,事务没有生效的根本原因是没有开启事务。

你看到这句话可能会觉得好笑。

开启事务不是一个项目中,最最最基本的功能吗?

为什么还会没有开启事务?

没错,如果项目已经搭建好了,事务功能肯定是有的。

但如果你是在搭建项目demo的时候,只有一张表,而这张表的事务没有生效。那么会是什么原因造成的呢?

当然原因有很多,但没有开启事务,这个原因极其容易被忽略。

如果你使用的是springboot项目,那么你很幸运。因为springboot通过
DataSourceTransactionManagerAutoConfiguration
类,已经默默的帮你开启了事务。

你所要做的事情很简单,只需要配置
spring.datasource
相关参数即可。

但如果你使用的还是传统的spring项目,则需要在applicationContext.xml文件中,手动配置事务相关参数。如果忘了配置,事务肯定是不会生效的。

具体配置如下信息:

   
<!-- 配置事务管理器 --> 
<bean class="org.springframework.jdbc.datasource.DataSourceTransactionManager" id="transactionManager"> 
    <property name="dataSource" ref="dataSource"></property> 
</bean> 
<tx:advice id="advice" transaction-manager="transactionManager"> 
    <tx:attributes> 
        <tx:method name="*" propagation="REQUIRED"/>
    </tx:attributes> 
</tx:advice> 
<!-- 用切点把事务切进去 --> 
<aop:config> 
    <aop:pointcut expression="execution(* com.susan.*.*(..))" id="pointcut"/> 
    <aop:advisor advice-ref="advice" pointcut-ref="pointcut"/> 
</aop:config> 

默默的说一句,如果在pointcut标签中的切入点匹配规则,配错了的话,有些类的事务也不会生效。

二 事务不回滚

1.错误的传播特性

其实,我们在使用
@Transactional
注解时,是可以指定
propagation
参数的。

该参数的作用是指定事务的传播特性,spring目前支持7种传播特性:

  • REQUIRED
    如果当前上下文中存在事务,那么加入该事务,如果不存在事务,创建一个事务,这是默认的传播属性值。
  • SUPPORTS
    如果当前上下文存在事务,则支持事务加入事务,如果不存在事务,则使用非事务的方式执行。
  • MANDATORY
    如果当前上下文中存在事务,否则抛出异常。
  • REQUIRES_NEW
    每次都会新建一个事务,并且同时将上下文中的事务挂起,执行当前新建事务完成以后,上下文事务恢复再执行。
  • NOT_SUPPORTED
    如果当前上下文中存在事务,则挂起当前事务,然后新的方法在没有事务的环境中执行。
  • NEVER
    如果当前上下文中存在事务,则抛出异常,否则在无事务环境上执行代码。
  • NESTED
    如果当前上下文中存在事务,则嵌套事务执行,如果不存在事务,则新建事务。

如果我们在手动设置propagation参数的时候,把传播特性设置错了,比如:

@Service
public class UserService {

    @Transactional(propagation = Propagation.NEVER)
    public void add(UserModel userModel) {
        saveData(userModel);
        updateData(userModel);
    }
}

我们可以看到add方法的事务传播特性定义成了Propagation.NEVER,这种类型的传播特性不支持事务,如果有事务则会抛异常。

目前只有这三种传播特性才会创建新事务:REQUIRED,REQUIRES_NEW,NESTED。

2.自己吞了异常

事务不会回滚,最常见的问题是:开发者在代码中手动try...catch了异常。比如:

@Slf4j
@Service
public class UserService {
    
    @Transactional
    public void add(UserModel userModel) {
        try {
            saveData(userModel);
            updateData(userModel);
        } catch (Exception e) {
            log.error(e.getMessage(), e);
        }
    }
}

这种情况下spring事务当然不会回滚,因为开发者自己捕获了异常,又没有手动抛出,换句话说就是把异常吞掉了。

如果想要spring事务能够正常回滚,必须抛出它能够处理的异常。如果没有抛异常,则spring认为程序是正常的。

3.手动抛了别的异常

即使开发者没有手动捕获异常,但如果抛的异常不正确,spring事务也不会回滚。

@Slf4j
@Service
public class UserService {
    
    @Transactional
    public void add(UserModel userModel) throws Exception {
        try {
             saveData(userModel);
             updateData(userModel);
        } catch (Exception e) {
            log.error(e.getMessage(), e);
            throw new Exception(e);
        }
    }
}

上面的这种情况,开发人员自己捕获了异常,又手动抛出了异常:Exception,事务同样不会回滚。

因为spring事务,默认情况下只会回滚
RuntimeException
(运行时异常)和
Error
(错误),对于普通的Exception(非运行时异常),它不会回滚。

4.自定义了回滚异常

在使用@Transactional注解声明事务时,有时我们想自定义回滚的异常,spring也是支持的。可以通过设置
rollbackFor
参数,来完成这个功能。

但如果这个参数的值设置错了,就会引出一些莫名其妙的问题,例如:

@Slf4j
@Service
public class UserService {
    
    @Transactional(rollbackFor = BusinessException.class)
    public void add(UserModel userModel) throws Exception {
       saveData(userModel);
       updateData(userModel);
    }
}

如果在执行上面这段代码,保存和更新数据时,程序报错了,抛了SqlException、DuplicateKeyException等异常。而BusinessException是我们自定义的异常,报错的异常不属于BusinessException,所以事务也不会回滚。

即使rollbackFor有默认值,但阿里巴巴开发者规范中,还是要求开发者重新指定该参数。

这是为什么呢?

因为如果使用默认值,一旦程序抛出了Exception,事务不会回滚,这会出现很大的bug。所以,建议一般情况下,将该参数设置成:Exception或Throwable。

5.嵌套事务回滚多了

public class UserService {

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private RoleService roleService;

    @Transactional
    public void add(UserModel userModel) throws Exception {
        userMapper.insertUser(userModel);
        roleService.doOtherThing();
    }
}

@Service
public class RoleService {

    @Transactional(propagation = Propagation.NESTED)
    public void doOtherThing() {
        System.out.println("保存role表数据");
    }
}

这种情况使用了嵌套的内部事务,原本是希望调用roleService.doOtherThing方法时,如果出现了异常,只回滚doOtherThing方法里的内容,不回滚 userMapper.insertUser里的内容,即回滚保存点。。但事实是,insertUser也回滚了。

why?

因为doOtherThing方法出现了异常,没有手动捕获,会继续往上抛,到外层add方法的代理方法中捕获了异常。所以,这种情况是直接回滚了整个事务,不只回滚单个保存点。

怎么样才能只回滚保存点呢?

@Slf4j
@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private RoleService roleService;

    @Transactional
    public void add(UserModel userModel) throws Exception {

        userMapper.insertUser(userModel);
        try {
            roleService.doOtherThing();
        } catch (Exception e) {
            log.error(e.getMessage(), e);
        }
    }
}

可以将内部嵌套事务放在try/catch中,并且不继续往上抛异常。这样就能保证,如果内部嵌套事务中出现异常,只回滚内部事务,而不影响外部事务。

三 其他

1 大事务问题

在使用spring事务时,有个让人非常头疼的问题,就是大事务问题。

通常情况下,我们会在方法上
@Transactional
注解,填加事务功能,比如:

@Service
public class UserService {
    
    @Autowired 
    private RoleService roleService;
    
    @Transactional
    public void add(UserModel userModel) throws Exception {
       query1();
       query2();
       query3();
       roleService.save(userModel);
       update(userModel);
    }
}


@Service
public class RoleService {
    
    @Autowired 
    private RoleService roleService;
    
    @Transactional
    public void save(UserModel userModel) throws Exception {
       query4();
       query5();
       query6();
       saveData(userModel);
    }
}


@Transactional
注解,如果被加到方法上,有个缺点就是整个方法都包含在事务当中了。

上面的这个例子中,在UserService类中,其实只有这两行才需要事务:

roleService.save(userModel);
update(userModel);

在RoleService类中,只有这一行需要事务:

saveData(userModel);

现在的这种写法,会导致所有的query方法也被包含在同一个事务当中。

如果query方法非常多,调用层级很深,而且有部分查询方法比较耗时的话,会造成整个事务非常耗时,而从造成大事务问题。

关于大事务问题的危害,可以阅读一下我的另一篇文章《
让人头痛的大事务问题到底要如何解决?
》,上面有详细的讲解。

2.编程式事务

上面聊的这些内容都是基于
@Transactional
注解的,主要说的是它的事务问题,我们把这种事务叫做:
声明式事务

其实,spring还提供了另外一种创建事务的方式,即通过手动编写代码实现的事务,我们把这种事务叫做:
编程式事务
。例如:


   @Autowired
   private TransactionTemplate transactionTemplate;
   
   ...
   
   public void save(final User user) {
         queryData1();
         queryData2();
         transactionTemplate.execute((status) => {
            addData1();
            updateData2();
            return Boolean.TRUE;
         })
   }

在spring中为了支持编程式事务,专门提供了一个类:TransactionTemplate,在它的execute方法中,就实现了事务的功能。

相较于
@Transactional
注解声明式事务,我更建议大家使用,基于
TransactionTemplate
的编程式事务。主要原因如下:

  1. 避免由于spring aop问题,导致事务失效的问题。
  2. 能够更小粒度的控制事务的范围,更直观。

建议在项目中少使用@Transactional注解开启事务。但并不是说一定不能用它,如果项目中有些业务逻辑比较简单,而且不经常变动,使用@Transactional注解开启事务开启事务也无妨,因为它更简单,开发效率更高,但是千万要小心事务失效的问题。

最后说一句(求关注,别白嫖我)

如果这篇文章对您有所帮助,或者有所启发的话,帮忙扫描下发二维码关注一下,您的支持是我坚持写作最大的动力。
求一键三连:点赞、转发、在看。
关注公众号:【苏三说技术】,在公众号中回复:进大厂,可以免费获取我最近整理的10万字的面试宝典,好多小伙伴靠这个宝典拿到了多家大厂的offer。

上周赠送了
OneThingAI 算力代金券

天翼云电脑
,这周继续给园子会员送福利。

这周赠送的是一年华为云服务器,配置是2核2G3M带宽,限量100台,先到先得,领完为止。

领取要求:博客园年度VIP或者PLUS
会员
,华为云新用户或者在华为云没有过任何消费的老用户。

领取方式:加园子
企业微信
,加好友时请备注【领华为云服务器】,或者给「博客园团队」企微发消息。

如果符合领取条件,会将您加入专门领华为云服务器的微信群,群中有一个Wod文档,按照文档中的流程完成领取,领取过程中遇到任何问题可以在群中咨询。

负责发放云服务器的是华为云服务商浙江杭云网络科技有限公司,也是园子的合作伙伴,园子的
华为云代理业务
就是和他们合作的,如果您有华为云云产品购买需求,也欢迎和我们联系。