首页
关于
Search
1
同步本地Markdown至Typecho站点
88 阅读
2
微服务
41 阅读
3
苍穹外卖
32 阅读
4
JavaWeb——后端
25 阅读
5
消息队列MQ
20 阅读
后端学习
项目
杂项
科研
论文
默认分类
登录
推荐文章
推荐
拼团设计模式
项目
8月11日
0
8
0
推荐
拼团交易系统
项目
6月20日
0
26
1
推荐
Smile云图库
项目
6月7日
0
37
0
热门文章
88 ℃
同步本地Markdown至Typecho站点
项目
3月22日
0
88
0
41 ℃
微服务
后端学习
3月21日
0
41
0
32 ℃
苍穹外卖
项目
3月21日
0
32
0
最新发布
2025-03-21
Mysql数据库
Mysql数据库 安装启动Mysql mysql 是 MySQL 的命令行客户端工具,用于连接、查询和管理 MySQL 数据库。 你可以通过它来执行 SQL 命令、查看数据和管理数据库。 mysqld 是 MySQL 服务器守护进程,也就是 MySQL 数据库的实际运行程序。 它负责处理数据库的存储、查询、并发访问、用户验证等核心任务。 添加环境变量: 将'\path\to\mysql-8.0.31-winx64\bin\'目录添加到 PATH 环境变量中,便于命令行操作。 启动Mysql net start mysql // 启动mysql服务 net stop mysql // 停止mysql服务 修改root账户密码 mysqladmin -u root password 123456 本地windows下的账号:root 密码: 123456 登录 mysql -u用户名 -p密码 [-h数据库服务器的IP地址 -P端口号] mysql -uroot -p123456 -h 参数不加,默认连接的是本地 127.0.0.1 的MySQL服务器 -P 参数不加,默认连接的端口号是 3306 图形化工具 推荐Navicat Mysql简介 通用语法 1、SQL语句可以单行或多行书写,以分号结尾。 2、SQL语句可以使用空格/缩进来增强语句的可读性。因为SQL语句在执行时,数据库会忽略额外的空格和换行符 SELECT name, age, address FROM users WHERE age > 18; 3、MySQL数据库的SQL语句不区分大小写。 4、注释: 单行注释:-- 注释内容 或 # 注释内容(MySQL特有) 多行注释: /* 注释内容 */ 分类 分类 全称 说明 DDL Data Definition Language 数据定义语言,用来定义数据库对象(数据库,表,字段) DML Data Manipulation Language 数据操作语言,用来对数据库表中的数据进行增删改 DQL Data Query Language 数据查询语言,用来查询数据库中表的记录 DCL Data Control Language 数据控制语言,用来创建数据库用户、控制数据库的访问权限 数据类型 字符串类型 CHAR(n):声明的字段如果数据类型为char,则该字段占据的长度固定为声明时的值,例如:char(4),存入值 'ab',其长度仍为4. VARCHAR(n):varchar(100)表示最多可以存100个字符,每个字符占用的字节数取决于所使用的字符集。存储开销:除了存储实际数据外,varchar 类型还会额外存储 1 或 2 个字节来记录字符串的长度。 TEXT:用于存储大文本数据,存储长度远大于 VARCHAR,但不支持索引整列内容(通常索引长度有限制)。 日期时间类型: 类型 大小 范围 格式 描述 DATE 3 1000-01-01 至 9999-12-31 YYYY-MM-DD 日期值 TIME 3 -838:59:59 至 838:59:59 HH:MM:SS 时间值或持续时间 DATETIME 8 1000-01-01 00:00:00 至 9999-12-31 23:59:59 YYYY-MM-DD HH:MM:SS 混合日期和时间值 注意:字符串和日期时间型数据在 SQL 语句中应包含在引号内,例如:'2025-03-29'、'hello'。 数值类型 类型 大小 有符号(SIGNED)范围 无符号(UNSIGNED)范围 描述 TINYINT 1byte (-128,127) (0,255) 小整数值 INT/INTEGER 4bytes (-2^31,2^31-1) (0,2^32-1) 大整数值 FLOAT 4bytes (-3.402823466 E+38,3.402823466351 E+38) 0 和 (1.175494351 E-38,3.402823466 E+38) 单精度浮点数值 DECIMAL 依赖于M(精度)和D(标度)的值 依赖于M(精度)和D(标度)的值 小数值(精确定点数) DECIMAL(M, D):定点数类型,M 表示总位数,D 表示小数位数,适合存储精度要求较高的数值(如金钱)。 DDL(数据定义语言) 数据库操作 查询所有数据库: show databases; 创建一个itcast数据库。 create database itcast; 切换到itcast数据 use itcast; 查询当前正常使用的数据库: select database(); 删除itcast数据库 drop database if exists itcast; -- itcast数据库存在时删除,不存在也不报错 表操作 查询当前数据库下所有表 show tables; 查看指定表的结构(字段) desc tb_tmps; ( tb_tmps为表名) 创建表 通常一个列定义的顺序如下: 列名(字段) 字段类型 可选的字符集或排序规则(如果需要) 约束:例如 NOT NULL、UNIQUE、PRIMARY KEY、DEFAULT 等 特殊属性:例如 AUTO_INCREMENT 注释:例如 COMMENT '说明' create table 表名( 字段1 字段1类型 [约束] [comment '字段1注释' ], 字段2 字段2类型 [约束] [comment '字段2注释' ], ...... 字段n 字段n类型 [约束] [comment '字段n注释' ] ) [ comment '表注释' ] ; 注意: [ ] 中的内容为可选参数; 最后一个字段后面没有逗号 eg: create table tb_user ( id int comment 'ID,唯一标识', # id是一行数据的唯一标识(不能重复) username varchar(20) comment '用户名', name varchar(10) comment '姓名', age int comment '年龄', gender char(1) comment '性别' ) comment '用户表'; 复制某个表的结构: CREATE TABLE new_table LIKE old_table; 删除表 DROP TABLE demo; 约束 约束 描述 关键字 非空约束 限制该字段值不能为null not null 唯一约束 保证字段的所有数据都是唯一、不重复的 unique 主键约束 主键是一行数据的唯一标识,要求非空且唯一 primary key 默认约束 保存数据时,如果未指定该字段值,则采用默认值 default 外键约束 让两张表的数据建立连接,保证数据的一致性和完整性 foreign key CREATE TABLE tb_user ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID,唯一标识', username VARCHAR(20) NOT NULL UNIQUE COMMENT '用户名', name VARCHAR(10) NOT NULL COMMENT '姓名', age INT COMMENT '年龄', gender CHAR(1) DEFAULT '男' COMMENT '性别' ) COMMENT '用户表'; -- 假设我们有一个 orders 表,它将 tb_user 表的 id 字段作为外键 CREATE TABLE orders ( order_id INT PRIMARY KEY AUTO_INCREMENT COMMENT '订单ID', order_date DATE COMMENT '订单日期', user_id INT, FOREIGN KEY (user_id) REFERENCES tb_user(id) ON DELETE CASCADE ON UPDATE CASCADE, COMMENT '订单表' ); foreign key: 保证数据的一致性和完整性 ON DELETE CASCADE:如果父表中的某行被删除,那么子表中所有与之关联的行也会被自动删除。 ON DELETE SET NULL:如果父表中的某行被删除,子表中的相关外键列会被设置为 NULL。 ON UPDATE CASCADE:如果父表中的外键值被更新,那么子表中的相关外键值也会自动更新。 注意:在实际的 Java 项目中,特别是在一些微服务架构或分布式系统中,通常不直接依赖数据库中的外键约束。相反,开发者通常会在代码中通过逻辑来确保数据的一致性和完整性。 auto_increment: 每次插入新的行记录时,数据库自动生成id字段(主键)下的值 具有auto_increment的数据列是一个正数序列且整型(从1开始自增) 不能应用于多个字段 设计表的字段时,还应考虑: id:主键,唯一标志这条记录 create_time :插入记录的时间 now()函数可以获取当前时间 update_time:最后修改记录的时间 DCL(数据控制语言) GRANT(授予权限) GRANT 权限列表 ON 数据库对象 TO 用户名 [WITH GRANT OPTION]; # 授予用户 user1对表 students的 查询权限: GRANT SELECT ON students TO user1; REVOKE(撤销权限) REVOKE 权限列表 ON 数据库对象 FROM 用户名; # 撤销用户 user1对表 students的 修改权限: REVOKE UPDATE ON students FROM user1; DML(增删改) DML英文全称是Data Manipulation Language(数据操作语言),用来对数据库中表的数据记录进行增、删、改操作。 添加数据(INSERT) 修改数据(UPDATE) 删除数据(DELETE) INSERT insert语法: 向指定字段添加数据 insert into 表名 (字段名1, 字段名2) values (值1, 值2); 全部字段添加数据 insert into 表名 values (值1, 值2, ...); 批量添加数据(指定字段) insert into 表名 (字段名1, 字段名2) values (值1, 值2), (值1, 值2); 批量添加数据(全部字段) insert into 表名 values (值1, 值2, ...), (值1, 值2, ...); “如果不存在,就插入一条新记录;如果已存在,则返回已存在那条的 id。” INSERT INTO orders (user_id, idempotency_key, ...) VALUES (:uid, :key, ...) ON DUPLICATE KEY UPDATE id = LAST_INSERT_ID(id); SELECT LAST_INSERT_ID() AS order_id; 保证操作是幂等的;同一 idempotency_key 的重复请求,只会返回同一条订单 ID。 UPDATE update语法: update 表名 set 字段名1 = 值1 , 字段名2 = 值2 , .... [where 条件] ; 案例1:将tb_emp表中id为1的员工,姓名name字段更新为'张三' update tb_emp set name='张三',update_time=now() where id=1; 案例2:将tb_emp表的所有员工入职日期更新为'2010-01-01' update tb_emp set entrydate='2010-01-01',update_time=now(); **注意!**不带where会更新表中所有记录! DELETE delete语法: delete from 表名 [where 条件] ; 案例1:删除tb_emp表中id为1的员工 delete from tb_emp where id = 1; 案例2:删除tb_emp表中所有员工(记录) delete from tb_emp; DELETE 语句不能删除某一个字段的值(可以使用UPDATE,将该字段值置为NULL即可)。 DQL(查询) DQL英文全称是Data Query Language(数据查询语言),用来查询数据库表中的记录。 查询关键字:SELECT 查询操作是所有SQL语句当中最为常见,也是最为重要的操作。 语法 SELECT 字段列表 FROM 表名列表 ----基本查询 WHERE 条件列表 ----条件查询 GROUP BY 分组字段列表 HAVING 分组后条件列表 ----分组查询 ORDER BY 排序字段列表 ----排序查询 LIMIT 分页参数 ----分页查询 基本查询 查询多个字段 select 字段1, 字段2, 字段3 from 表名; 查询所有字段(通配符) select * from 表名; 设置别名 select 字段1 [ as 别名1 ] , 字段2 [ as 别名2 ] from 表名; 去除重复记录 select distinct 字段列表 from 表名; eg:select distinct job from tb_emp; 条件查询 比较运算符 功能 between ... and ... 在某个范围之内(含最小、最大值) in(...) 在in之后的列表中的值,多选一 like 占位符 模糊匹配(_匹配单个字符, %匹配任意个字符) is null 是null = 等于 逻辑运算符 功能 and 或 && 并且 (多个条件同时成立) or 或 || 或者 (多个条件任意一个成立) not 或 ! 非 , 不是 表数据: id name gender job entrydate 1 张三 2 2 2005-04-15 2 李四 1 3 2007-07-22 3 王五 2 4 2011-09-01 4 赵六 1 2 2008-06-11 案例1:查询 入职时间 在 '2000-01-01' (包含) 到 '2010-01-01'(包含) 之间 且 性别为女 的员工信息 select * from tb_emp where entrydate between '2000-01-01' and '2010-01-01' and gender = 2; 案例2:查询 职位是 2 (讲师), 3 (学工主管), 4 (教研主管) 的员工信息 select * from tb_emp where job in (2,3,4); 案例3:查询 姓名 为两个字的员工信息 常见的 LIKE 模式匹配符包括: %:表示零个或多个字符。 _:表示一个字符。 select * from tb_emp where name like '__'; # 通配符 "_" 代表任意1个字符 字符串、数值函数 字符串函数: 函数 作用 示例 结果 LENGTH(str) 字符串长度(字节) LENGTH('abc') 3 CHAR_LENGTH(str) 字符数(推荐用这个) CHAR_LENGTH('中文') 2 CONCAT(s1, s2, …) 拼接字符串 CONCAT('a','b') ab UPPER(str) 转大写 UPPER('abc') ABC LOWER(str) 转小写 LOWER('ABC') abc SUBSTRING(str, pos, len) 截取子串 SUBSTRING('abcdef',2,3) bcd REPLACE(str, from, to) 替换 REPLACE('a,b,c',',','-') a-b-c TRIM(str) 去掉首尾空格 TRIM(' hi ') hi 示例:清理和格式化 SELECT TRIM(LOWER(name)) AS clean_name, LOWER(email) AS email_lower FROM users; 数值函数: 函数 作用 示例 结果 ABS(x) 绝对值 ABS(-5) 5 ROUND(x, d) 四舍五入 ROUND(3.1415,2) 3.14 CEIL(x) 向上取整 CEIL(2.3) 3 FLOOR(x) 向下取整 FLOOR(2.9) 2 MOD(x, y) 取余数 MOD(10,3) 1 ROUND(x) 表示四舍五入到整数。 示例:计算总价并四舍五入 SELECT product, ROUND(price * quantity, 2) AS total_price FROM sales; 聚合函数 之前我们做的查询都是横向查询,就是根据条件一行一行的进行判断,而使用聚合函数查询就是纵向查询,它是对分组后的每一组数据在纵向方向上进行计算。如果没有分组,则默认整张表是“一组”。 聚合函数: 函数 功能 count 统计数量 max 最大值 min 最小值 avg 平均值 sum 求和 语法: select 聚合函数(字段名、列名) from 表名 ; 注意 : 聚合函数会忽略空值,对NULL值不作为统计。 # count(*) 推荐此写法(MySQL底层进行了优化) select count(*) from tb_emp; -- 统计记录数 SELECT SUM(amount) FROM tb_sales; -- 统计amount列的总金额 组合使用字符串、数值函数和聚合函数 1)聚合函数放里层,数值/字符串函数放外层(最常见) SELECT CONCAT(UPPER(student_name), ' 平均分: ', ROUND(AVG(score), 1)) AS summary FROM students s JOIN exam_results e ON s.student_id = e.student_id GROUP BY s.student_name; AVG(price) → 先求平均值(聚合) ROUND(..., 2) → 再四舍五入保留 2 位小数 2)先处理再聚合 SELECT AVG(ROUND(score)) AS avg_rounded_score FROM exam_results; 每次考试的成绩先 ROUND(score) 四舍五入; 再计算平均值。 3)错误示例 -- ❌ 错误示例:聚合函数嵌在聚合函数内 SELECT SUM(AVG(price)) FROM products; SQL 不允许聚合函数再嵌套聚合函数。 分组+Having过滤 分组: 分组其实就是按列进行分类(指定列下相同的数据归为一类),然后可以对分类完的数据进行合并计算。 分组查询通常会使用聚合函数进行计算。 没有 GROUP BY 的话,所有行就是一组。 select 字段列表 from 表名 [where 条件] group by 分组字段名 [having 分组后过滤条件]; orders表: customer_id amount 1 100 1 200 2 150 2 300 例如,假设我们有一个名为 orders 的表,其中包含 customer_id 和 amount 列,我们想要计算每个客户的订单总金额,可以这样写查询: SELECT customer_id, SUM(amount) AS total_amount FROM orders GROUP BY customer_id; 结果: customer_id total_amount 2 450 1 300 在这个例子中,GROUP BY customer_id 将结果按照 customer_id 列的值进行分组,并对每个客户的订单金额求和,生成每个客户的总金额。 过滤 SELECT customer_id, SUM(amount) AS total_amount FROM orders GROUP BY customer_id HAVING total_amount > specified_amount; 在这个查询中,HAVING 子句用于筛选出消费金额(total_amount)大于指定数目(specified_amount)的记录。你需要将 specified_amount 替换为你指定的金额数目。 注意事项: 分组之后,查询的字段一般为聚合函数和分组字段,查询其他字段无任何意义 执行顺序:WHERE → GROUP BY(分组)→ 聚合函数 → HAVING WHERE 在聚合前过滤,HAVING 在聚合后过滤。 进阶 那如果按多个字段分组呢? 农场表 farm_id farm_name 1 Green Valley Farm 2 Sunshine Acres 作物表 crop_id crop_name crop_type 1 小麦 谷物 2 玉米 谷物 3 水稻 谷物 4 番茄 蔬菜 作物种植数据 farm_id crop_id ... 1 1 ... 1 2 ... 2 3 ... 2 4 ... 农场1(Green Valley Farm)有:小麦、玉米 农场2(Sunshine Acres)有:水稻、番茄 按作物统计(跨农场) GROUP BY crop_name 一行一个作物 按农场统计(跨作物) GROUP BY farm_name 一行一个农场 按农场 + 作物统计 GROUP BY farm_name, crop_name 一行代表农场中的一种作物 按农场 + 作物 + 类型统计 GROUP BY farm_name, crop_name, crop_type 最细粒度 注意:只能 SELECT 分组字段或聚合函数 排序查询 语法: select 字段列表 from 表名 [where 条件列表] [group by 分组字段 ] order by 字段1 排序方式1 , 字段2 排序方式2 … ; 排序方式: ASC :升序(默认值) DESC:降序 select id, username, password, name, gender, image, job, entrydate, create_time, update_time from tb_emp order by entrydate ASC; -- 按照entrydate字段下的数据进行升序排序 分页查询 有两种写法: select 字段列表 from 表名 limit 起始索引(offset), 每页显示记录数 ; # 推荐 select 字段列表 from 表名 limit 记录数 OFFSET 起始索引; 前端传过来的一般是页码,要计算起始索引 两者并不等价!!! 注意事项: 起始索引从0开始。 计算公式 : 起始索引 = (查询页码 - 1)* 每页显示记录数 分页查询是数据库的方言,不同的数据库有不同的实现,MySQL中是LIMIT 如果查询的是第一页数据,起始索引可以省略,直接简写为 limit 条数 例子 1:第 3 页,每页 20 条 SELECT id, title, created_at FROM post WHERE status = 'PUBLISHED' ORDER BY created_at DESC, id DESC LIMIT 20 OFFSET 40; -- 等价: LIMIT 40, 20 多表设计 外键约束 外键约束的语法: -- 创建表时指定 CREATE TABLE child_table ( id INT PRIMARY KEY, parent_id INT, -- 外键字段 FOREIGN KEY (parent_id) REFERENCES parent_table (id) ON DELETE CASCADE -- 可选,表示父表数据删除时,子表数据也会删除 ON UPDATE CASCADE -- 可选,表示父表数据更新时,子表数据会同步更新 ); -- 建完表后,添加外键 ALTER TABLE child_table ADD CONSTRAINT fk_parent_id -- 外键约束的名称,可选 FOREIGN KEY (parent_id) REFERENCES parent_table (id) ON DELETE CASCADE ON UPDATE CASCADE; 一对多 一对多关系实现:在数据库表中多的一方,添加外键字段(如dept_id),来关联'一'这方的主键(id)。 一对一 一对一关系表在实际开发中应用起来比较简单,通常是用来做单表的拆分。一对一的应用场景: 用户表=》基本信息表+身份信息表,以此来提高数据的操作效率。 基本信息:用户的ID、姓名、性别、手机号、学历 身份信息:民族、生日、身份证号、身份证签发机关,身份证的有效期(开始时间、结束时间) 一对一 :在任意一方加入外键,关联另外一方的主键,并且设置外键为唯一的(UNIQUE) 多对多 多对多的关系在开发中属于也比较常见的。比如:学生和老师的关系,一个学生可以有多个授课老师,一个授课老师也可以有多个学生。 案例:学生与课程的关系 关系:一个学生可以选修多门课程,一门课程也可以供多个学生选择 实现关系:建立第三张中间表(选课表),中间表至少包含两个外键,分别关联两方主键 多表查询 分类 多表查询可以分为: 连接查询 内连接:相当于查询A、B交集部分数据 外连接 左外连接:查询左表所有数据(包括两张表交集部分数据) 右外连接:查询右表所有数据(包括两张表交集部分数据) 子查询 内连接 内连接(INNER JOIN)只返回两个表中连接条件完全匹配的行。如果某行在一个表中存在但在另一个表中没有匹配项,则该行不会出现在结果集中。 隐式内连接语法: select 字段列表 from 表1 , 表2 where 条件 ... ; 显式内连接语法: select 字段列表 from 表1 [ inner ] join 表2 on 连接条件 ... ; [inner]可省略 案例:查询员工的姓名及所属的部门名称 隐式内连接实现 select tb_emp.name , tb_dept.name -- 分别查询两张表中的数据 from tb_emp , tb_dept -- 关联两张表 where tb_emp.dept_id = tb_dept.id; -- 消除笛卡尔积 显示内连接 select tb_emp.name , tb_dept.name from tb_emp inner join tb_dept on tb_emp.dept_id = tb_dept.id; 使用场景:获取严格关联的数据(查询所有有订单的客户信息) 外连接 左外连接语法结构: select 字段列表 from 表1 left [ outer ] join 表2 on 连接条件 ... ; 左外连接相当于查询表1(左表)的所有行,当然也包含表1和表2交集部分的数据。 右外连接语法结构: select 字段列表 from 表1 right [ outer ] join 表2 on 连接条件 ... ; 右外连接相当于查询表2(右表)的所有行,当然也包含表1和表2交集部分的数据。 案例:查询所有员工及其部门信息(包括没有分配部门的员工) -- 左外连接:以left join关键字左边的表为主表,查询主表中所有数据,以及和主表匹配的右边表中的数据 select emp.name , dept.name from tb_emp AS emp left join tb_dept AS dept on emp.dept_id = dept.id; 外连接的作用:1.查询主表所有记录及相关联信息(查询所有员工及其部门信息(包括没有分配部门的员工)) 2.查找缺失数据(找出没有订单的客户) FULL OUTER JOIN(全外连接) 左外连接 (LEFT JOIN):保留左表全部 + 右表匹配 右外连接 (RIGHT JOIN):保留右表全部 + 左表匹配 全外连接 (FULL OUTER JOIN):保留左表全部 + 右表全部,即“左外 ∪ 右外” MySQL 的实现方式:用 LEFT JOIN ∪ RIGHT JOIN -- 左外连接:保证所有员工都能查出来 select emp.*, dept.deptName from t_emp emp left join t_dept dept on emp.deptId = dept.id union -- 右外连接:保证所有部门都能查出来 select emp.*, dept.deptName from t_emp emp right join t_dept dept on emp.deptId = dept.id; 子查询 SQL语句中嵌套select语句,称为嵌套查询,又称子查询。 SELECT * FROM t1 WHERE column1 = ( SELECT column1 FROM t2 ... ); 子查询外部的语句可以是insert / update / delete / select 的任何一个,最常见的是 select。 标量子查询 子查询返回的结果是单个值(数字、字符串、日期等),最简单的形式,这种子查询称为标量子查询。 常用的操作符: = <> > >= < <= 案例1:查询"教研部"的所有员工信息 可以将需求分解为两步: 查询 "教研部" 部门ID 根据 "教研部" 部门ID,查询员工信息 -- 1.查询"教研部"部门ID select id from tb_dept where name = '教研部'; #查询结果:2 -- 2.根据"教研部"部门ID, 查询员工信息 select * from tb_emp where dept_id = 2; -- 合并出上两条SQL语句 select * from tb_emp where dept_id = (select id from tb_dept where name = '教研部'); 列子查询 子查询返回的结果是一列(可以是多行,即多条记录),这种子查询称为列子查询。 常用的操作符: 操作符 描述 IN 在指定的集合范围之内,多选一 NOT IN 不在指定的集合范围之内 案例:查询"教研部"和"咨询部"的所有员工信息 分解为以下两步: 查询 "销售部" 和 "市场部" 的部门ID 根据部门ID, 查询员工信息 -- 1.查询"销售部"和"市场部"的部门ID select id from tb_dept where name = '教研部' or name = '咨询部'; #查询结果:3,2 -- 2.根据部门ID, 查询员工信息 select * from tb_emp where dept_id in (3,2); -- 合并以上两条SQL语句 select * from tb_emp where dept_id in (select id from tb_dept where name = '教研部' or name = '咨询部'); 行子查询 子查询返回的结果是一行(可以是多列,即多字段),这种子查询称为行子查询。 常用的操作符:= 、<> 、IN 、NOT IN 案例:查询与"韦一笑"的入职日期及职位都相同的员工信息 可以拆解为两步进行: 查询 "韦一笑" 的入职日期 及 职位 查询与"韦一笑"的入职日期及职位相同的员工信息 -- 查询"韦一笑"的入职日期 及 职位 select entrydate , job from tb_emp where name = '韦一笑'; #查询结果: 2007-01-01 , 2 -- 查询与"韦一笑"的入职日期及职位相同的员工信息 select * from tb_emp where (entrydate,job) = ('2007-01-01',2); -- 合并以上两条SQL语句 select * from tb_emp where (entrydate,job) = (select entrydate , job from tb_emp where name = '韦一笑'); 表子查询 子查询返回的结果是多行多列,常作为临时表,这种子查询称为表子查询。 案例:查询入职日期是 "2006-01-01" 之后的员工信息 , 及其部门信息 分解为两步执行: 查询入职日期是 "2006-01-01" 之后的员工信息 基于查询到的员工信息,在查询对应的部门信息 select * from emp where entrydate > '2006-01-01'; select e.*, d.* from (select * from emp where entrydate > '2006-01-01') e left join dept d on e.dept_id = d.id ; 存储过程/函数 存储过程 把一段 SQL 逻辑封装起来,存到数据库里, 以后只要执行名字,就能重复调用。 可以执行 INSERT/UPDATE/DELETE,可返回多结果集。 用法举例: DELIMITER // CREATE PROCEDURE add_user(IN username VARCHAR(50), IN age INT) BEGIN INSERT INTO users(name, age, created_at) VALUES (username, age, NOW()); END // DELIMITER ; 调用: CALL add_user('Alice', 25); 补充语法说明: 1)DELIMITER 用来修改 MySQL 命令结束符(分隔符)。 默认情况下,MySQL 认为每条 SQL 命令以 ; 结束。但是存储过程里,本身也会包含很多语句(每句都要以 ; 结尾)。如果不改分隔符,MySQL 就会误以为存储过程还没写完就已经结束了。 2)BEGIN ... END 用来包裹一段可执行的语句块。 3)SELECT age INTO age_val FROM users WHERE id = 1; 把结果存入变量 age_val 中 函数 函数和存储过程很像,但它必须返回一个值,通常用于查询计算(比如求平均、格式化、拼接等)。 一般只读(不能修改数据),只能返回一个标量。 DELIMITER // CREATE PROCEDURE get_user_age(IN uid INT) BEGIN DECLARE age_val INT; -- 从 users 表取 age,赋给变量 age_val SELECT age INTO age_val FROM users WHERE id = uid; -- 输出结果 SELECT CONCAT('用户年龄是: ', age_val); END // DELIMITER ; 执行: CALL get_user_age(5); 结果: 用户年龄是: 23 游标 游标(Cursor)是用来逐行读取查询结果集的指针。 普通的 SQL 一次会返回整张表的结果: SELECT name, age FROM users; 但在存储过程中,有时你希望像“遍历数组”一样——一行一行地取数据、处理逻辑、再取下一行。这时就要用到 游标。 游标使用的4个步骤: 步骤 关键语句 说明 1️⃣ 声明游标 DECLARE cursor_name CURSOR FOR SELECT ... 定义要遍历的结果集 2️⃣ 打开游标 OPEN cursor_name; 执行查询,准备读取 3️⃣ 取数据 FETCH cursor_name INTO var1, var2; 每次取一行结果 4️⃣ 关闭游标 CLOSE cursor_name; 释放资源 遍历所有用户,把他们的年龄大于 30 的标记为“中年用户”。 DELIMITER // CREATE PROCEDURE mark_middle_age() BEGIN -- 定义变量 DECLARE done INT DEFAULT 0; DECLARE uid INT; DECLARE uage INT; -- 定义游标(要遍历的结果集) DECLARE cur CURSOR FOR SELECT id, age FROM users; -- 定义结束条件(游标读完) DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = 1; -- 打开游标 OPEN cur; -- 开始循环读取 read_loop: LOOP FETCH cur INTO uid, uage; -- 把当前行取出到变量 IF done THEN LEAVE read_loop; -- 没数据就退出循环 END IF; -- 条件判断逻辑 IF uage >= 30 THEN UPDATE users SET tag = '中年用户' WHERE id = uid; END IF; END LOOP; -- 关闭游标 CLOSE cur; END // DELIMITER ; 事务 事务是一组操作的集合,它是一个不可分割的工作单位。事务会把所有的操作作为一个整体一起向系统提交或撤销操作请求,即这些操作要么同时成功,要么同时失败。 手动提交事务使用步骤: 第1种情况:开启事务 => 执行SQL语句 => 成功 => 提交事务 第2种情况:开启事务 => 执行SQL语句 => 失败 => 回滚事务 -- 开启事务 start transaction ; -- 删除学工部 delete from tb_dept where id = 1; -- 删除学工部的员工 delete from tb_emp where dept_id = 1; 上述的这组SQL语句,如果如果执行成功,则提交事务 -- 提交事务 (成功时执行) commit ; 上述的这组SQL语句,如果如果执行失败,则回滚事务 -- 回滚事务 (出错时执行) rollback ; 面试题:事务有哪些特性? 1)原子性(Atomicity):事务是不可分割的最小单元,要么全部成功,要么全部失败。 2)一致性(Consistency):事务完成时,必须使所有的数据都保持一致状态。 约束层面一致性主要指 数据要符合数据库定义的各种约束,主键约束、外键约束、唯一约束等必须始终成立。 业务层面一致性:部门和该部门下的员工数据全部删除 3)隔离性(Isolation):数据库系统提供的隔离机制,保证事务在不受外部并发操作影响的独立环境下运行(事务还没commit,那么别的窗口就看不到该修改 )。在 MySQL(InnoDB 引擎)里,隔离性主要是通过 事务隔离级别 + MVCC(多版本并发控制) + 锁机制 来实现的。 SQL 标准定义了 4 种隔离级别(MySQL 都支持): 读未提交 (READ UNCOMMITTED) 能读到别的事务未提交的数据(脏读)。 几乎没隔离,效率最高。 读已提交 (READ COMMITTED) 只能读到别的事务已提交的数据。 Oracle 默认级别,避免脏读,但会出现不可重复读。 可重复读 (REPEATABLE READ) MySQL 默认级别。 在同一个事务里,多次读同一行结果一致(避免不可重复读)。 InnoDB 在此级别下还能避免幻读(通过间隙锁)。 串行化 (SERIALIZABLE) 所有事务串行执行(加锁),最安全但效率最低。 4)持久性(Durability):事务一旦提交或回滚,它对数据库中的数据的改变就是永久的。 事务的四大特性简称为:ACID 索引 索引(index):是帮助数据库高效获取数据的数据结构 。 创建索引 -- 添加索引 create index idx_sku_sn on tb_sku (sn); #在添加索引时,也需要消耗时间 -- 查询数据(使用了索引) select * from tb_sku where sn = '100000003145008'; 查看索引 show index from 表名; 案例:查询 tb_emp 表的索引信息 show index from tb_emp; 删除索引 drop index 索引名 on 表名; 案例:删除 tb_emp 表中name字段的索引 drop index idx_emp_name on tb_emp; 优点: 提高数据查询的效率,降低数据库的IO成本。 通过索引列对数据进行排序,降低数据排序的成本,降低CPU消耗。 缺点: 索引会占用存储空间。 索引大大提高了查询效率,同时却也降低了insert、update、delete的效率。 因为插入一条数据,要重新维护索引结构 注意事项: 主键字段,在建表时,会自动创建主键索引 (primarily key) 添加唯一约束时,数据库实际上会添加唯一索引 (unique约束) 结构 mysql默认采用B+树来作索引 采用二叉搜索树或者是红黑树来作为索引的结构有什么问题? 答案 最大的问题就是在数据量大的情况下,树的层级比较深,会影响检索速度。因为不管是二叉搜索数还是红黑数,一个节点下面只能有两个子节点。此时在数据量大的情况下,就会造成数的高度比较高,树的高度一旦高了,检索速度就会降低。 说明:如果数据结构是红黑树,那么查询1000万条数据,根据计算树的高度大概是23左右,这样确实比之前的方式快了很多,但是如果高并发访问,那么一个用户有可能需要23次磁盘IO,那么100万用户,那么会造成效率极其低下。所以为了减少红黑树的高度,那么就得增加树的宽度,就是不再像红黑树一样每个节点只能保存一个数据,可以引入另外一种数据结构,一个节点可以保存多个数据,这样宽度就会增加从而降低树的高度。这种数据结构例如BTree就满足。 下面我们来看看B+Tree(多路平衡搜索树)结构中如何避免这个问题: B+Tree结构: 每一个节点,可以存储多个key(有n个key,就有n个指针) 节点分为:叶子节点、非叶子节点 叶子节点,就是最后一层子节点,所有的数据都存储在叶子节点上 非叶子节点,不是树结构最下面的节点,用于索引数据,存储的的是:key+指针 为了提高范围查询效率,叶子节点形成了一个双向链表,便于数据的排序及区间范围查询
后端学习
zy123
3月21日
0
4
0
2025-03-21
JavaWeb——前端
JavaWeb JavaWeb学习路线 前后端分离开发 需求分析:首先我们需要阅读需求文档,分析需求,理解需求。 接口定义:查询接口文档中关于需求的接口的定义,包括地址,参数,响应数据类型等等 前后台并行开发:各自按照接口文档进行开发,实现需求 测试:前后台开发完了,各自按照接口文档进行测试 前后段联调测试:前段工程请求后端工程,测试功能 Html/CSS 标签速记: 不闭合标签: 空格占位符: 正文格式:line-height:设置行高;text-indent:设置首行缩进;text-align:规定文本的水平对齐方式 CSS引入方式 名称 语法描述 示例 行内样式 在标签内使用style属性,属性值是css属性键值对 <h1 style="xxx:xxx;">中国新闻网</h1> 内嵌样式 定义<style>标签,在标签内部定义css样式 <style> h1 {...} </style> 外联样式 定义<link>标签,通过href属性引入外部css文件 <link rel="stylesheet" href="css/news.css"> CSS选择器 1.元素(标签)选择器: 选择器的名字必须是标签的名字 作用:选择器中的样式会作用于所有同名的标签上 元素名称 { css样式名:css样式值; } 例子如下: div{ color: red; } 2.id选择器: 选择器的名字前面需要加上# 作用:选择器中的样式会作用于指定id的标签上,而且有且只有一个标签(由于id是唯一的) #id属性值 { css样式名:css样式值; } 例子如下: #did { color: blue; } 3.类选择器: 选择器的名字前面需要加上 . 作用:选择器中的样式会作用于所有class的属性值和该名字一样的标签上,可以是多个 .class属性值 { css样式名:css样式值; } 例子如下: .cls{ color: green; } 这里使用了第二种CSS引入方式,内嵌样式,<style>包裹,里面用了三种CSS选择器 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>焦点访谈:中国底气 新思想夯实大国粮仓</title> <style> h1 { color: #4D4F53; } /* 元素选择器 */ /* span { color: red; } */ /* 类选择器 */ /* .cls { color: green; } */ /* ID选择器 */ #time { color: #968D92; font-size: 13px; /* 设置字体大小 */ } </style> </head> <body> <img src="img/news_logo.png"> 新浪政务 > 正文 <h1>焦点访谈:中国底气 新思想夯实大国粮仓</h1> <hr> <span class="cls" id="time">2023年03月02日 21:50</span> <span class="cls">央视网</span> <hr> </body> </html> 页面布局 盒子模型,盒子部分指的是border及以内的部分,不包括margin 布局标签:实际开发网页中,会大量频繁的使用 div 和 span 这两个没有语义的布局标签。 标签: 特点: div标签: 一行只显示一个(独占一行) 宽度默认是父元素的宽度,高度默认由内容撑开 可以设置宽高(width、height) span标签: 一行可以显示多个 宽度和高度默认由内容撑开 不可以设置宽高(width、height) box-sizing: border-box,此时指定width height为盒子的高宽,而不是content的高宽 表格标签 table> : 用于定义整个表格, 可以包裹多个 <tr>, 常用属性如下: border:规定表格边框的宽度 width:规定表格的宽度 cellspacing: 规定单元之间的空间 <tr> : 表格的行,可以包裹多个 <td> <td> : 表格单元格(普通),可以包裹内容 , 如果是表头单元格,可以替换为 <th> ,th具有加粗居中展示的效果 表单标签 表单场景: 表单就是在网页中负责数据采集功能的,如:注册、登录的表单。 表单标签: <form> 表单属性: action: 规定表单提交时,向何处发送表单数据,表单提交的URL。 method: 规定用于发送表单数据的方式,常见为: GET、POST。 GET:表单数据是拼接在url后面的, 如: xxxxxxxxxxx?username=Tom&age=12,url中能携带的表单数据大小是有限制的。 POST: 表单数据是在请求体(消息体)中携带的,大小没有限制。 表单项标签: 不同类型的input元素、下拉列表、文本域等。 input: 定义表单项,通过type属性控制输入形式 type取值 描述 text 默认值,定义单行的输入字段 password 定义密码字段 radio 定义单选按钮 checkbox 定义复选框 file 定义文件上传按钮 date/time/datetime-local 定义日期/时间/日期时间 number 定义数字输入框 email 定义邮件输入框 hidden 定义隐藏域 submit / reset / button 定义提交按钮 / 重置按钮 / 可点击按钮 select: 定义下拉列表 textarea: 定义文本域 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>HTML-表单项标签</title> </head> <body> <!-- value: 表单项提交的值 --> <form action="" method="post"> 姓名: <input type="text" name="name"> <br><br> 密码: <input type="password" name="password"> <br><br> 性别: <input type="radio" name="gender" value="1"> 男 <label><input type="radio" name="gender" value="2"> 女 </label> <br><br> 爱好: <label><input type="checkbox" name="hobby" value="java"> java </label> <label><input type="checkbox" name="hobby" value="game"> game </label> <label><input type="checkbox" name="hobby" value="sing"> sing </label> <br><br> 图像: <input type="file" name="image"> <br><br> 生日: <input type="date" name="birthday"> <br><br> 时间: <input type="time" name="time"> <br><br> 日期时间: <input type="datetime-local" name="datetime"> <br><br> 邮箱: <input type="email" name="email"> <br><br> 年龄: <input type="number" name="age"> <br><br> 学历: <select name="degree"> <option value="">----------- 请选择 -----------</option> <option value="1">大专</option> <option value="2">本科</option> <option value="3">硕士</option> <option value="4">博士</option> </select> <br><br> 描述: <textarea name="description" cols="30" rows="10"></textarea> <br><br> <input type="hidden" name="id" value="1"> <!-- 表单常见按钮 --> <input type="button" value="按钮"> <input type="reset" value="重置"> <input type="submit" value="提交"> <br> </form> </body> </html> name="gender":这个属性定义了单选按钮组的名称,它们被分为同一个组,因此只能选择其中的一个按钮。在这种情况下,所有具有相同 name 属性值的单选按钮都被视为同一组。 value="1" 和 value="2":这些是单选按钮的值。当用户选择某个单选按钮时,该单选按钮的值将被提交到服务器。在这种情况下,value="1" 表示选择男性,而 value="2" 表示选择女性。 用户在浏览器中看到的文本内容是 "男" 和 "女"。 女 这里的label意味着用户不仅可以点击单选按钮本身来选择选项,当用户单击 "女" 这个标签文本时,与之关联的单选按钮也会被选中。 JavaScript JS引入方式 **第一种方式:**内部脚本,将JS代码定义在HTML页面中 JavaScript代码必须位于<script></script>标签之间 在HTML文档中,可以在任意地方,放置任意数量的<script> 一般会把脚本置于<body>元素的底部,可改善显示速度 例子: <script> alert("Hello JavaScript") </script> **第二种方式:**外部脚本将, JS代码定义在外部 JS文件中,然后引入到 HTML页面中 外部JS文件中,只包含JS代码,不包含<script>标签 引入外部js的<script>标签,必须是双标签 例子: <script src="js/demo.js"></script> 注意:demo.js中只有js代码,没有<script>标签 JS基础语法 书写语法 区分大小写:与 Java 一样,变量名、函数名以及其他一切东西都是区分大小写的 每行结尾的分号可有可无 大括号表示代码块 注释: 单行注释:// 注释内容 多行注释:/* 注释内容 */ 输出的三种形式: api 描述 window.alert() 警告框 document.write() 在HTML 输出内容 console.log() 写入浏览器控制台 变量 关键字 解释 var 早期ECMAScript5中用于变量声明的关键字 let ECMAScript6中新增的用于变量声明的关键字,相比较var,let只在代码块内生效(大括号) const 声明常量的,常量一旦声明,不能修改 var:作用域比较大,全局的;可以重复定义,后面的覆盖前面的 let:局部变量(代码块内生效{}),不可重复定义。 const: const pi=3.14 数据类型和运算符 数据类型 描述 number 数字(整数、小数、NaN(Not a Number)) string 字符串,单双引皆可 boolean 布尔。true,false null 对象为空 undefined 当声明的变量未初始化时,该变量的默认值是 undefined 运算规则 运算符 算术运算符 + , - , * , / , % , ++ , -- 赋值运算符 = , += , -= , *= , /= , %= 比较运算符 > , < , >= , <= , != , == , === 注意 == 会进行类型转换,=== 不会进行类型转换 逻辑运算符 && , || , ! 三元运算符 条件表达式 ? true_value: false_value parseint() ,将其他类型转化为数字 函数 第一种: function 函数名(参数1,参数2..){ 要执行的代码 } 因为JavaScript是弱数据类型的语言,所以有如下几点需要注意: 形式参数不需要声明类型,并且JavaScript中不管什么类型都是let或者var去声明,加上也没有意义。 返回值也不需要声明类型,直接return即可 如下示例: function add(a, b){ return a + b; } var result=add(10,20)可以接收返回值 第二种可以通过var去定义函数的名字,具体格式如下: var functionName = function (参数1,参数2..){ //要执行的代码 } 如下示例: var add = function(a,b){ return a + b; } var result = add(10,20); 函数的调用不变 JS对象 Array对象 方式1: var 变量名 = new Array(元素列表); 例如: var arr = new Array(1,2,3,4); //1,2,3,4 是存储在数组中的数据(元素) 方式2: var 变量名 = [ 元素列表 ]; 例如: var arr = [1,2,3,4]; //1,2,3,4 是存储在数组中的数据(元素) 长度可变=》可以直接arr[10]=100,不会报错 类型可变=》arr[1]="hello",可以既存数字又存字符串 属性: 属性 描述 length 设置或返回数组中元素的数量。 方法: 方法方法 描述 forEach() 遍历数组中的每个有值的元素,并调用一次传入的函数 push() 将新元素添加到数组的末尾,并返回新的长度 splice() 从数组中删除元素 普通for循环:会遍历每个数组元素,无论是否有值 var arr = [1,2,3,4]; arr[10] = 50; for (let i = 0; i < arr.length; i++) { console.log(arr[i]); } foreach: arr.forEach(function(e){ console.log(e); }) 在ES6中,引入箭头函数的写法,语法类似java中lambda表达式,修改上述代码如下: arr.forEach((e) => { console.log(e); }) push: arr.push(7,8,9) 可以一次添加多个元素 splice: arr.splice(start,cnt),从start开始,删cnt个元素 String字符串 String对象也提供了一些常用的属性和方法,如下表格所示: 属性: 属性 描述 length 字符串的长度。 方法: 方法 描述 charAt() 返回在指定位置的字符。 indexOf() 检索字符串。 trim() 去除字符串两边的空格 substring() 提取字符串中两个指定的索引号之间的字符。 length属性: length属性可以用于返回字符串的长度,添加如下代码: //length console.log(str.length); charAt()函数: charAt()函数用于返回在指定索引位置的字符,函数的参数就是索引。添加如下代码: console.log(str.charAt(4)); indexOf()函数 indexOf()函数用于检索指定内容在字符串中的索引位置的,返回值是索引,参数是指定的内容。添加如下代码: console.log(str.indexOf("lo")); trim()函数 trim()函数用于去除字符串两边的空格的。添加如下代码: var s = str.trim(); console.log(s.length); substring()函数 substring()函数用于截取字符串的,函数有2个参数。 参数1:表示从那个索引位置开始截取。包含 参数2:表示到那个索引位置结束。不包含 console.log(s.substring(0,5)); JSON对象 自定义对象 var 对象名 = { 属性名1: 属性值1, 属性名2: 属性值2, 属性名3: 属性值3, 函数名称: function(形参列表){} }; 我们可以通过如下语法调用属性: 对象名.属性名 通过如下语法调用函数: 对象名.函数名() json对象 JSON对象:JavaScript Object Notation,JavaScript对象标记法。是通过JavaScript标记法书写的文本。其格式如下: { "key":value, "key":value, "key":value } 其中,key必须使用引号并且是双引号标记,value可以是任意数据类型。 JSON字符串示例: var jsonstr = '{"name":"Tom", "age":18, "addr":["北京","上海","西安"]}'; alert(jsonstr.name); 注意外层的单引号不要忘记! JSON字符串=》JS对象 var obj = JSON.parse(jsonstr); 对象.属性 就可以获得key对应的值 JS对象=》JS字符串 var jsonstr=JSON.stringify(obj) JSON格式数据 {"name":"666"} 是一个 JSON 对象,[{"name":"666"},{"name":"li"}] 是一个 JSON 数组,它们都是 JSON 格式的数据。 BOM对象 重点学习的是Window对象、Location(地址栏)对象 window对象 常用方法:通过可简写,window.alert()->alert() 函数 描述 alert() 显示带有一段消息和一个确认按钮的警告框。 comfirm() 显示带有一段消息以及确认按钮和取消按钮的对话框。 setInterval() 按照指定的周期(以毫秒计)来调用函数或计算表达式。 setTimeout() 在指定的毫秒数后调用函数或计算表达式。 setInterval(fn,毫秒值):定时器,用于周期性的执行某个功能,并且是循环执行。该函数需要传递2个参数: fn:函数,需要周期性执行的功能代码 毫秒值:间隔时间 //定时器 - setInterval -- 周期性的执行某一个函数 var i = 0; setInterval(function(){ i++; console.log("定时器执行了"+i+"次"); },2000); setTimeout(fn,毫秒值) :定时器,只会在一段时间后执行一次功能。参数和上述setInterval一致 注释掉之前的代码,添加代码如下: //定时器 - setTimeout -- 延迟指定时间执行一次 setTimeout(function(){ alert("JS"); },3000); 浏览器打开,3s后弹框,关闭弹框,发现再也不会弹框了。 Location对象 location是指代浏览器的地址栏对象,对于这个对象,我们常用的是href属性,用于获取或者设置浏览器的地址信息,添加如下代码: //获取浏览器地址栏信息 alert(location.href); //设置浏览器地址栏信息 location.href = "https://www.itcast.cn"; 设置后会自动跳转到该地址。 DOM对象 DOM介绍 DOM:Document Object Model 文档对象模型。也就是 JavaScript 将 HTML 文档的各个组成部分封装为对象。 封装的对象分为 Document:整个文档对象 Element:元素对象 Attribute:属性对象 Text:文本对象 Comment:注释对象 那么我们学习DOM技术有什么用呢?主要作用如下: 改变 HTML 元素的内容 改变 HTML 元素的样式(CSS) 对 HTML DOM 事件作出反应 添加和删除 HTML 元素 从而达到动态改变页面效果目的。 DOM获取 函数 描述 document.getElementById() 根据id属性值获取,返回单个Element对象 document.getElementsByTagName() 根据标签名称获取,返回Element对象数组 document.getElementsByName() 根据name属性值获取,返回Element对象数组 document.getElementsByClassName() 根据class属性值获取,返回Element对象数组 示例代码: <body> <img id="h1" src="img/off.gif"> <br><br> <div class="cls">传智教育</div> <br> <div class="cls">黑马程序员</div> <br> <input type="checkbox" name="hobby"> 电影 <input type="checkbox" name="hobby"> 旅游 <input type="checkbox" name="hobby"> 游戏 </body> document.getElementById(): 根据标签的id属性获取标签对象,id是唯一的,所以获取到是单个标签对象。 <script> //1. 获取Element元素 //1.1 获取元素-根据ID获取 var img = document.getElementById('h1'); alert(img); </script> document.getElementsByTagName() : 根据标签的名字获取标签对象,同名的标签有很多,所以返回值是数组。重点! var divs = document.getElementsByTagName('div'); for (let i = 0; i < divs.length; i++) { alert(divs[i]); } DOM修改 同上面的例子: 你想要如何操作获取到的DOM元素,你需要查阅手册,看它支持的属性 var divs = document.getElementsByClassName('cls'); var div1 = divs[0]; div1.innerHTML = "传智教育666"; JS事件 JavaScript对于事件的绑定提供了2种方式: 方式1:通过html标签中的事件属性进行绑定 <input type="button" id="btn1" value="事件绑定1" onclick="on()"> <script> function on(){ alert("按钮1被点击了..."); } </script> 方式2:通过DOM中Element元素的事件属性进行绑定 <input type="button" id="btn2" value="事件绑定2"> <script> document.getElementById('btn2').onclick = function(){ alert("按钮2被点击了..."); } </script> 常见事件: 事件属性名 说明 onclick 鼠标单击事件 onblur 元素失去焦点 onfocus 元素获得焦点 onload 某个页面或图像被完成加载 onsubmit 当表单提交时触发该事件 onmouseover 鼠标被移到某元素之上 onmouseout 鼠标从某元素移开 VUE VUE简介 我们引入了一种叫做MVVM(Model-View-ViewModel)的前端开发思想,即让我们开发者更加关注数据,而非数据绑定到视图这种机械化的操作。那么具体什么是MVVM思想呢? MVVM:其实是Model-View-ViewModel的缩写,有3个单词,具体释义如下: Model: 数据模型,特指前端中通过请求从后台获取的数据 View: 视图,用于展示数据的页面,可以理解成我们的html+css搭建的页面,但是没有数据 ViewModel: 数据绑定到视图,负责将数据(Model)通过JavaScript的DOM技术,将数据展示到视图(View)上 基于上述的MVVM思想,其中的Model我们可以通过Ajax来发起请求从后台获取;对于View部分,我们将来会学习一款ElementUI框架来替代HTML+CSS来更加方便的搭建View;而今天我们要学习的就是侧重于ViewModel部分开发的vue前端框架,用来替代JavaScript的DOM操作,让数据展示到视图的代码开发变得更加的简单。 VUE快速上手 第一步:在VS Code中创建名为12. Vue-快速入门.html的文件,并且在html文件同级创建js目录,将资料/vue.js文件目录下得vue.js拷贝到js目录 第二步:然后编写<script>标签来引入vue.js文件,代码如下: <script src="js/vue.js"></script> 第三步:在js代码区域定义vue对象,代码如下: <script> //定义Vue对象 new Vue({ el: "#app", //vue接管区域 data:{ message: "Hello Vue" } }) </script> 在创建vue对象时,有几个常用的属性: el: 用来指定哪儿些标签受 Vue 管理。 该属性取值 #app 中的 app 需要是受管理的标签的id属性值 data: 用来定义数据模型 methods: 用来定义函数。这个我们在后面就会用到 第四步:在html区域编写视图,其中{{}}是插值表达式,用来将vue对象中定义的model展示到页面上的 <body> <div id="app"> <input type="text" v-model="message"> {{message}} </div> </body> Vue指令 **指令:**HTML 标签上带有 v- 前缀的特殊属性,不同指令具有不同含义。 指令 作用 v-bind 为HTML标签绑定属性值,如设置 href , css样式等 v-model 在表单元素上创建双向数据绑定 v-on 为HTML标签绑定事件 v-if 条件性的渲染某元素,判定为true时渲染,否则不渲染 v-else v-else-if v-show 根据条件展示某元素,区别在于切换的是display属性的值 v-for 列表渲染,遍历容器的元素或者对象的属性 V-bind和v-model v-bind: 为HTML标签绑定属性值,如设置 href , css样式等。当vue对象中的数据模型发生变化时,标签的属性值会随之发生变化。单向绑定! v-model: 在表单元素上创建双向数据绑定。什么是双向? vue对象的data属性中的数据变化,视图展示会一起变化 视图数据发生变化,vue对象的data属性中的数据也会随着变化。 data属性中数据变化,我们知道可以通过赋值来改变,但是视图数据为什么会发生变化呢?只有表单项标签!所以双向绑定一定是使用在表单项标签上的。 <body> <div id="app"> <a v-bind:href="url">链接1</a> <a :href="url">链接2</a> <input type="text" v-model="url"> </div> </body> <script> //定义Vue对象 new Vue({ el: "#app", //vue接管区域 data:{ url: "https://www.baidu.com" } }) </script> v-on v-on: 用来给html标签绑定事件的 <input type="button" value="点我一下" v-on:click="handle()"> 简写: <input type="button" value="点我一下" @click="handle()"> script: <script> //定义Vue对象 new Vue({ el: "#app", //vue接管区域 data:{ }, methods: { handle: function(){ alert("你点我了一下..."); } } }) </script> v-if和v-show 年龄<input type="text" v-model="age">经判定,为: <span v-if="age <= 35">年轻人(35及以下)</span> <span v-else-if="age > 35 && age < 60">中年人(35-60)</span> <span v-else>老年人(60及以上)</span> 年龄<input type="text" v-model="age">经判定,为: <span v-show="age <= 35">年轻人(35及以下)</span> <span v-show="age > 35 && age < 60">中年人(35-60)</span> <span v-show="age >= 60">老年人(60及以上)</span> v-show和v-if的作用效果是一样的,只是原理不一样。v-if指令,不满足条件的标签代码直接没了,而v-show指令中,不满足条件的代码依然存在,只是添加了css样式来控制标签不去显示。 vue-for v-for: 从名字我们就能看出,这个指令是用来遍历的。其语法格式如下: <div id="app"> <div v-for="addr in addrs">{{addr}}</div> <hr> <div v-for="(addr,index) in addrs">{{index}} : {{addr}}</div> </div> <script> //定义Vue对象 new Vue({ el: "#app", //vue接管区域 data:{ addrs:["北京", "上海", "西安", "成都", "深圳"] }, methods: { } }) </script> index从0开始 Vue生命周期 状态 阶段周期 beforeCreate 创建前 created 创建后 beforeMount 挂载前 mounted 挂载完成 beforeUpdate 更新前 updated 更新后 beforeDestroy 销毁前 destroyed 销毁后 其中我们需要重点关注的是**mounted,**其他的我们了解即可。 与methods平级 mounted:挂载完成,Vue初始化成功,HTML页面渲染成功。以后我们一般用于页面初始化自动的ajax请求后台数据 Ajax-Axios Ajax: 全称Asynchronous JavaScript And XML,异步的JavaScript和XML。其作用有如下2点: 与服务器进行数据交换:通过Ajax可以给服务器发送请求,并获取服务器响应的数据。 异步交互:可以在不重新加载整个页面的情况下,与服务器交换数据并更新部分网页的技术,如:搜索联想、用户名是否可用的校验等等。 现在Ajax已经淘汰,用Axios了,是对Ajax的封装 Axios快速上手 Axios的使用比较简单,主要分为2步: 引入Axios文件 <script src="js/axios-0.18.0.js"></script> 使用Axios发送请求,并获取响应结果,官方提供的api很多,此处给出2种,如下 发送 get 请求 axios({ method:"get", url:"http://localhost:8080/ajax-demo1/aJAXDemo1?username=zhangsan" }).then(function (resp){ alert(resp.data); }) 发送 post 请求 axios({ method:"post", url:"http://localhost:8080/ajax-demo1/aJAXDemo1", data:"username=zhangsan" }).then(function (resp){ alert(resp.data); }); 推荐以下方法! 方法 描述 axios.get(url [, config]) 发送get请求 axios.delete(url [, config]) 发送delete请求 axios.post(url [, data[, config]]) 发送post请求 axios.put(url [, data[, config]]) 发送put请求 axios.get("http://yapi.smart-xwork.cn/mock/169327/emp/list").then(result => { console.log(result.data); }) axios.post("http://yapi.smart-xwork.cn/mock/169327/emp/deleteById","id=1").then(result => { console.log(result.data); }) axios使用步骤: 步骤: 首先创建文件,提前准备基础代码,包括表格以及vue.js和axios.js文件的引入 我们需要在vue的mounted钩子函数中发送ajax请求,获取数据 拿到数据,数据需要绑定给vue的data属性 在<tr>标签上通过v-for指令遍历数据,展示数据,这里同Vue中的步骤。 <script> new Vue({ el: "#app", data: { emps:[] }, mounted () { //发送异步请求,加载数据 axios.get("http://yapi.smart-xwork.cn/mock/169327/emp/list").then(result => { console.log(result.data); this.emps = result.data.data; }) } }); </script> Vue中先定义emps空数组,再axios将数据取到里面 this.emps=xxxx Nginx docker-compose.yml version: '3.8' networks: group-buy-network: external: true services: # 前端(保持不变) group-buy-market-front: image: nginx:alpine ports: ['86:80'] volumes: - ./nginx/html:/usr/share/nginx/html - ./nginx/conf/nginx.conf:/etc/nginx/nginx.conf:ro networks: - group-buy-network # 后端实例1 group-buying-sys-1: image: smile/group-buying-sys:latest ports: ['8091:8091'] networks: - group-buy-network # 后端实例2 group-buying-sys-2: image: smile/group-buying-sys:latest # 使用相同镜像 ports: ['8092:8091'] # 宿主机端口不同,容器内部端口相同 networks: - group-buy-network nginx.conf # 全局配置块 user nginx; # 以nginx用户身份运行worker进程(安全性考虑) worker_processes auto; # 自动根据CPU核心数设置工作进程数量(优化性能) error_log /var/log/nginx/error.log warn; # 错误日志路径,只记录警告及以上级别的错误 pid /var/run/nginx.pid; # 存储Nginx主进程ID的文件位置(用于进程管理) # 事件处理模块配置 events { worker_connections 1024; # 每个worker进程能处理的最大并发连接数 # 总并发量 = worker_processes * worker_connections } # HTTP服务器配置 http { # 基础文件类型支持 include /etc/nginx/mime.types; # 包含MIME类型定义文件(扩展名与Content-Type映射) default_type application/octet-stream; # 默认MIME类型(当无法识别文件类型时使用) # 性能优化参数 sendfile on; # 启用高效文件传输模式(零拷贝技术) keepalive_timeout 65; # 客户端保持连接的超时时间(秒),减少TCP握手开销 # 上游服务器组定义(负载均衡) upstream gbm_backend { # 负载均衡配置(两个后端实例) server group-buying-sys-1:8091 weight=3; # 后端实例1,权重为3(获得60%流量) server group-buying-sys-2:8091 weight=2; # 后端实例2,权重为2(获得40%流量) keepalive 32; # 保持到后端服务器的长连接数(提升性能) } # 支付服务上游(单实例) upstream pay_backend { server pay-mall:8092; # 支付服务地址(Docker服务名) } # 虚拟主机配置(一个server代表一个网站) server { # 监听配置 listen 80 default_server; # 监听IPv4的80端口,作为默认服务器 listen [::]:80 default_server; # 监听IPv6的80端口 server_name groupbuy.bitday.top; # 绑定的域名(支持多个,用空格分隔) # 静态文件服务配置 root /usr/share/nginx/html; # 前端静态文件根目录(Docker需挂载此目录) index index.html; # 默认访问的索引文件 # 前端路由处理(适配Vue/React等单页应用) location / { try_files $uri $uri/ /index.html; # 尝试顺序:匹配文件→匹配目录→回退到index.html } # 组购系统API代理配置 location /api/v1/gbm/ { proxy_pass http://gbm_backend; # 请求转发到负载均衡组 proxy_set_header Host $host; # 传递原始域名 proxy_set_header X-Real-IP $remote_addr; # 传递客户端真实IP proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # 代理链IP信息 } # 支付/登录API代理(正则匹配) location ~ ^/api/v1/(alipay|login)/ { proxy_pass http://pay_backend; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } } } 注意事项: 1.由于是docker部署,要区别宿主机端口与容器内端口!比如 server group-buying-sys-1:8091 weight=3; 这里的8091就是 group-buying-sys-1 这个容器内端口。类似地,nginx也是docker部署,通过宿主机端口86映射到group-buy-market-front这个容器内的80端口。nginx.conf配置中的端口一定是和容器内挂钩的,而不是和宿主机!!! 2.如果不是docker部署,比如在本地windows运行nginx.exe ,那么 就没有 ports: ['8091:8091'] 这种映射关系,你的java项目中写的端口是多少,那么nginx.conf就配置多少 3.docker部署,如果 nginx 与 group-buying-sys 位于同一网络中,可以服务名:容器内端口 等价于 宿主机ip:端口。比如 group-buying-sys-2 中 ports: ['8092:8091'] ,假设服务器ip地址为124.71.159.xxx ,那么 upstream gbm_backend { server group-buying-sys-2:8091 weight=2; #推荐! nginx->后端容器 } upstream gbm_backend { server 124.71.159.xxx:8092 weight=2; #非常不推荐!相关nginx->宿主机->后端容器 } 两种效果一样,但推荐第一种!!! 4.Nginx 反向代理的核心配置 location /api/v1/gbm/ { proxy_pass http://gbm_backend; # 请求转发到负载均衡组 proxy_set_header Host $host; # 传递原始域名 proxy_set_header X-Real-IP $remote_addr; # 传递客户端真实IP proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # 代理链IP信息 } 将所有匹配 /api/v1/gbm/ 路径的请求转发到 gbm_backend 这个后端容器(可负载均衡为多台)。 请求 http://nginx-server/api/v1/gbm/order → 转发到 http://gbm_backend/api/v1/gbm/order **注意,**如果 location /api/v1/gbm/ { proxy_pass http://gbm_backend/; # 注意末尾的斜杠 } 请求 http://nginx-server/api/v1/gbm/order → 转发到 http://gbm_backend/order(/api/v1/gbm/ 被移除) 5.如果配置了反向代理(第4点内容),那么前端调用后端接口时,HTML、JS中无需写具体的后端服务器所在地址(ip:端口),而且如果做了负载均衡,有多台,这里也不好指定哪一台。 为什么? 比如前端发送请求fetch(/api/v1/gbm/index/query_group_buy_market_config) 发现匹配 /api/v1/gbm ,nginx根据配置就发给http://gbm_backend负载均衡组,组里有group-buying-sys-1和 group-buying-sys-2,根据负载均衡规则转发到其中一台后端服务器。 6.静态文件挂载 server { ... # 静态文件服务配置 root /usr/share/nginx/html; # 前端静态文件根目录(Docker需挂载此目录) index index.html; ... } root 要指定静态文件根目录,如果是docker部署,一定要写容器内的位置;index.html 这个文件是/usr/share/nginx/html目录下的。 坑点: 如果要上传文件,可能会报413错误:单次请求的 整体 body 大小超限(默认1MB) 若后端需要解析比较耗时,还可能报503错误:超时间限制(默认60秒) 业务涉及 大文件上传 或 长时间任务,务必调整这些配置!如: location /api/ { proxy_pass http://ai-rag-knowledge-app:8095; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; client_max_body_size 50M; # 允许上传 50MB # 添加代理超时设置(单位:秒) proxy_connect_timeout 3600s; proxy_send_timeout 3600s; proxy_read_timeout 3600s; }
杂项
zy123
3月21日
0
4
0
2025-03-21
anaconda基础命令
Anaconda基础命令 cuda版本 12.3.1 驱动版本 546.26 打开anaconda prompt(普通命令行cmd也可以): 查看版本和环境 conda -V 查看版本 conda env list 查看已安装的环境 *代表当前环境 环境管理 conda create -n 新环境名字 python=3.7 (若只有python则下载最新版python) conda activate 新环境名字 可以切换环境 conda deactivate 退出环境到base conda remove -n 新环境名字 --all 删除创建的环境(先deactivate退出) 包管理 注:包管理操作前请先激活目标环境。 conda list 列出当前环境所安装的包 conda search numpy 可以查看numpy有哪些版本 conda install numpy 可以指定版本,默认最新版 pip install -r requirements.txt (使用 pip 安装依赖包列表) conda remove numpy 删除numpy以及所有依赖关系的包 查看激活的环境的python版本 python --version 结合 PyCharm 使用 conda 环境 在 conda 中创建好虚拟环境 如上文所示,使用 conda create -n 新环境名字 python=版本 创建。 在 PyCharm 中使用已有的 conda 环境 打开 PyCharm,进入 File > Settings > Project: YourProject > Python Interpreter 点击右侧的 Show All,可以看到 PyCharm 已经检测到的所有解释器。 若没有显示目标 conda 环境,可以点击右侧的加号(+)添加现有 conda 环境作为解释器。 这是添加conda镜像 conda config --add channels http://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/free/ conda config --add channels http://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/main/ conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/free conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/mysys2/ conda config --set show_channel_urls yes 这是添加Pypi镜像,适用于pip安装 清华:https://pypi.tuna.tsinghua.edu.cn/simple 阿里云:https://mirrors.aliyun.com/pypi/simple/ 中国科技大学: https://pypi.mirrors.ustc.edu.cn/simple/ 华中理工大学:https://pypi.hustunique.com/ 山东理工大学:https://pypi.sdutlinux.org/ 豆瓣:https://pypi.douban.com/simple/ pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple 在conda中导出pip requirements.txt: pip freeze > requirements.txt Conda环境与Pycharm环境的大坑 如果你的pycharm中使用conda环境,那么你在pycharm的终端中所用的可能不是conda环境! 解决办法: shell path改为 cmd.exe 这样虚拟环境就默认设置为conda环境了 如果命令行cd到项目根目录,所用的也并不是conda环境!这里用的是conda的默认环境? 正确方法: 1.使用anaconda prompt打开 2.conda activate env 激活环境 3.cd到项目根目录 4.输入命令
杂项
zy123
3月21日
0
2
0
2025-03-21
招标文件解析
产品官网:智标领航 - 招投标AI解决方案 产品后台:xxx 项目地址:xxx git clone地址:xxx 选择develop分支,develop-xx 后面的xx越近越新。 正式环境:xxx 测试环境:xxx 大解析:指从招标文件解析入口进去,upload.py 小解析:从投标文件生成入口进去,little_zbparse 和get_deviation,两个接口后端一起调 项目启动与维护: .env存放一些密钥(大模型、textin等),它是gitignore忽略了,因此在服务器上git pull项目的时候,这个文件不会更新(因为密钥比较重要),需要手动维护服务器相应位置的.env。 如何更新服务器上的版本: 步骤 进入项目文件夹 **注意:**需要确认.env是否存在在服务器,默认是隐藏的 输入cat .env 如果不存在,在项目文件夹下sudo vim .env 将密钥粘贴进去!!! git pull sudo docker-compose up --build -d 更新并重启 或者 sudo docker-compose build 先构建镜像 sudo docker-compose up -d 等空间时再重启 sudo docker-compose logs flask_app --since 1h 查看最近1h的日志(如果重启后报错也能查看,推荐重启后都运行一下这个) requirements.txt一般无需变动,除非代码中使用了新的库,也要手动在该文件中添加包名及对应的版本 docker基础知识 docker-compose: 本项目为单服务项目,只有flask_app(服务名) build context(context: .): 这是在构建镜像时提供给 Docker 的文件集,指明哪些文件可以被 Dockerfile 中的 COPY 或 ADD 指令使用。它是构建过程中的“资源包”。 对于多服务,build下就要针对不同的服务,指定所需的“资源包”和对应的Dockerfile dockerfile: COPY . .(在 Dockerfile 中): 这条指令会将构建上下文中的所有内容复制到镜像中的当前工作目录(这里是 /flask_project)。 docker exec -it zbparse-flask_app-1 sh 这个命令会直接进入到flask_project目录内部ls之后可以看到: Dockerfile README.md docker-compose.yml flask_app md_files requirements.txt 如果这个基础上再cd /会切换到这个容器的根目录,可以看到flask_project文件夹以及其他基础系统环境。如: bin boot dev etc flask_project home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var 数据卷挂载: volumes: -/home/Z/zbparse_output_dev:/flask_project/flask_app/static/output # 额外的数据卷挂载 本地路径:容器内路径 都从根目录找起。 完整的容器名 <项目名>-<服务名>-<序号> 项目名:默认是当前目录的名称(这里是 zbparse),或者你在启动 Docker Compose 时通过 -p 参数指定的项目名称。 服务名:在 docker-compose.yml 文件中定义的服务名称(这里是 flask_app)。 序号:如果同一个服务启动了多个容器,会有数字序号来区分(这里是 1)。 docker-compose exec flask_app sh docker exec -it zbparse-flask_app-1 sh 这两个是等价的,因为docker-compose 会自动找到对应的完整容器名并执行命令。 删除所有悬空镜像(无容器引用的 <none> 镜像) docker image prune 如何本地启动本项目: Pycharm启动 requirements.txt里的环境要配好 conda create -n zbparse python=3.8 conda activate zbparse pip install -r requirements.txt .env环境配好 (一般不需要在电脑环境变量中额外配置了,但是要在Pycharm中安装插件,使得项目在启动时能将env中的环境变量自动配置到系统环境变量中!!!) 点击下拉框,Edit configurations 设置run_serve.py为启动脚本 注意这里的working directory要设置到最外层文件夹,而不是flask_app!!! 命令行启动 1.编写ps1脚本 # 切换到指定目录 cd D:\PycharmProjects\zbparse # 激活 Conda 环境 conda activate zbparse # 检查是否存在 .env 文件 if (Test-Path .env) { # 读取 .env 文件并设置环境变量 Get-Content .env | ForEach-Object { if ($_ -match '^\s*([^=]+)=(.*)') { $name = $matches[1].Trim() $value = $matches[2].Trim() [System.Environment]::SetEnvironmentVariable($name, $value) } } } else { Write-Host ".env not find" } # 设置 PYTHONPATH 环境变量 $env:PYTHONPATH = "D:\flask_project" # 运行 Python 脚本 python flask_app\run_serve.py $env:PYTHONPATH = "D:\flask_project",告诉 Python 去 D:\flask_project 查找模块,这样就能让 Python 找到你的 flask_app 包。 2.确保conda已添加到系统环境变量 打开 Anaconda Prompt,然后输入 where conda 来查看 conda 的路径。 打开系统环境变量Path,添加一条:C:\ProgramData\anaconda3\condabin 或者 CMD 中 set PATH=%PATH%;新添加的路径 重启终端可以刷新环境变量 3.如果你尚未在 PowerShell 中初始化 conda,可以在 Anaconda Prompt 中运行: conda init powershell 4.进入到存放run.ps1文件的目录,在搜索栏中输入powershell 5.默认情况下,PowerShell 可能会阻止运行脚本。你可以调整执行策略: Set-ExecutionPolicy RemoteSigned -Scope CurrentUser 6.运行脚本 .\run.ps1 注意!!! Windows 控制台存在QuickEdit 模式,在 QuickEdit 模式下,当你在终端窗口中点击(尤其是拖动或选中内容)时,控制台会进入文本选择状态,从而暂停正在运行的程序!! 禁用 QuickEdit 模式 在 PowerShell 窗口标题栏上点击右键,选择“属性”。 在“选项”选项卡中,取消勾选“快速编辑模式”。 点击“确定”,重启 PowerShell 窗口后再试。 模拟用户请求 postman打post请求测试: http://127.0.0.1:5000/upload body: { "file_url":"xxxx", "zb_type":2 } file_url如何获取:OSS管理控制台 bid-assistance/test 里面找个文件的url,推荐'094定稿-湖北工业大学xxx' 注意这里的url地址有时效性,要经常重新获取新的url 清理服务器上的文件夹 1.编写shell文件,sudo vim clean_dir.sh 清理/home/Z/zbparse_output_dev下的output1这些二级目录下的c8d2140d-9e9a-4a49-9a30-b53ba565db56这种uuid的三级目录(只保留最近7天)。 #!/bin/bash # 需要清理的 output 目录路径 ROOT_DIR="/home/Z/zbparse_output_dev" # 检查目标目录是否存在 if [ ! -d "$ROOT_DIR" ]; then echo "目录 $ROOT_DIR 不存在!" exit 1 fi echo "开始清理 $ROOT_DIR 下超过 7 天的目录..." echo "以下目录将被删除:" # -mindepth 2 表示从第二层目录开始查找,防止删除 output 下的直接子目录(如 output1、output2) # -depth 采用深度优先遍历,确保先处理子目录再处理父目录 find "$ROOT_DIR" -mindepth 2 -depth -type d -mtime +7 -print -exec rm -rf {} \; echo "清理完成。" 2.添加权限。 sudo chmod +x ./clean_dir.sh 3.执行 sudo ./clean_dir.sh 以 root 用户的身份编辑 crontab 文件,从而设置或修改系统定时任务(cron jobs)。每天零点10分清理 sudo crontab -e 在里面添加: 10 0 * * * /home/Z/clean_dir.sh 目前测试服务器和正式服务器都写上了!无需变动 内存泄漏问题 问题定位 查看容器运行时占用的文件FD套接字FD等(排查内存泄漏,长期运行这三个值不会很大) [Z@iZbp13rxxvm0y7yz7l02hbZ zbparse]$ docker exec -it zbparse-flask_app-1 sh ls -l /proc/1/fd | awk ' BEGIN { file=0; socket=0; pipe=0; other=0 } { if(/socket:/) socket++ else if(/pipe:/) pipe++ else if(/\/|tmp/) file++ # 识别文件路径特征 else other++ } END { print "文件FD:", file print "套接字FD:", socket print "管道FD:", pipe print "其他FD:", other }' 可以发现文件FD很大,基本上发送一个请求文件FD就加一,且不会衰减: 经排查,@validate_and_setup_logger注解会为每次请求都创建一个logger,需要在@app.teardown_request中获取与本次请求有关的logger并释放。 def create_logger(app, subfolder): """ 创建一个唯一的 logger 和对应的输出文件夹。 参数: subfolder (str): 子文件夹名称,如 'output1', 'output2', 'output3' """ unique_id = str(uuid.uuid4()) g.unique_id = unique_id output_folder = os.path.join("flask_app", "static", "output", subfolder, unique_id) os.makedirs(output_folder, exist_ok=True) log_filename = "log.txt" log_path = os.path.join(output_folder, log_filename) logger = logging.getLogger(unique_id) if not logger.handlers: file_handler = logging.FileHandler(log_path) file_formatter = CSTFormatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') file_handler.setFormatter(file_formatter) logger.addHandler(file_handler) stream_handler = logging.StreamHandler() stream_handler.setFormatter(logging.Formatter('%(message)s')) logger.addHandler(stream_handler) logger.setLevel(logging.INFO) logger.propagate = False g.logger = logger g.output_folder = output_folder #输出文件夹路径 handler:每当 logger 生成一条日志信息时,这条信息会被传递给所有关联的 handler,由 handler 决定如何输出这条日志。例如,FileHandler 会把日志写入文件,而 StreamHandler 会将日志输出到控制台。 logger.setLevel(logging.INFO) :它设置了 logger 的日志级别阈值。Logger 只会处理大于或等于 INFO 级别的日志消息(例如 INFO、WARNING、ERROR、CRITICAL),而 DEBUG 级别的消息会被忽略。 解决这个文件句柄问题后内存泄漏仍未解决,考虑分模块排查。 本项目结构大致是**1.**预处理(文件读取切分) **2.**并发调用5个函数分别调用大模型获取结果。 因此排查思路: 先将预处理模块单独拎出来作为接口,上传文件测试。 文件一般几MB,首先会读到内存,再处理,必然会占用很多内存,且它是调用每个接口都会经历的环节(little_zbparse/upload等) 内存泄漏排查工具 pip install memory_profiler from memory_profiler import memory_usage import time @profile def my_function(): a = [i for i in range(100000)] time.sleep(1) # 模拟耗时操作 b = {i: i*i for i in range(100000)} time.sleep(1) return a, b # 监控函数“运行前”和“运行后”的内存快照 mem_before = memory_usage()[0] result=my_function() mem_after = memory_usage()[0] print(f"Memory before: {mem_before} MiB, Memory after: {mem_after} MiB") @profile注解加在函数上,可以逐行分析内存增减情况。 memory_usage()[0] 可以获取当前程序所占内存的快照 产生的数据都存到result变量-》内存中,这是正常的,因此my_function没有内存泄漏问题。 但是 @profile def extract_text_by_page(file_path): result = "" with open(file_path, 'rb') as file: reader =PdfReader(file) num_pages = len(reader.pages) # print(f"Total pages: {num_pages}") for page_num in range(num_pages): page = reader.pages[page_num] text = page.extract_text() return "" 可以发现尽管我返回"",内存仍然没有释放!因为就是读取pdf这块发生了内存泄漏! tracemalloc def extract_text_by_page(file_path): result = "" with open(file_path, 'rb') as file: reader =PdfReader(file) num_pages = len(reader.pages) # print(f"Total pages: {num_pages}") for page_num in range(num_pages): page = reader.pages[page_num] text = page.extract_text() return result # 开始跟踪内存分配 tracemalloc.start() # 捕捉函数调用前的内存快照 snapshot_before = tracemalloc.take_snapshot() # 调用函数 file_path=r'C:\Users\Administrator\Desktop\fsdownload\00550cfc-fd33-469e-8272-9215291b175c\ztbfile.pdf' result = extract_text_by_page(file_path) # 捕捉函数调用后的内存快照 snapshot_after = tracemalloc.take_snapshot() # 比较两个快照,获取内存分配差异信息 stats = snapshot_after.compare_to(snapshot_before, 'lineno') print("[ Top 10 内存变化 ]") for stat in stats[:10]: print(stat) # 停止内存分配跟踪 tracemalloc.stop() tracemalloc能更深入的分析,不仅是自己写的代码,调用的库函数产生的内存也能分析出来。在这个例子中就是PyPDF2中的各个函数占用了大部分内存。 综上,定位到问题,就是读取PDF,使用PyPDF2库的地方 如何解决: 首先尝试用with open打开文件,代替直接使用 reader =PdfReader(file_path) 能够确保文件正常关闭。但是没有效果。 考虑为每次请求开子进程处理,有效隔离内存泄漏导致的资源占用,这样子进程运行结束后会释放资源。 但是解析流程是流式/分段返回的,因此还需处理: _child_target 是一个“桥梁”: 它在子进程内调用 goods_bid_main(...) (你的生成器) 并把每一次 yield 得到的数据放进队列。 结束时放一个 None 表示没有更多数据。 run_in_subprocess 是主进程使用的接口,开启子进程: 它启动子进程并实时 get() 队列数据,然后 yield 给外界调用者。 当队列里读到 None,说明子进程运行完毕,就 break 循环并 p.join()。 main_func是真正执行的函数!!! def _child_target(main_func, queue, output_folder, file_path, file_type, unique_id): """ 子进程中调用 `main_func`(它是一个生成器函数), 将其 yield 出的数据逐条放进队列,最后放一个 None 表示结束。 """ try: for data in main_func(output_folder, file_path, file_type, unique_id): queue.put(data) except Exception as e: # 如果要把异常也传给父进程,以便父进程可感知 queue.put(json.dumps({'error': str(e)}, ensure_ascii=False)) finally: queue.put(None) def run_in_subprocess(main_func, output_folder, file_path, file_type, unique_id): """ 启动子进程调用 `main_func(...)`,并在父进程流式获取其输出(通过 Queue)。 子进程结束时,操作系统回收其内存;父进程则保持实时输出。 """ queue = multiprocessing.Queue() p = multiprocessing.Process( target=_child_target, args=(main_func, queue, output_folder, file_path, file_type, unique_id) ) p.start() while True: item = queue.get() # 阻塞等待子进程产出的数据 if item is None: break yield item p.join() 如果开子线程,线程共享同一进程的内存空间,所以如果发生内存泄漏,泄漏的内存会累积在整个进程中,影响所有线程。 开子进程的缺点:多进程通常消耗的系统资源(如内存、启动开销)比多线程要大,因为每个进程都需要独立的资源和上下文切换开销。 进程池 在判断上传的文件是否为招标文件时,需要快速准确地响应。因此既保证内存不泄漏,又保证速度的方案就是在项目启动时创建进程池。(因为创建进程需要耗时2到3秒!) 如果是Waitress服务器启动,这里的进程池是全局共享的;但如果Gunicorn启动,每个请求分配一个worker进程,进程池是在worker里面共享的!!! #创建app,启动时 def create_app(): # 创建全局日志记录器 app = Flask(__name__) app.process_pool = Pool(processes=10, maxtasksperchild=3) app.global_logger = create_logger_main('model_log') # 全局日志记录器 #调用时 pool = current_app.process_pool # 使用全局的进程池 def judge_zbfile_exec_sub(file_path): result = pool.apply( judge_zbfile_exec, # 你的实际执行函数 args=(file_path,) ) return result 但是存在一个问题:第一次发送请求执行时间较慢! 可以发现实际执行只需7.7s,但是接口实际耗时10.23秒,主要是因懒加载或按需初始化:有些模块或资源在子进程启动时并不会马上加载,而是在子进程首次真正执行任务时才进行初始化。 解决思路:提前热身(warm up)进程池 在应用启动后、还没正式接受请求之前,可以提交一个简单的“空任务”或非常小的任务给进程池,让子进程先完成相关的初始化。这种“预热”方式能在正式请求到来之前就完成大部分初始化,减少首次请求的延迟。 还可以快速验证服务是否正常启动 def warmup_request(): # 等待服务器完全启动,例如等待 1-2 秒 time.sleep(5) try: url = "http://127.0.0.1:5000/judge_zbfile" #url必须为永久地址,完成热启动,创建进程池 payload = {"file_url": "xxx"} # 根据实际情况设置 file_url headers = {"Content-Type": "application/json"} response = requests.post(url, json=payload, headers=headers) print(f"Warm-up 请求发送成功,状态码:{response.status_code}") except Exception as e: print(f"Warm-up 请求出错:{e}") threading.Thread(target=warmup_request, daemon=True).start() flask_app结构介绍 项目中做限制的地方 账号、服务器分流 服务器分流:目前linux服务器和windows服务器主要是硬件上的分流(文件切分需要消耗CPU资源),大模型基底还是调用阿里,共用的tpm qpm。 账号分流:qianwen_plus下的 api_keys = cycle([ os.getenv("DASHSCOPE_API_KEY"), # os.getenv("DASHSCOPE_API_KEY_BACKUP1"), # os.getenv("DASHSCOPE_API_KEY_BACKUP2") ]) api_keys_lock = threading.Lock() def get_next_api_key(): with api_keys_lock: return next(api_keys) api_key = get_next_api_key() 只需轮流使用不同的api_key即可。目前没有启用。 大模型的限制 general/llm下的doubao.py 和通义千问long_plus.py 目前是linux和windows各部署一套,因此项目中的qps是对半的,即calls=? 这是qianwen-long的限制(针对阿里qpm为1200,每秒就是20,又linux和windows服务器对半,就是10;TPM无上限) @sleep_and_retry @limits(calls=10, period=1) # 每秒最多调用10次 def rate_limiter(): pass # 这个函数本身不执行任何操作,只用于限流 这是qianwen-plus的限制(针对tpm为1000万,每个请求2万tokens,那么linux和windows总的qps为8时,8x60x2=960<1000。单个为4) 经过2.11号测试,calls=4时最高TPM为800,因此把目前稳定版把calls设为5 2.12,用turbo作为超限后的承载,目前把calls设为7 @sleep_and_retry @limits(calls=7, period=1) # 每秒最多调用7次 def qianwen_plus(user_query, need_extra=False): logger = logging.getLogger('model_log') # 通过日志名字获取记录器 qianwen_turbo的限制(TPM为500万,由于它是plus后的手段,稳妥一点,qps设为6,两个服务器分流即calls=3) @sleep_and_retry @limits(calls=3, period=1) # 500万tpm,每秒最多调用6次,两个服务器分流就是3次 (plus超限后的保底手段,稳妥一点) 重点!!后续阿里扩容之后成倍修改这块calls=? 如果不用linux和windows负载均衡,这里的calls也要乘2!! 接口的限制 start_up.py的def create_app()函数,限制了对每个接口同时100次请求。这里事实上不再限制了(因为100已经足够大了),默认限制做到大模型限制这块。 app.connection_limiters['upload'] = ConnectionLimiter(max_connections=100) app.connection_limiters['get_deviation'] = ConnectionLimiter(max_connections=100) app.connection_limiters['default'] = ConnectionLimiter(max_connections=100) app.connection_limiters['judge_zbfile'] = ConnectionLimiter(max_connections=100) ConnectionLimiter.py以及每个接口上的装饰器,如 @require_connection_limit(timeout=1800) def zbparse(): 这里限制了每个接口内部执行的时间,暂时设置到了30分钟!(不包括排队时间)超时就是解析失败 后端的限制: 目前后端发起招标请求,如果发送超过100(max_connections=100)个请求,我这边会排队后面的请求,这时后端的计时器会将这些请求也视作正在解析中,事实上它们还在排队等待中,这样会导致在极端情况下,新进的解析文件速度大于解析的速度,排队越来越长,后面的文件会因为等待时间过长而直接失败,而不是'解析失败'。 general 是公共函数存放的文件夹,llm下是各类大模型,读取文件下是docx pdf文件的读取以及文档清理clean_pdf,去页眉页脚页码 general下的llm下的清除file_id.py 需要每周运行至少一次,防止file_id数量超出(我这边对每次请求结束都有file_id记录并清理,向应该还没加) llm下的model_continue_query是'模型继续回答'脚本,应对超长文本模型一次无法输出完的情况,继续提问,拼接成完整的内容。 general下的file2markdown是textin 文件--》markdown general下的format_change是pdf-》docx 或doc/docx->pdf general下的merge_pdfs.py是拼接文件的:1.拼接招标公告+投标人须知 2.拼接评标细则章节+资格审查章节 general中比较重要的!!! 后处理: general下的post_processing,解析后的后处理部分,包括extract_info、 资格审查、技术偏离 商务偏离 所需提交的证明材料,都在这块生成。 post_processing中的inner_post_processing专门提取extracted_info post_processing中的process_functions_in_parallel提取 资格审查、技术偏离、 商务偏离、 所需提交的证明材料 大解析upload用了post_processing完整版, little_zbparse.py、小解析main.py用了inner_post_processing get_deviation.py、偏离表数据解析main.py用了process_functions_in_parallel 截取pdf: 截取pdf_main.py是顶级函数, 二级是截取pdf货物标版.py和截取pdf工程标版.py (非general下) 三级是截取pdf通用函数.py 如何判断截取位置是否正确?根据output文件夹中的切分情况(打开各个文件查看是否切分准确,目前的逻辑主要是按大章切分,即'招标公告'章节) 如果切分不准确,如何定位正则表达式? 首先判断当前是工程标解析还是货物标解析,即zb_type=1还是2 如果是2,那么是货物标解析,那么就是截取pdf_main.py调用截取pdf货物标版.py,如下图,selection=1代表截取'招标公告',那么如果招标公告没有切准,就在这块修改。这里可以发现get_notice是通用函数,即截取pdf通用函数.py中的get_notice函数,那么继续往内部跳转。 若开头没截准,就改begin_pattern,末尾没截准,就改end_pattern 另外:在截取pdf货物标版.py中,还有extract_pages_twice函数,即第一次没有切分到之后,会运行该函数,这边又有一套begin_pattern和end_pattern,即二次提取 如何测试? 输入pdf_path,和你要切分的序号,selection=1代表切公告,依次类推,可以看切出来的效果如何。 无效标和废标公共代码 获取无效标与废标项的主要执行代码。对docx文件进行预处理=》正则=》temp.txt=》大模型筛选 如果提的不全,可能是正则没涵盖到位,也可能是大模型提示词漏选了。 这里:如果段落中既被正则匹配,又被follow_up_keywords中的任意一个匹配,那么不会添加到temp中(即不会被大模型筛选),它会直接添加到最后的返回中! 投标人须知正文条款提取成json文件 将截取到的ztbfile_tobidders_notice_part2.pdf ,即须知正文,转为clause1.json 文件,便于后续提取开评定标流程、投标文件要求、重新招标、不再招标和终止招标 这块的主要逻辑就是匹配形如'一、总则'这样的大章节 然后匹配形如'1.1' '1.1.1'这样的序号,由于是按行读取pdf,一个序号后面的内容可能有好几行,因此遇到下一个序号(如'2.1')开头,之前的内容都视为上一个序号的。 old_version 都是废弃文件代码,未在正式、测试环境中使用的,不用管 routes 是接口以及主要实现部分,一一对应 get_deviation对应偏离表数据解析main,获得偏离表数据 judge_zbfile对应判断是否是招标文件 little_zbparse对应小解析main,负责解析extract_info test_zbparse是测试接口,无对应 upload对应工程标解析和货物标解析,即大解析 混淆澄清:小解析可以指代一个过程,即从'投标文件生成'这个入口进去的解析,后端会同时调用little_zbparse和get_deviation。这个过程称为'小解析'。 但是little_zbparse也叫小解析,命名如此因为最初只需返回这些数据(extract_info),后续才陆续返回商务、技术偏离... utils是接口这块的公共功能函数。其中validate_and_setup_logger函数对不同的接口请求对应到不同的output文件夹,如upload->output1。后续增加接口也可直接在这里写映射关系。 重点关注大解析:upload.py和货物标解析main.py static 存放解析的输出和提示词 其中output用gitignore了,git push不会推送这块内容。 各个文件夹(output1 output2..)对应不同的接口请求 test_case&testdir test_case是测试用例,是对一些函数的测试。好久没更新了 testdir是平时写代码的测试的地方 它们都不影响正式和测试环境的解析 工程标&货物标 是两个解析流程中不一样的地方(一样的都写在general中了) 主要是货物标额外解析了采购要求(提取采购需求main+技术参数要求提取+商务服务其他要求提取) 最后: ConnectionLimiter.py定义了接口超时时间->超时后断开与后端的连接 logger_setup.py 为每个请求创建单独的log,每个log对应一个log.txt start_up.py是启动脚本,run_serve也是启动脚本,是对start_up.py的简单封装,目前dockerfile定义的直接使用run_serve启动 持续关注 yield sse_format(tech_deviation_response) yield sse_format(tech_deviation_star_response) yield sse_format(zigefuhe_deviation_response) yield sse_format(shangwu_deviation_response) yield sse_format(shangwu_star_deviation_response) yield sse_format(proof_materials_response) 工程标解析目前仍没有解析采购要求这一块,因此后处理返回的只有'资格审查'和''证明材料"和"extracted_info",没有''商务偏离''及'商务带星偏离',也没有'技术偏离'和'技术带星偏离',而货物标解析是完全版。 其中''证明材料"和"extracted_info"是直接返给后端保存的 大解析中返回了技术评分,后端接收后不仅显示给前端,还会返给向,用于生成技术偏离表 小解析时,get_deviation.py其实也可以返回技术评分,但是没有返回,因为没人和我对接,暂时注释了。 4.商务评议和技术评议偏离表,即评分细则的偏离表,暂时没做,但是商务评分、技术评分无论大解析还是小解析都解析了,稍微对该数据处理一下返回给后端就行。 这个是解析得来的结果,适合给前端展示,但是要生成商务技术评议偏离表的话,需要再调一次大模型,对该数据进行重新归纳,以字符串列表为佳。再传给后端。(未做) 如何定位问题 查看static下的output文件夹 (upload大解析对应output1) docker-compose文件中规定了数据卷挂载的路径:- /home/Z/zbparse_output_dev:/flask_project/flask_app/static/output 也就是说static/output映射到了服务器的Z/zbparse_output_dev文件夹 根据时间查找哪个子文件夹(uuid作为子文件名) 查看是否有final_result.json文件,如果有,说明解析流程正常结束了,问题可能出在后端(a.后端接口请求超限30分钟 b.后处理存在解析数据的时候出错) 也可能出现在自身解析,可以查看子文件内的log.txt,查看日志。 若解析正常(有final_result)但解析不准,可以根据以下定位: a.查看子文件夹下的文件切分是否准确,例如:如果评标办法不准确,那么查看ztbfile_evaluation_methon,是否正确切到了评分细则。如果切到了,那就改general/商务技术评分提取里的提示词;否则修改截取pdf那块关于'评标办法'的正则表达式。 b.总之是先看切的准不准,再看提示词能否优化,都要定位到对应的代码中! 学习总结 Flask + Waitress : Flask 和 Waitress 是两个不同层级的工具,在 Python Web 开发中扮演互补角色。它们的协作关系可以概括为:Flask 负责构建 Web 应用逻辑,而 Waitress 作为生产级服务器承载 Flask 应用。 # Flask 开发服务器(仅用于开发) if __name__ == '__main__': app.run(host='0.0.0.0', port=5000) # 使用 Waitress 启动(生产环境) from waitress import serve serve(app, host='0.0.0.0', port=8080) Waitress 的工作方式 作为 WSGI 服务器:Waitress 作为一个 WSGI 服务器,负责监听指定端口上的网络请求,并将请求传递给 WSGI 应用(如 Flask 应用)。 多线程处理:默认情况下,waitress 在单个进程内启用线程池。当请求到达时,waitress 会从线程池中分配一个线程来处理这个请求。由于 GIL 限制,同一时间只有一个线程在执行 Python 代码(只能使用一个核心,CPU利用率只能到100%)。 Flask 与 waitress 的协同工作 WSGI 接口:Flask 应用实现了 WSGI 接口。waitress 接收到请求后,会调用 Flask 应用对应的视图函数来处理请求,生成响应。 请求处理流程 请求进入 waitress waitress 分配一个线程并调用 Flask 应用 Flask 根据路由匹配并执行对应的处理函数 处理函数返回响应,waitress 将响应发送给客户端 Waitress 的典型使用场景 跨平台部署:尤其适合 Windows 环境(Gunicorn 等服务器不支持)。 简单配置:无需复杂设置即可获得比开发服务器(Flask自带)更强的性能。 中小型应用:对并发要求不极高的场景,Waitress 的轻量级特性优势明显。 Waitress的不足与处理 由于 waitress 是在单进程下工作,所有线程共享进程内存,如果业务逻辑简单且无复杂资源共享问题,这种方式是足够的。 引入子进程:如果需要每个请求实现内存隔离或者绕过 GIL 来利用多核 CPU,有时会在 Flask 视图函数内部启动子进程来处理实际任务。 直接采用多进程部署方案:使用 Gunicorn 的多 worker 模式 Gunicorn Gunicorn 的工作方式 预启动 Worker 进程。Gunicorn 启动时,会按照配置数量(例如 4 个 worker)创建多个 worker 进程。这些 worker 进程会一直运行,并监听同一个端口上的请求。不会针对每个请求单独创建新进程。 共享 socket:所有 worker 进程共享同一个监听 socket,当有请求到来时,操作系统会将请求分发给某个空闲的 worker。 推荐worker 数量 = (2 * CPU 核心数) + 1 如何启动: 要使用异步 worker,你需要: pip install gevent 启动 Gunicorn 时指定 worker 类型和数量,例如: gunicorn -k gevent -w 4 --max-requests 100 flask_app.start_up:create_app --bind 0.0.0.0:5000 使用 -k gevent(或者 -k eventlet)就可以使用异步 worker,单个 worker 能够处理多个 I/O 密集型请求。 使用--max-requests 100 。每个 worker 在处理完 100 个请求后会自动重启,从而释放可能累积的内存。 本项目的执行流程: 调用CPU进行PDF文件的读取与切分,CPU密集型,耗时半分钟 针对切分之后的不同部分,分别调用大模型,得到回答,IO密集型,耗时2分钟。 解决方案: 1.使用flask+waitress,waitress会为每个用户请求开新的线程处理,然后我的代码逻辑会在这个线程内开子进程来执行具体的代码,以绕过GIL限制,且正确释放内存资源。 **后续可以开一个共享的进程池代替为每个请求开子进程。以避免高并发下竞争多核导致的频繁CPU切换问题。 2.使用Gunicorn的异步worker,gunicorn为固定创建worker(进程),处理用户请求,一个异步 worker 可以同时处理多个用户请求,因为当一个请求在等待外部响应(例如调用大模型接口)时,worker 可以切换去处理其他请求。 全局解释器锁(GIL): Python(特别是 CPython 实现)中有一个叫做全局解释器锁(Global Interpreter Lock,简称 GIL)的机制,这个锁确保在任何时刻只有一个线程在执行 Python 字节码。 这意味着,即使你启动了多个线程,它们在执行 Python 代码时实际上是串行执行的,而不是并行利用多核 CPU。 在 Java 中,多线程通常能充分利用多核,因为 Java 的线程是真正的系统级线程,不存在类似 CPython 中的 GIL 限制。 影响: CPU密集型任务:由于 GIL 的存在,在 CPU 密集型任务中,多线程往往不能提高性能,因为同时只有一个线程在执行 Python 代码。 I/O密集型任务:如果任务主要等待 I/O(例如网络、磁盘读写),线程在等待时会释放 GIL,此时多线程可以提高程序的响应性和吞吐量。 NumPy能够在一定程度上绕过 Python 的 GIL 限制。许多 NumPy 的数值计算操作(如矩阵乘法、向量化运算等)是由高度优化的 C 或 Fortran 库(如 BLAS、LAPACK)实现的。这些库通常在执行计算密集型任务时会释放 GIL。C 扩展模块的方式将 C 代码嵌入到 Python 中,从而利用底层 C 库的高性能优势 进程与线程 1、进程是操作系统分配任务的基本单位,进程是python中正在运行的程序;当我们打开了1个浏览器时就是开始了一个浏览器进程; 线程是进程中执行任务的基本单元(执行指令集),一个进程中至少有一个线程、当只有一个线程时,称为主线程 2、线程的创建和销毁耗费资源少,进程的创建和销毁耗费资源多;线程很容易创建,进程不容易创建 3、线程的切换速度快,进程慢 4、一个进程中有多个线程时:线程之间可以进行通信;一个进程中有多个子进程时,进程与进程之间不可以相互通信,如果需要通信时,就必须通过一个中间代理实现,Queue、Pipe。 5、多进程可以利用多核cpu,多线程不可以利用多核cpu 6、一个新的线程很容易被创建,一个新的进程创建需要对父进程进行一次克隆 7、多进程的主要目的是充分使用CPU的多核机制,多线程的主要目的是充分利用某一个单核 ——————————————— 每个进程有自己的独立 GIL 多线程适用于 I/O 密集型任务 多进程适用于CPU密集型任务 因此,多进程用于充分利用多核,进程内开多线程以充分利用单核。 进程池 multiprocessing.Pool库:,通过 maxtasksperchild 指定每个子进程在退出前最多执行的任务数,这有助于防止某些任务中可能存在的内存泄漏问题 pool =Pool(processes=10, maxtasksperchild=3) concurrent.futures.ProcessPoolExecutor更高级、更统一,没有类似 maxtasksperchild 的参数,意味着进程在整个执行期内会一直存活,适合任务本身比较稳定的场景。 pool =ProcessPoolExecutor(max_workers=10) 最好创建的进程数等同于CPU核心数,如果大于,且每个进程都是CPU密集型(高负债一直用到CPU),那么进程之间会竞争CPU,导致上下文切换增加,反而会降低性质。 设置的工作进程数接近 CPU 核心数,以便每个进程能独占一个核运行。 进程、线程间通信 线程间通信: 线程之间可以直接共享全局变量、对象或数据结构,不需要额外的序列化过程,但这也带来了同步的复杂性(如竞态条件)。 import threading num=0 def work(): global num for i in range(1000000): num+=1 print('work',num) def work1(): global num for i in range(1000000): num+=1 print('work1',num) if __name__ == '__main__': t1=threading.Thread(target=work) t2=threading.Thread(target=work1) t1.start() t2.start() t1.join() t2.join() print('主线程执行结果',num) 运行结果: work 1551626 work1 1615783 主线程执行结果 1615783 这些数值都小于预期的 2000000,因为: 即使存在 GIL,num += 1 这样的操作实际上并不是原子的。GIL 确保同一时刻只有一个线程执行 Python 字节码,但在执行 num += 1 时,实际上会发生下面几步操作: 从内存中读取 num 的当前值 对读取到的值进行加 1 操作 将新的值写回到内存 由多个字节码组成!!! 因此会导致: 线程 A 读取到 num 的值 切换到线程 B,线程 B 也读取同样的 num 值并进行加 1,然后写回 当线程 A 恢复时,它依然基于之前读取的旧值进行加 1,最后写回,从而覆盖了线程 B 的更新 解决: from threading import Lock import threading num=0 def work(): global num for i in range(1000000): with lock: num+=1 print('work',num) def work1(): global num for i in range(1000000): with lock: num+=1 print('work1',num) if __name__ == '__main__': lock=Lock() t1=threading.Thread(target=work) t2=threading.Thread(target=work1) t1.start() t2.start() t1.join() t2.join() print('主线程执行结果',num) 进程间通信(IPC): 进程之间默认不共享内存,因此如果需要传递数据,就必须使用专门的通信机制。 在 Python 中,可以使用 multiprocessing.Queue、multiprocessing.Pipe、共享内存(如 multiprocessing.Value 和 multiprocessing.Array)等方式实现进程间通信。 from multiprocessing import Process, Queue def worker(process_id, q): # 每个进程将数据放入队列 q.put(f"data_from_process_{process_id}") print(f"Process {process_id} finished.") if __name__ == '__main__': q = Queue() processes = [] for i in range(5): p = Process(target=worker, args=(i, q)) processes.append(p) p.start() for p in processes: p.join() # 从队列中收集数据 results = [] while not q.empty(): results.append(q.get()) print("Collected data:", results) 当你在主进程中创建了一个 Queue 对象,然后将它作为参数传递给子进程时,子进程会获得一个能够与主进程通信的“句柄”。 子进程中的 q.put(...) 操作会将数据通过这个管道传送到主进程,而主进程可以通过 q.get() 来获取这些数据。 这种机制虽然看起来像是“共享”,但实际上是通过 IPC(进程间通信)实现的,而不是直接共享内存中的变量。 项目贡献 效果图
项目
zy123
3月21日
0
7
0
2025-03-21
微服务
微服务 踩坑总结 Mybatis-PLUS 分页不生效,因为mybatis-plus自3.5.9起,默认不包含分页插件,需要自己引入。 <dependencyManagement> <dependencies> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-bom</artifactId> <version>3.5.9</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <!-- MyBatis Plus 分页插件 --> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-jsqlparser-4.9</artifactId> </dependency> config包下新建: @Configuration @MapperScan("edu.whut.smilepicturebackend.mapper") public class MybatisPlusConfig { /** * 拦截器配置 * * @return {@link MybatisPlusInterceptor} */ @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); // 分页插件 interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } } 雪花算法表示精度问题 “雪花算法”(Snowflake)生成的 ID 本质上是一个 64 位的整数(Java等后端里通常对应 long ),而浏览器端的 JavaScript Number 类型只能安全地表示到 2^53−1 以内的整数,超出这个范围就会出现 “精度丢失”──即低位那几位数字可能会被四舍五入掉,导致 ID 读取或比对出错。因此,最佳实践是: 后端依然用 long(或等价的 64 位整数)存储和处理雪花 ID。 对外接口(REST/graphQL 等)返回时,将这类超出 JS 安全范围的整数序列化为字符串,比如: @Configuration public class JacksonConfig { private static final String DATE_FORMAT = "yyyy-MM-dd"; private static final String DATETIME_FORMAT = "yyyy-MM-dd HH:mm:ss"; private static final String TIME_FORMAT = "HH:mm:ss"; @Bean public Jackson2ObjectMapperBuilderCustomizer jacksonCustomizer() { return builder -> { // 将所有 long / Long 类型序列化成 String SimpleModule longToString = new SimpleModule(); longToString.addSerializer(Long.class, ToStringSerializer.instance); longToString.addSerializer(Long.TYPE, ToStringSerializer.instance); builder.modules(longToString); }; } } 包扫描问题(非常容易出错!) 以 Spring Boot 为例,框架默认会扫描启动类所在包及其子包中的组件(@Component/@Service/@Repository/@Configuration 等),将它们注册到 Spring 容器中。 问题:当你把某些业务组件、配置类或第三方模块放在了启动类的同级或平级包下(而非子包),却没有手动指定扫描路径,就会出现 “无法注入 Bean” 的情况。 // 启动类 @SpringBootApplication public class OrderServiceApplication { … } // 业务类位于 com.example.common 包 @Service public class PaymentClient { … } 如果项目结构是: com.example.orderservice ← 启动类 com.example.common ← 依赖组件 默认情况下 com.example.common 不会被扫描到,导致注入 PaymentClient 时抛出 NoSuchBeanDefinitionException。 解决方案: 1)显式指定扫描路径**: @SpringBootApplication @ComponentScan(basePackages = { "com.example.orderservice", "com.example.common" }) public class OrderServiceApplication { … } 2)使用 @Import 或者 Spring Cloud 的自动配置机制(如编写 spring.factories,让依赖模块自动装配)。 数据库连接池 为什么需要? 每次通过 JDBC 调用 DriverManager.getConnection(...),都要完成网络握手、权限验证、初始化会话等大量开销,通常耗时在几十到几百毫秒不等。连接池通过提前建立好 N 条物理连接并在应用各处循环复用,避免了反复的开销。 流程 数据库连接池在应用启动时预先创建一定数量的物理连接,并将它们保存在空闲队列中;当业务需要访问数据库时,直接从池中“借用”一个连接(无需新建),用完后调用 close() 即把它归还池中;池会根据空闲超时或最大寿命策略自动回收旧连接,并在借出或定期扫描时执行简单心跳(如 SELECT 1)来剔除失效连接,确保始终有可用、健康的连接供高并发场景下快速复用。 ┌─────────────────────────────────────────┐ │ 应用线程 A 调用 getConnection() │ │ ┌──────────┐ ┌─────────────┐ │ │ │ 空闲连接队列 │──取出──▶│ 物理连接 │───┐│ │ └──────────┘ └─────────────┘ ││ │ (代理包装) ││ │ 返回代理连接给业务代码 ││ └─────────────────────────────────────────┘ │ │ ┌─────────────────────────────────────────┐ │ │ 业务执行 SQL,最后调用 close() │ │ ┌───────────────┐ ┌────────────┐ │ │ │ 代理 Connection │──归还──▶│ 空闲连接队列 │◀─────┘ │ └───────────────┘ └────────────┘ └─────────────────────────────────────────┘ 当你从连接池里拿到一个底层已被远程关闭的连接时,HikariCP(以及大多数成熟连接池)会在“借出”前先做一次简易校验(默认为 Connection.isValid(),或你配置的 connection-test-query)。如果校验失败,连接池会自动将这条“死”连接销毁,并尝试从池里或新建一个新的物理连接来替换,再把新的健康连接返给业务;只有当新的连接也创建或校验失败到达池的最大重试次数时,才会抛出拿不到连接的超时异常。 遇到的问题 如果本地启动了 Java 应用和前端 Nginx,而 MySQL 部署在远程服务器上,Java 应用通过连接池与远程数据库建立的 TCP 连接在 5 分钟内若无任何 SQL 操作,就会因中间网络设备(如 NAT、负载均衡器、防火墙)超时断开,且应用层不会主动感知,导致后续 SQL 请求失败。 13:20:01:383 WARN 43640 --- [nio-8084-exec-4] com.zaxxer.hikari.pool.PoolBase : HikariPool-1 - Failed to validate connection com.mysql.cj.jdbc.ConnectionImpl@36e971ae (No operations allowed after connection closed.). Possibly consider using a shorter maxLifetime value. 13:20:01:384 ERROR 43640 --- [nio-8084-exec-4] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.exceptions.PersistenceException: ### Error querying database. Cause: org.springframework.jdbc.CannotGetJdbcConnectionException: Failed to obtain JDBC Connection; nested exception is java.sql.SQLTransientConnectionException: HikariPool-1 - Connection is not available, request timed out after 30048ms. 为了解决这个问题, 1.只需在 Spring Boot 配置中为 HikariCP 添加定期心跳,让连接池在真正断连前保持流量: spring: datasource: hikari: keepalive-time: 180000 # 3 分钟发送一次心跳(维持 TCP 活跃) 这样,HikariCP 会每隔 3 分钟自动对空闲连接执行轻量级的验证操作(如 Connection.isValid()),确保中间网络链路不会因长时间静默而被强制关闭。 2.如果JAVA应用和Mysql在同一服务器上(可互通),就不会有上述问题! Sentinel无数据 sentinel 控制台可以发现哪些微服务连接了,但是Dashboard 在尝试去拿各个微服务上报的规则(端点 /getRules)和指标(端点 /metric)时,一直连不上它们,因为JAVA微服务是在本地私网内部署的,Dashboard无法连接上。 Failed to fetch metric from http://192.168.0.107:8725/metric?… Failed to fetch metric from http://192.168.0.107:8721/metric?… HTTP request failed: http://192.168.0.107:8721/getRules?type=flow java.net.ConnectException: Operation timed out 解决办法: 1.将JAVA应用部署到服务器,但我的服务器内存不够 2.将Dashboard部署到本机docker中,和JAVA应用可互通。 Nacos迁移后的 No DataSource set 原本Nacos和Mysql都是部署到公网服务器,mysql容器对外暴露3307,因此Nacos的env文件中可以是: MYSQL_SERVICE_DB_NAME=124.xxx.xxx.xxx MYSQL_SERVICE_PORT=3307 填的mysql的公网ip,以及它暴露的端口3307,这是OK的 但是如果将它们部署在docker同一网络中,应该这样写: MYSQL_SERVICE_DB_NAME=mysql MYSQL_SERVICE_PORT=3306 mysql是服务名,不能写localhost(或 127.0.0.1),它永远只会指向「当前容器自己」!!! 注意,Nacos中的配置文件也要迁移过来,导入nacos配置列表中,并且修改JAVA项目中nacos的地址 Docker Compose问题 1)如果你把某个服务从 docker-compose.yml 里删掉,然后再执行: docker compose down 默认情况下 并不会 停止或删除那个已经“离开”了 Compose 配置的容器。 只能: docker compose down --remove-orphans #清理这些“孤儿”容器 或者手动清理: docker ps #列出容器 docker stop <container_id_or_name> docker rm <container_id_or_name> 2)端口占用问题 Error response from daemon: Ports are not available: exposing port TCP 0.0.0.0:5672 -> 0.0.0.0:0: listen tcp 0.0.0.0:5672: bind: An attempt was made to access a socket in a way forbidden by its access permissions. 先查看是否端口被占用: netstat -aon | findstr 5672 如果没有被占用,那么就是windows的bug,在CMD使用管理员权限重启NAT网络服务即可 net stop winnat net start winnat 3)ip地址问题 seata-server: image: seataio/seata-server:1.5.2 container_name: seata-server restart: unless-stopped depends_on: - mysql - nacos environment: # 指定 Seata 注册中心和配置中心地址 - SEATA_IP=192.168.10.218 # IDEA 可以访问到的宿主机 IP - SEATA_SERVICE_PORT=17099 - SEATA_CONFIG_TYPE=file # 可视情况再加:SEATA_NACOS_SERVER_ADDR=nacos:8848 networks: - hmall-net ports: - "17099:7099" # TC 服务端口 - "8099:8099" # 服务管理端口(Console) volumes: - ./seata:/seata-server/resources SEATA_IP配置的是宿主机IP,你的电脑换了IP,如从教室到寝室,那这里的IP也要跟着变:ipconfig查看宿主机ip 认识微服务 微服务架构,首先是服务化,就是将单体架构中的功能模块从单体应用中拆分出来,独立部署为多个服务。 SpringCloud 使用Spring Cloud 2021.0.x以及Spring Boot 2.7.x版本(需要对应)。 在父pom中的<dependencyManagement>锁定版本,使得后续你在子模块里引用 Spring Cloud 或 Spring Cloud Alibaba 的各个组件时,不需要再写 <version>,Maven 会统一采用你在父 POM 中指定的版本。 微服务拆分 微服务拆分时: 高内聚:每个微服务的职责要尽量单一,包含的业务相互关联度高、完整度高。 低耦合:每个微服务的功能要相对独立,尽量减少对其它微服务的依赖,或者依赖接口的稳定性要强。 一般微服务项目有两种不同的工程结构: 完全解耦:每一个微服务都创建为一个独立的工程,甚至可以使用不同的开发语言来开发,项目完全解耦。 优点:服务之间耦合度低 缺点:每个项目都有自己的独立仓库,管理起来比较麻烦 Maven聚合:整个项目为一个Project,然后每个微服务是其中的一个Module 优点:项目代码集中,管理和运维方便 缺点:服务之间耦合,编译时间较长 ,每个模块都要有:pom.xml application.yml controller service mapper pojo 启动类 IDEA配置小技巧 1.自动导包 2.配置service窗口,以显示多个微服务启动类 3.如何在idea中虚拟多服务负载均衡? More options->Add VM options -> -Dserver.port=xxxx 这边设置不同的端口号! 服务注册和发现 注册中心、服务提供者、服务消费者三者间关系如下: 流程如下: 服务启动时就会注册自己的服务信息(服务名、IP、端口)到注册中心 调用者可以从注册中心订阅想要的服务,获取服务对应的实例列表(1个服务可能多实例部署) 调用者自己对实例列表负载均衡,挑选一个实例 调用者向该实例发起远程调用 当服务提供者的实例宕机或者启动新实例时,调用者如何得知呢? 服务提供者会定期向注册中心发送请求,报告自己的健康状态(心跳请求) 当注册中心长时间收不到提供者的心跳时,会认为该实例宕机,将其从服务的实例列表中剔除 当服务有新实例启动时,会发送注册服务请求,其信息会被记录在注册中心的服务实例列表 当注册中心服务列表变更时,会主动通知微服务,更新本地服务列表(防止服务调用者继续调用挂逼的服务) Nacos部署: 1.依赖mysql中的一个数据库 ,可由nacos.sql初始化 2.需要.env文件,配置和数据库的连接信息: PREFER_HOST_MODE=hostname MODE=standalone SPRING_DATASOURCE_PLATFORM=mysql MYSQL_SERVICE_HOST=124.71.159.*** MYSQL_SERVICE_DB_NAME=nacos MYSQL_SERVICE_PORT=3307 MYSQL_SERVICE_USER=root MYSQL_SERVICE_PASSWORD=******* MYSQL_SERVICE_DB_PARAM=characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai 3.docker部署: nacos: image: nacos/nacos-server:v2.1.0 container_name: nacos-server restart: unless-stopped env_file: - ./nacos/custom.env # 自定义环境变量文件 ports: - "8848:8848" # Nacos 控制台端口 - "9848:9848" # RPC 通信端口 (TCP 长连接/心跳) - "9849:9849" # gRPC 通信端口 networks: - hm-net depends_on: - mysql volumes: - ./nacos/init.d:/docker-entrypoint-init.d # 如果需要额外初始化脚本,可选 启动完成后,访问地址:http://ip:8848/nacos/ 初始账号密码都是nacos 服务注册 1.在item-service的pom.xml中添加依赖: <!--nacos 服务注册发现--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> 2.配置Nacos 在item-service的application.yml中添加nacos地址配置: spring: application: name: item-service #服务名 cloud: nacos: server-addr: 124.71.159.***:8848 # nacos地址 注意,服务注册默认连9848端口!云服务需要开启该端口! 配置里的item-service就是服务名! 多个实例注册 version: '3' services: item-service-1: image: item-service container_name: item-service-1 environment: - spring.application.name=item-service - nacos.server-addr=124.71.159.***:8848 ports: - "8081:8080" # 映射端口 8081 item-service-2: image: item-service container_name: item-service-2 environment: - spring.application.name=item-service - nacos.server-addr=124.71.159.***:8848 ports: - "8082:8080" # 映射端口 8082 item-service-1 和 item-service-2 都会向 Nacos 注册为名为 item-service 的服务实例,但它们是不同的容器实例,具有不同的端口和 instanceId。 这样就能实现多实例部署,可以负载均衡了。 服务发现 前两步同服务注册 3.通过 DiscoveryClient 发现服务实例列表,然后通过负载均衡算法,选择一个实例去调用 discoveryClient发现服务 + restTemplate远程调用 @Service public class CartServiceImpl { @Autowired private DiscoveryClient discoveryClient; // 注入 DiscoveryClient @Autowired private RestTemplate restTemplate; // 用于发 HTTP 请求 private void handleCartItems(List<CartVO> vos) { // 1. 获取商品 id 列表 Set<Long> itemIds = vos.stream() .map(CartVO::getItemId) .collect(Collectors.toSet()); // 2.1. 发现 item-service 服务的实例列表 List<ServiceInstance> instances = discoveryClient.getInstances("item-service"); // 2.2. 负载均衡:随机挑选一个实例 ServiceInstance instance = instances.get( RandomUtil.randomInt(instances.size()) ); // 2.3. 发送请求,查询商品详情 String url = instance.getUri().toString() + "/items?ids={ids}"; ResponseEntity<List<ItemDTO>> response = restTemplate.exchange( url, HttpMethod.GET, null, new ParameterizedTypeReference<List<ItemDTO>>() {}, String.join(",", itemIds) ); // 2.4. 处理结果 if (response.getStatusCode().is2xxSuccessful()) { List<ItemDTO> items = response.getBody(); // … 后续处理 … } else { throw new RuntimeException("查询商品失败: " + response.getStatusCode()); } } } OpenFeign 让远程调用像本地方法调用一样简单 快速入门 1.引入依赖 <!--openFeign--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> <!--负载均衡器--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-loadbalancer</artifactId> </dependency> 2.启用OpenFeign 在服务调用者cart-service的CartApplication启动类上添加注解: @EnableFeignClients 3.编写OpenFeign客户端 在cart-service中,定义一个新的接口,编写Feign客户端: @FeignClient("item-service") public interface ItemClient { @GetMapping("/items") List<ItemDTO> queryItemByIds(@RequestParam("ids") Collection<Long> ids); } queryItemByIds这个方法名可以随便取,但@GetMapping("/items") 和 @RequestParam("ids") 要跟 item-service 服务中实际暴露的接口路径和参数名保持一致(直接参考服务提供者的Controller层对应方法对应即可); 一个客户端对应一个服务,可以在ItemClient里面写多个方法。 4.使用 List<ItemDTO> items = itemClient.queryItemByIds(Arrays.asList(1L, 2L, 3L)); Feign 会帮你把 ids=[1,2,3] 序列化成一个 HTTP GET 请求,URL 形如: GET http://item-service/items?ids=1&ids=2&ids=3 连接池 Feign底层发起http请求,依赖于其它的框架。其底层支持的http客户端实现包括: HttpURLConnection:默认实现,不支持连接池 Apache HttpClient :支持连接池 OKHttp:支持连接池 这里用带有连接池的HttpClient 替换默认的 1.引入依赖 <dependency> <groupId>io.github.openfeign</groupId> <artifactId>feign-httpclient</artifactId> </dependency> 2.开启连接池 feign: httpclient: enabled: true # 使用 Apache HttpClient(默认关闭) 重启服务,连接池就生效了。 最佳实践 如果拆分了交易微服务(trade-service),它也需要远程调用item-service中的根据id批量查询商品功能。这个需求与cart-service中是一样的。那么会再次定义ItemClient接口导致重复编程。 思路1:抽取到微服务之外的公共module,需要调用client就引用该module的坐标。 思路2:每个微服务自己抽取一个module,比如item-service,将需要共享的domain实体放在item-dto模块,需要供其他微服务调用的cilent放在item-api模块,自己维护自己的,然后其他微服务引入maven坐标直接使用。 大型项目思路2更清晰、更合理。但这里选择思路1,方便起见。 拆分之后重启报错:Parameter 0 of constructor in com.hmall.cart.service.impl.CartServiceImpl required a bean of type 'com.hmall.api.client.ItemClient' that could not be found. 是因为:Feign Client 没被扫描到,Spring Boot 默认只会在主应用类所在包及其子包里扫描 @FeignClient。 需要额外设置basePackages package com.hmall.cart; @MapperScan("com.hmall.cart.mapper") @EnableFeignClients(basePackages= "com.hmall.api.client") @SpringBootApplication public class CartApplication { public static void main(String[] args) { SpringApplication.run(CartApplication.class, args); } } 网关 在微服务拆分后的联调过程中,经常会遇到以下问题: 不同业务数据分布在各自微服务,需要维护多套地址和端口,调用繁琐且易错; 前端无法直接访问注册中心(如 Nacos),无法实时获取服务列表,导致接口切换不灵活。 此外,单体架构下只需完成一次登录与身份校验,所有业务模块即可共享用户信息;但在微服务架构中: 每个微服务是否都要重复实现登录校验和用户信息获取? 服务间调用时,如何安全、可靠地传递用户身份? 通过引入 API 网关,我们可以在统一入口处解决以上问题:它提供动态路由与负载均衡,前端只需调用一个地址;它与注册中心集成,实时路由调整;它还在网关层集中完成登录鉴权和用户信息透传,下游服务无需重复实现安全逻辑。 快速入门 网关本身也是一个独立的微服务,因此也需要创建一个模块开发功能。大概步骤如下: 创建网关微服务 引入 SpringCloudGateway 、NacosDiscovery依赖 编写启动类 配置网关路由 1.依赖引入: <!-- 网关 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency> <!-- Nacos Discovery --> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <!-- 负载均衡 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-loadbalancer</artifactId> </dependency> 2.配置网关路由 id:给这条路由起个唯一的标识,方便你在日志、监控里看是哪个规则。(最好和服务名一致) uri: lb://xxx:xxx 必须和服务注册时的名字一模一样(比如 Item-service 或全大写 ITEM-SERVICE,取决于你在微服务启动时 spring.application.name 配置) server: port: 8080 spring: application: name: gateway cloud: nacos: server-addr: 192.168.150.101:8848 gateway: routes: - id: item # 路由规则id,自定义,唯一 uri: lb://item-service # 路由的目标服务,lb代表负载均衡,会从注册中心拉取服务列表 predicates: # 路由断言,判断当前请求是否符合当前规则,符合则路由到目标服务 - Path=/items/**,/search/** # 支持多个路径模式,用逗号隔开 - id: cart uri: lb://cart-service predicates: - Path=/carts/** - id: user uri: lb://user-service predicates: - Path=/users/**,/addresses/** - id: trade uri: lb://trade-service predicates: - Path=/orders/** - id: pay uri: lb://pay-service predicates: - Path=/pay-orders/** predicates:路由断言,其实就是匹配条件 After 是某个时间点后的请求 - After=2037-01-20T17:42:47.789-07:00[America/Denver] Before 是某个时间点之前的请求 - Before=2031-04-13T15:14:47.433+08:00[Asia/Shanghai] Path 请求路径必须符合指定规则 - Path=/red/{segment},/blue/** 如果(predicates)符合这些规则,就把请求送到(uri)这里去。 Ant风格路径 用来灵活地匹配文件或请求路径: ?:匹配单个字符(除了 /)。 例如,/user/??/profile 能匹配 /user/ab/profile,但不能匹配 /user/a/profile 或 /user/abc/profile。 *:匹配任意数量的字符(零 个或 多个),但不跨越路径分隔符 /。 例如,/images/*.png 能匹配 /images/a.png、/images/logo.png,却不匹配 /images/icons/logo.png。 **:匹配任意层级的路径(可以跨越多个 /)。 例如,/static/** 能匹配 /static/、/static/css/style.css、/static/js/lib/foo.js,甚至 /static/a/b/c/d。 AntPathMatcher 是 Spring Framework 提供的一个工具类,用来对“Ant 风格”路径模式做匹配 @Component @ConfigurationProperties(prefix = "auth") public class AuthProperties { private List<String> excludePaths; // getter + setter } @Component public class AuthInterceptor implements HandlerInterceptor { private final AntPathMatcher pathMatcher = new AntPathMatcher(); private final List<String> exclude; public AuthInterceptor(AuthProperties props) { this.exclude = props.getExcludePaths(); } @Override public boolean preHandle(HttpServletRequest req, HttpServletResponse res, Object handler) { String path = req.getRequestURI(); // e.g. "/search/books/123" // 检查是否匹配任何一个“放行”模式 for (String pattern : exclude) { if (pathMatcher.match(pattern, path)) { return true; // 放行,不做 auth } } // 否则执行认证逻辑 // ... return false; } } 当然 predicates: - Path=/users/**,/addresses/** 这里不需要手写JAVA逻辑进行路径匹配,因为Gateway自动实现了。但是后面自定义Gateway过滤器的时候就需要AntPathMatcher了! 登录校验 我们需要实现一个网关过滤器,有两种可选: GatewayFilter:路由过滤器,作用范围比较灵活,可以是任意指定的路由Route. GlobalFilter:全局过滤器,作用范围是所有路由,不可配置。 网关需要实现两个功能: JWT 校验:网关会拦截请求,验证 JWT Token 的有效性。如果 Token 无效,返回 401 错误。如果 Token 有效,提取用户 ID,并将其作为请求头的一部分传递给微服务。 传递用户信息:网关将 user-info(用户 ID)传递给微服务。微服务的拦截器会从请求头中获取并保存用户信息到 ThreadLocal,后续代码可以方便地获取。 网关过滤器 - JWT 校验 + 用户信息传递 @Component @RequiredArgsConstructor public class AuthGlobalFilter implements GlobalFilter, Ordered { private final JwtTool jwtTool; private final AuthProperties authProperties; private final AntPathMatcher antPathMatcher = new AntPathMatcher(); @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { // 获取请求 ServerHttpRequest request = exchange.getRequest(); // 判断是否不需要拦截 if (isExclude(request.getPath().toString())) { return chain.filter(exchange); // 跳过不需要拦截的路径 } // 获取 Token String token = request.getHeaders().getFirst("authorization"); // 校验并解析 Token Long userId = null; try { userId = jwtTool.parseToken(token); // 校验 Token 并获取用户 ID } catch (UnauthorizedException e) { ServerHttpResponse response = exchange.getResponse(); response.setRawStatusCode(401); // Token 校验失败,返回 401 return response.setComplete(); } // 将用户信息添加到请求头 ServerWebExchange modifiedExchange = exchange.mutate() .request(builder -> builder.header("user-info", userId.toString())) .build(); // 放行请求,继续执行后续过滤器 return chain.filter(modifiedExchange); } private boolean isExclude(String path) { // 判断路径是否是需要排除的路径(不需要拦截) for (String pattern : authProperties.getExcludePaths()) { if (antPathMatcher.match(pattern, path)) { return true; } } return false; } @Override public int getOrder() { return 0; // 优先级,数字越小优先级越高 } } JWT 校验:通过 jwtTool.parseToken(token) 校验 Token 是否有效。如果有效,就提取用户 ID;如果无效,返回 401 Unauthorized 错误。 传递用户信息:将 user-info(用户 ID)添加到请求头中,然后放行请求。 isExclude 方法:检查当前请求路径是否匹配不需要拦截的路径(如登录、注册等)。 微服务拦截器 - 获取用户信息 为了统一处理微服务中的用户信息提取,我们将拦截器放在 common 模块中。拦截器的作用是从请求头中获取 user-info,并将其保存到 UserContext 中,供后续业务逻辑使用。具体的拦截和校验逻辑由 网关过滤器 处理,而拦截器的职责仅仅是将用户信息存入 ThreadLocal,避免每个微服务都实现相同的逻辑。 1.用户信息拦截器 public class UserInfoInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 1.获取请求头中的用户信息 String userInfo = request.getHeader("user-info"); // 2.判断是否为空 if (StrUtil.isNotBlank(userInfo)) { // 不为空,保存到ThreadLocal UserContext.setUser(Long.valueOf(userInfo)); } // 3.放行 return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { // 移除用户 UserContext.removeUser(); } } 2.配置拦截器 在 common 模块中,我们通过配置类 MvcConfig 来注册拦截器,使其在微服务应用中生效。该配置类实现了 WebMvcConfigurer 接口,并在 addInterceptors 方法中注册 UserInfoInterceptor。 @Configuration @ConditionalOnClass(DispatcherServlet.class) public class MvcConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new UserInfoInterceptor()); } } 3.解决包扫描问题 由于 common 模块与其他微服务模块(如 item、cart)是平级的,common 包无法被微服务自动扫描到。因此,我们需要通过以下方式确保微服务能够加载 common 模块中的拦截器配置。解决方法: 1.在每个微服务的启动类上添加包扫描 @SpringBootApplication( scanBasePackages = {"com.hmall.item", "com.hmall.common"} // 扫描 item 和 common 包 ) public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } } 主包以及common包 2.通过 @Import 引入配置类 @SpringBootApplication @Import(com.hmall.common.config.MvcConfig.class) // 引入 common 模块中的拦截器配置 public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } } 3.将 common 模块做成 Spring Boot 自动配置 1)在 common 模块的 src/main/resources/META-INF/spring.factories 文件中声明: org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.hmall.common.config.MvcConfig 2)在 common 模块里给 MvcConfig 加上 @Configuration @ConditionalOnClass(DispatcherServlet.class) //网关不生效 spring服务生效 public class MvcConfig { … } 3)这样,任何微服务只要依赖了 common 模块,MvcConfig 配置就会自动加载,拦截器会自动生效,无需修改微服务的 @SpringBootApplication 配置。 OpenFeign传递用户 前端发起的请求都会经过网关再到微服务,微服务可以轻松获取登录用户信息。但是,有些业务是比较复杂的,请求到达微服务后还需要调用其它多个微服务,微服务之间的调用无法传递用户信息,因为不在一个上下文(线程)中! 解决思路:让每一个由OpenFeign发起的请求自动携带登录用户信息。要借助Feign中提供的一个拦截器接口:feign.RequestInterceptor public class DefaultFeignConfig { @Bean public RequestInterceptor userInfoRequestInterceptor(){ return new RequestInterceptor() { @Override public void apply(RequestTemplate template) { // 获取登录用户 Long userId = UserContext.getUser(); if(userId == null) { // 如果为空则直接跳过 return; } // 如果不为空则放入请求头中,传递给下游微服务 template.header("user-info", userId.toString()); } }; } } 同时,需要在服务调用者的启动类上添加: @EnableFeignClients( basePackages = "com.hmall.api.client", defaultConfiguration = DefaultFeignConfig.class ) @SpringBootApplication public class PayApplication { 这样 DefaultFeignConfig.class 会对于所有Client类生效 @FeignClient(value = "item-service", configuration = DefaultFeignConfig.class) public interface ItemClient { @GetMapping("/items") List<ItemDTO> queryItemByIds(@RequestParam("ids") Collection<Long> ids); } 这种只对ItemClient生效! 整体流程图 配置管理 微服务共享的配置可以统一交给Nacos保存和管理,在Nacos控制台修改配置后,Nacos会将配置变更推送给相关的微服务,并且无需重启即可生效,实现配置热更新。 配置共享 在nacos控制台的配置管理中添加配置文件 数据库ip:通过${hm.db.host:192.168.150.101}配置了默认值为192.168.150.101,同时允许通过${hm.db.host}来覆盖默认值 配置读取流程: 微服务整合Nacos配置管理的步骤如下: 1)引入依赖: <!--nacos配置管理--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId> </dependency> <!--读取bootstrap文件--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-bootstrap</artifactId> </dependency> 2)新建bootstrap.yaml 在cart-service中的resources目录新建一个bootstrap.yaml文件: 主要给nacos的信息 spring: application: name: cart-service # 服务名称 profiles: active: dev cloud: nacos: server-addr: 192.168.150.101 # nacos地址 config: file-extension: yaml # 文件后缀名 shared-configs: # 共享配置 - dataId: shared-jdbc.yaml # 共享mybatis配置 - dataId: shared-log.yaml # 共享日志配置 - dataId: shared-swagger.yaml # 共享日志配置 3)修改application.yaml server: port: 8082 feign: okhttp: enabled: true # 开启OKHttp连接池支持 hm: swagger: title: 购物车服务接口文档 package: com.hmall.cart.controller db: database: hm-cart 配置热更新 有很多的业务相关参数,将来可能会根据实际情况临时调整,如何不重启服务,直接更改配置文件生效呢? 示例:购物车中的商品上限数量需动态调整。 1)在nacos中添加配置 在nacos中添加一个配置文件,将购物车的上限数量添加到配置中: 文件的dataId格式: [服务名]-[spring.active.profile].[后缀名] 文件名称由三部分组成: 服务名:我们是购物车服务,所以是cart-service spring.active.profile:就是spring boot中的spring.active.profile,可以省略,则所有profile共享该配置(不管local还是dev还是prod) 后缀名:例如yaml 示例:cart-service.yaml hm: cart: maxAmount: 1 # 购物车商品数量上限 2)在微服务中配置 @Data @Component @ConfigurationProperties(prefix = "hm.cart") public class CartProperties { private Integer maxAmount; } 3)下次,只需改nacos中的配置文件 =》发布,即可实现热更新。 动态路由 1.监听Nacos的配置变更 NacosConfigManager可以获取ConfigService 配置信息 String configInfo = nacosConfigManager.getConfigService() 内容是带换行和缩进的 YAML 文本或者 JSON 格式(取决于你的配置文件格式): //多条路由 [ { "id": "user-service", "uri": "lb://USER-SERVICE", "predicates": [ "Path=/user/**" ], "filters": [ "StripPrefix=1" ] }, { "id": "order-service", "uri": "lb://ORDER-SERVICE", "predicates": [ "Path=/order/**" ], "filters": [ "StripPrefix=1", "AddRequestHeader=X-Order-Source,cloud" ] } ] 因为YAML格式解析不方便,故配置文件采用 JSON 格式保存、读取、解析! String getConfigAndSignListener( String dataId, // 配置文件id String group, // 配置组,走默认 long timeoutMs, // 读取配置的超时时间 Listener listener // 监听器 ) throws NacosException; getConfigAndSignListener既可以在第一次读配置文件又可以在后面进行监听 每当 Nacos 上该配置有变更,会触发其内部receiveConfigInfo(...) 方法 2.然后手动把最新的路由更新到路由表中。 RouteDefinitionWriter public interface RouteDefinitionWriter { /** * 更新路由到路由表,如果路由id重复,则会覆盖旧的路由 */ Mono<Void> save(Mono<RouteDefinition> route); /** * 根据路由id删除某个路由 */ Mono<Void> delete(Mono<String> routeId); } @Slf4j @Component @RequiredArgsConstructor public class DynamicRouteLoader { private final RouteDefinitionWriter writer; private final NacosConfigManager nacosConfigManager; // 路由配置文件的id和分组 private final String dataId = "gateway-routes.json"; private final String group = "DEFAULT_GROUP"; // 保存更新过的路由id private final Set<String> routeIds = new HashSet<>(); //order-service ... @PostConstruct public void initRouteConfigListener() throws NacosException { // 1.注册监听器并首次拉取配置 String configInfo = nacosConfigManager.getConfigService() .getConfigAndSignListener(dataId, group, 5000, new Listener() { @Override public Executor getExecutor() { return null; } @Override public void receiveConfigInfo(String configInfo) { updateConfigInfo(configInfo); } }); // 2.首次启动时,更新一次配置 updateConfigInfo(configInfo); } private void updateConfigInfo(String configInfo) { log.debug("监听到路由配置变更,{}", configInfo); // 1.反序列化 List<RouteDefinition> routeDefinitions = JSONUtil.toList(configInfo, RouteDefinition.class); // 2.更新前先清空旧路由 // 2.1.清除旧路由 for (String routeId : routeIds) { writer.delete(Mono.just(routeId)).subscribe(); } routeIds.clear(); // 2.2.判断是否有新的路由要更新 if (CollUtils.isEmpty(routeDefinitions)) { // 无新路由配置,直接结束 return; } // 3.更新路由 routeDefinitions.forEach(routeDefinition -> { // 3.1.更新路由 writer.save(Mono.just(routeDefinition)).subscribe(); // 3.2.记录路由id,方便将来删除 routeIds.add(routeDefinition.getId()); }); } } 可以在项目启动时先更新一次路由,后续随着配置变更通知到监听器,完成路由更新。 服务保护 服务保护方案 1)请求限流 限制或控制接口访问的并发流量,避免服务因流量激增而出现故障。 2)线程隔离 为了避免某个接口故障或压力过大导致整个服务不可用,我们可以限定每个接口可以使用的资源范围,也就是将其“隔离”起来。 3)服务熔断 线程隔离虽然避免了雪崩问题,但故障服务(商品服务)依然会拖慢购物车服务(服务调用方)的接口响应速度。 所以,我们要做两件事情: 编写服务降级逻辑:就是服务调用失败后的处理逻辑,根据业务场景,可以抛出异常,也可以返回友好提示或默认数据。 异常统计和熔断:统计服务提供方的异常比例,当比例过高表明该接口会影响到其它服务,应该拒绝调用该接口,而是直接走降级逻辑。 无非就是停止无意义的等待,直接返回Fallback方案。 Sentinel 介绍和安装 Sentinel是阿里巴巴开源的一款服务保护框架,quick-start | Sentinel 特性 Sentinel (阿里巴巴) Hystrix (网飞) 线程隔离 信号量隔离 线程池隔离 / 信号量隔离 熔断策略 基于慢调用比例或异常比例 基于异常比率 限流 基于 QPS,支持流量整形 有限的支持 Fallback 支持 支持 控制台 开箱即用,可配置规则、查看秒级监控、机器发现等 不完善 配置方式 基于控制台,重启后失效 基于注解或配置文件,永久生效 安装: 1)下载jar包 https://github.com/alibaba/Sentinel/releases 2)将jar包放在任意非中文、不包含特殊字符的目录下,重命名为sentinel-dashboard.jar 然后运行如下命令启动控制台: java -Dserver.port=8090 -Dcsp.sentinel.dashboard.server=localhost:8090 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard.jar 3)访问http://localhost:8090页面,就可以看到sentinel的控制台了 账号和密码,默认都是:sentinel 微服务整合 1)引入依赖 <!--sentinel--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId> </dependency> 2)配置控制台 修改application.yaml文件(可以用共享配置nacos),添加如下: spring: cloud: sentinel: transport: dashboard: localhost:8090 我们的SpringMVC接口是按照Restful风格设计,因此购物车的查询、删除、修改等接口全部都是/carts路径。默认情况下Sentinel会把路径作为簇点资源的名称,无法区分路径相同但请求方式不同的接口。 可以在application.yml中添加下面的配置 然后,重启服务 spring: cloud: sentinel: transport: dashboard: localhost:8090 http-method-specify: true # 开启请求方式前缀 OpenFeign整合Sentinel 默认sentinel只会整合spring mvc中的接口。 修改cart-service模块的application.yml文件,可开启Feign的sentinel功能: feign: sentinel: enabled: true # 开启feign对sentinel的支持 调用的别的服务(/item-service)的接口也会显示在这。 限流: 直接在sentinel控制台->簇点链路->流控 里面设置QPS 线程隔离 阈值类型选 并发线程数 ,代表这个接口所能用的线程数。 Fallback 触发限流或熔断后的请求不一定要直接报错,也可以返回一些默认数据或者友好提示,采用FallbackFactory,可以对远程调用的异常做处理。 业务场景:购物车服务需要同时openFeign调用服务B和商品服务,现在对商务服务做了线程隔离,在高并发的时候,会疯狂抛异常,现在做个fallback让它返回默认值。 步骤一:在hm-api模块中给ItemClient定义降级处理类,实现FallbackFactory: public class ItemClientFallback implements FallbackFactory<ItemClient> { @Override public ItemClient create(Throwable cause) { return new ItemClient() { @Override public List<ItemDTO> queryItemByIds(Collection<Long> ids) { log.error("远程调用ItemClient#queryItemByIds方法出现异常,参数:{}", ids, cause); // 查询购物车允许失败,查询失败,返回空集合 return CollUtils.emptyList(); } @Override public void deductStock(List<OrderDetailDTO> items) { // 库存扣减业务需要触发事务回滚,查询失败,抛出异常 throw new BizIllegalException(cause); } }; } } 步骤二:在hm-api模块中的com.hmall.api.config.DefaultFeignConfig类中将ItemClientFallback注册为一个Bean: @Bean public ItemClientFallback itemClientFallback(){ return new ItemClientFallback(); } 步骤三:在hm-api模块中的ItemClient接口中使用ItemClientFallbackFactory: @FeignClient(value = "item-service",fallbackFactory = ItemClientFallback.class) public interface ItemClient { @GetMapping("/items") List<ItemDTO> queryItemByIds(@RequestParam("ids") Collection<Long> ids); } 重启后,再次测试 熔断器 分布式事务 场景:订单服务依次调用了购物车服务和库存服务,它们各自操作不同的数据库。当清空购物车操作成功、库存扣减失败时,订单服务能捕获到异常,却无法通知已完成操作的购物车服务,导致数据不一致。虽然每个微服务内部都能保证本地事务的 ACID 特性,但跨服务调用缺乏全局协调,无法实现端到端的一致性。 Seeta 要解决这个问题,只需引入一个统一的事务协调者,负责跟每个分支通信,检测状态,并统一决定全局提交或回滚。 在 Seata 中,对应三大角色: TC(Transaction Coordinator)事务协调者 维护全局事务和各分支事务的状态,负责发起全局提交或回滚指令。 TM(Transaction Manager)事务管理器 定义并启动全局事务,最后根据应用调用决定调用提交或回滚。 RM(Resource Manager)资源管理器 嵌入到各微服务中,负责注册分支事务、上报执行结果,并在接到 TC 指令后执行本地提交或回滚。 其中,TM 和 RM 作为客户端依赖,直接集成到业务服务里;TC 则是一个独立部署的微服务,承担全局协调的职责。这样,无论有多少分支参与,都能保证“要么都成功、要么都回滚”的一致性。 部署TC服务 1)准备数据库表 seata-tc.sql 运行初始化脚本 2)准备配置文件 3)Docker部署 seeta-server: image: seataio/seata-server:1.5.2 container_name: seata-server restart: unless-stopped depends_on: - mysql - nacos environment: # 指定 Seata 注册中心和配置中心地址 - SEATA_IP=192.168.0.107 # IDEA 可以访问到的宿主机 IP - SEATA_SERVICE_PORT=17099 - SEATA_CONFIG_TYPE=file # 可视情况再加:SEATA_NACOS_SERVER_ADDR=nacos:8848 networks: - hmall-net ports: - "17099:7099" # TC 服务端口 - "8099:8099" # 服务管理端口(Console) volumes: - ./seata:/seata-server/resources 微服务集成Seata 1)引入依赖 <!--统一配置管理--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId> </dependency> <!--读取bootstrap文件--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-bootstrap</artifactId> </dependency> <!--seata--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-seata</artifactId> </dependency> 2)在nacos上添加一个共享的seata配置,命名为shared-seata.yaml,你在bootstrap中引入该配置即可: seata: registry: # TC服务注册中心的配置,微服务根据这些信息去注册中心获取tc服务地址 type: nacos # 注册中心类型 nacos nacos: server-addr: 192.168.0.107:8848 # 替换为自己的nacos地址 namespace: "" # namespace,默认为空 group: DEFAULT_GROUP # 分组,默认是DEFAULT_GROUP application: seata-server # seata服务名称 username: nacos password: nacos tx-service-group: hmall # 事务组名称 service: vgroup-mapping: # 事务组与tc集群的映射关系 hmall: "default" 这段配置是告诉你的微服务如何去「找到并使用」Seata 的 TC(Transaction Coordinator)服务,以便在本地发起、提交或回滚分布式事务。 XA模式 XA模式的优点是什么? 事务的强一致性,满足ACID原则 常用数据库都支持,实现简单,并且没有代码侵入 XA模式的缺点是什么? 因为一阶段需要锁定数据库资源,等待二阶段结束才释放,性能较差 依赖关系型数据库实现事务 实现方式 1)在Nacos中的共享shared-seata.yaml配置文件中设置: seata: data-source-proxy-mode: XA 2)利用@GlobalTransactional标记分布式事务的入口方法 @GlobalTransactional public Long createOrder(OrderFormDTO orderFormDTO) { ... } 3)子事务中方法前添加@Transactional ,方便回滚 AT模式 简述AT模式与XA模式最大的区别是什么? XA模式一阶段不提交事务,锁定资源;AT模式一阶段直接提交,不锁定资源。 XA模式依赖数据库机制实现回滚;AT模式利用数据快照实现数据回滚。 XA模式强一致;AT模式最终一致(存在短暂不一致) 实现方式: 1)为需要的微服务数据库中创建undo_log表 -- for AT mode you must to init this sql for you business database. the seata server not need it. CREATE TABLE IF NOT EXISTS `undo_log` ( `branch_id` BIGINT NOT NULL COMMENT 'branch transaction id', `xid` VARCHAR(128) NOT NULL COMMENT 'global transaction id', `context` VARCHAR(128) 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 KEY `ux_undo_log` (`xid`, `branch_id`) ) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8mb4 COMMENT ='AT transaction mode undo table'; 2)微服务的配置中设置(其实不设置,默认也是AT模式) seata: data-source-proxy-mode: AT
后端学习
zy123
3月21日
0
41
0
上一页
1
...
10
11
12
下一页