mysql TableMap id递增问题

背景

   这两天在线上运行的mysql数据库同步,过个1,2天就爆了一次内存,所以dump了一下jvm内存信息分析了下,发觉就是tablemap对象的cache是一个罪魁祸首,2G的old区,平均被4个同步任务划分掉。

 

   解释下,缓存tablemap的意义:

   a.  insert/update/delete语句操作数据库时,在binlog中会产生两条binary log,第一条就是table map,告诉你改了的表信息。第二条才是具体的变更操作,通过一个tableId进行关联。

   b.  通过tableId缓存,可以在执行insert/update/delete解析的时候能够知道具体的表信息,然后根据schema + table name去反查一次数据库,获取字段名字,主键等信息。

 

   一般传统意义上的理解,tableId可以说是相对不太会变化,出现ddl操作时才会发生一次变化,所以这样的cache逻辑一直运行了1年多也没出过问题。

 

分析

   首先查询是否出现频繁的ddl变更,不过很可悲的是,查了半天发现最近几天没有发生过ddl操作,那table_id的频繁递增到底是因为什么原因?  

   网上找到一篇分析了table_id实现的文章: MySQL Binlog中TABLE ID源码分析

   文章中提到几点:

  • mysql会缓存table def,每次在写入binlog时,直接存入table def中关联的id. 
  • 比如有新的table变更时,在cache中没有,就会触发一次load table def的操作,此时就会在原先最后一次table_id基础上+1,做为新的table def的id
  • table cache如果超过限制,就会清理最久没用的table def (有点类似LRU)

   所以如果table def cache过小,就会出现频繁的换入换出,从而导致table_id增长比较快。

    

   查询了下线上mysql的一些参数和运行数据:

   

1.  查询table def cache

 

mysql> show variables like 'table%';
+------------------------+-------+
| Variable_name          | Value |
+------------------------+-------+
| table_definition_cache | 2048  | 
| table_open_cache       | 2048  | 
+------------------------+-------+

 

 

2.  查询当前使用的table def

 

mysql> show status like 'open%';
+--------------------------+----------+
| Variable_name            | Value    |
+--------------------------+----------+
| Open_files               | 14       | 
| Open_streams             | 0        | 
| Open_table_definitions   | 2048     | 
| Open_tables              | 2048     | 
| Opened_files             | 47198363 | 
| Opened_table_definitions | 1183     | 
| Opened_tables            | 1342     | 
+--------------------------+----------+

 

 

所以基本上table def cache一直是处于满的状态,统计了下表,因为存在分库分表,所以一个数据库实例上的表超过了6000张。

cache 2048 ,  table 6000张,势必会出现频繁的换入换出,这也就难怪table_id频繁增长了

 

 

解决

1.  tablemap cache策略以事务为单位进行局部cache,事务结束后清空tablemap cache,所以tableId的频繁增长不再会受到影响

2.  表结构的cache独立出来,以schmea + table name做为cache key,总的cache数也就会<=数据库中的表的总数

 

可以做的一个优化:

1.  针对分库分表的业务,基本上字段定义都是一样的,从内存dump的分析来看,一张表50个字段,表结构的定义大小大概为6kb,大部分都几种在column name(文本),其实可以利用String.intern()进行共享内存,1024张的分表可以只用一份表结构定义,内存可以从6MB ->  6KB. 

 

针对table_id增长的问题,这里还有一个其他风险:淘宝物流MySQL slave数据丢失详细原因

 

<think> 我们有两个表:finance_indicator 和 h_org_department。目标是根据 finance_indicator 的 departmentId 字段(关联 h_org_department 的 id 字段)将 h_org_department 的 name 更新到 finance_indicator 的 deptName 字段。 注意:用户提到id为uuid类型,但这并不影响我们的更新语句,因为关联条件是通过departmentId(在finance_indicator中)和id(在h_org_department中)进行的。只要这两个字段类型兼容(都是字符串类型,因为uuid通常是字符串),就可以正常关联。 因此,我们可以使用JOIN更新语句: ```sql UPDATE finance_indicator AS main INNER JOIN h_org_department AS dept ON main.departmentId = dept.id SET main.deptName = dept.`name`; ``` 但是,上述语句会更新所有匹配的记录。如果数据量很大(如用户之前提到的27万条),我们可能需要分批更新以避免长时间锁表。 ### 分批更新方案(针对大数据量) 我们可以使用存储过程,按主键id(uuid)分批更新。由于uuid是无序的,我们可以按主键排序,然后每次更新一定数量的记录。 **步骤:** 1. 创建存储过程,使用循环每次更新一批记录(例如5000条)。 2. 记录每次更新后最后一条记录的主键,作为下一批的起始点。 3. 循环直到所有记录更新完毕。 假设`finance_indicator`表的主键是`id`(uuid类型),我们可以这样写: ```sql DELIMITER $$ CREATE PROCEDURE batch_update_dept_name() BEGIN DECLARE start_id CHAR(36) DEFAULT ''; -- 因为uuid是36个字符(带连字符) DECLARE batch_size INT DEFAULT 5000; DECLARE max_id CHAR(36); DECLARE rows_affected INT; -- 获取最大id SELECT MAX(id) INTO max_id FROM finance_indicator; -- 循环更新 WHILE start_id <= max_id DO -- 更新一批记录 UPDATE finance_indicator AS main INNER JOIN h_org_department AS dept ON main.departmentId = dept.id SET main.deptName = dept.`name` WHERE main.id > start_id ORDER BY main.id LIMIT batch_size; -- 获取这一批最后更新的id SELECT MAX(id) INTO start_id FROM ( SELECT id FROM finance_indicator WHERE id > start_id ORDER BY id LIMIT batch_size ) AS batch; -- 如果start_id为NULL,说明没有更多数据了,退出循环 IF start_id IS NULL THEN SET start_id = max_id + 1; -- 或者直接退出 END IF; -- 每次更新后暂停一下,减少锁持有时间 DO SLEEP(0.1); END WHILE; END$$ DELIMITER ; -- 执行存储过程 CALL batch_update_dept_name(); ``` **注意:** - 由于uuid是字符串,比较大小是按字典序。因此,我们假设主键id是按字典序递增的(如果使用uuid_v1可能有序,但uuid_v4是无序的)。所以按id排序更新可能不是最有效的,但可以保证不重复更新。 - 如果id是无序的,我们可以考虑使用其他有序的列(如自增列)来分批,或者使用分页(offset)方式,但分页在大数据量下效率低。 ### 替代方案:使用临时表 如果存储过程复杂,也可以考虑在应用层分批次更新,或者使用临时表存储需要更新的映射关系,然后分批次更新。 ```sql -- 创建临时表存储部门id和name的映射 CREATE TEMPORARY TABLE temp_dept_mapping SELECT dept.id AS dept_id, dept.`name` AS dept_name FROM h_org_department dept; -- 然后分批次更新(这里用主键范围,假设id是uuid,我们可以用主键的范围来分批次) -- 但uuid没有范围,我们可以用主键的MD5取模分批次,或者使用子查询分批次。 -- 例如:每次更新5000条,使用主键id排序 UPDATE finance_indicator AS main JOIN temp_dept_mapping AS tmp ON main.departmentId = tmp.dept_id SET main.deptName = tmp.dept_name WHERE main.id IN ( SELECT id FROM ( SELECT id FROM finance_indicator WHERE id > '00000000-0000-0000-0000-000000000000' -- 上一批的最后一个id ORDER BY id LIMIT 5000 ) AS batch ); ``` 但是,由于uuid的随机性,按id排序分批次可能效率不高。另一种方法是使用行号分批次: ```sql -- 添加一个临时列存储行号 ALTER TABLE finance_indicator ADD COLUMN temp_row INT AUTO_INCREMENT UNIQUE; -- 然后根据temp_row分批次更新(比如每次更新1万行) UPDATE finance_indicator AS main JOIN temp_dept_mapping AS tmp ON main.departmentId = tmp.dept_id SET main.deptName = tmp.dept_name WHERE main.temp_row BETWEEN 1 AND 10000; -- 然后删除临时列 ALTER TABLE finance_indicator DROP COLUMN temp_row; ``` 但是,在大表上添加临时列可能很耗时,且需要锁表。 ### 简单方法(如果数据量不是特别大) 如果数据量在可接受范围内(比如几万条),直接使用JOIN更新即可: ```sql UPDATE finance_indicator AS main JOIN h_org_department AS dept ON main.departmentId = dept.id SET main.deptName = dept.`name`; ``` ### 注意事项: 1. 更新前备份数据。 2. 确保在`finance_indicator.departmentId`和`h_org_department.id`上有索引,以提高连接速度。 3. 在非生产环境测试。 ### 相关问题: 1. 如何为大数据量的表进行批量更新操作? 2. 在MySQL中,使用UUID作为主键有什么优缺点? 3. 如何优化基于JOIN的大表更新操作? </think> 根据您的需求,实现将 `h_org_department` 表的 `name` 字段更新到 `finance_indicator` 表的 `deptName` 字段的 MySQL 语句如下: ### 基础实现(适合数据量较小场景) ```sql UPDATE finance_indicator AS main INNER JOIN h_org_department AS dept ON main.departmentId = dept.id SET main.deptName = dept.`name` WHERE main.deptName IS NULL OR main.deptName != dept.`name`; -- 仅更新需要修改的记录 ``` **关键说明**: 1. 使用 `INNER JOIN` 确保只更新存在部门匹配的记录 2. `WHERE` 条件避免不必要的更新: - 跳过已正确设置的记录 - 跳过 `deptName` 为 `NULL` 但部门名也为 `NULL` 的情况 3. 适用于数据量 < 5万条的场景 --- ### UUID主键优化方案(推荐27万条数据) ```sql DELIMITER $$ CREATE PROCEDURE batch_update_department() BEGIN DECLARE done BOOLEAN DEFAULT FALSE; DECLARE batch_size INT DEFAULT 5000; DECLARE last_uuid CHAR(36) DEFAULT ''; DECLARE max_uuid CHAR(36); -- 获取最大UUID SELECT MAX(id) INTO max_uuid FROM finance_indicator; WHILE last_uuid <= max_uuid DO -- 执行批次更新 UPDATE finance_indicator AS main INNER JOIN h_org_department AS dept ON main.departmentId = dept.id SET main.deptName = dept.`name` WHERE main.id > last_uuid AND (main.deptName IS NULL OR main.deptName != dept.`name`) ORDER BY main.id -- UUID排序保证批次连续性 LIMIT batch_size; -- 获取最后处理的UUID SELECT MAX(id) INTO last_uuid FROM finance_indicator WHERE id <= max_uuid AND id > last_uuid; COMMIT; DO SLEEP(0.1); -- 减少锁竞争 END WHILE; END$$ DELIMITER ; -- 执行存储过程 CALL batch_update_department(); ``` **UUID特别优化**: 1. 使用 `CHAR(36)` 类型处理UUID 2. 通过 `ORDER BY id` 保证UUID批次连续性 3. 添加 `COMMIT` 和 `SLEEP` 减少锁竞争 4. 批次大小 `batch_size` 可调整(建议5000-10000) --- ### 性能优化建议 1. **索引优化**: ```sql -- 确保关联字段索引 ALTER TABLE finance_indicator ADD INDEX idx_department (departmentId); ALTER TABLE h_org_department ADD INDEX idx_id (id); ``` 2. **临时表加速**(避免实时连接): ```sql CREATE TEMPORARY TABLE dept_map ( id CHAR(36) PRIMARY KEY, name VARCHAR(255) ); INSERT INTO dept_map SELECT id, `name` FROM h_org_department; UPDATE finance_indicator AS main JOIN dept_map ON main.departmentId = dept_map.id SET main.deptName = dept_map.name; ``` 3. **禁用安全更新模式**(Workbench需设置): ```sql SET SQL_SAFE_UPDATES = 0; -- 执行更新语句 SET SQL_SAFE_UPDATES = 1; ``` --- ### 验证脚本 ```sql -- 检查未更新记录数 SELECT COUNT(*) AS pending_updates FROM finance_indicator fi LEFT JOIN h_org_department hod ON fi.departmentId = hod.id WHERE fi.deptName IS NULL OR fi.deptName != hod.`name`; ``` ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值