月牙旁,你轻颦浅笑

向往的生活也许只是一种波普

O ever youthful, O ever weeping.


双十一优化总结(2017)

操作频繁小表宁愿绕点弯路也尽量避免被锁住

  • 对于频繁操作的小表,就算锁住的是其中一行记录,也有可能造成难以预期的锁等待。
  • 对于这样的表可以先将值读到变量里,再对变量进行操作

延迟关联

  • 这种操作经常会在大表的搜索中出现,先通过索引 SELECT 出想要的数据中主表的主键值,然后将这个结果集与主表关联读出想要的数据,这样做延缓了回表时间。因为大表搜索都涉及到分页,如果翻页的时候就回表查会大大降低效率,而通过仅读出主键(不回表)先确定要读出来的值,最后将确定要读的数据的主键跟主表关联得出真正想要的数据。在搜索卡顿的时候把这种方式作为优化方式之一。
  • 导出的时候(因为导出是导出所有数据,而不是像搜索一样只需要某一页的数据,但是又不能一次性将数据读出来否则开销太大)也可以将所有的想要的数据的主表主键存到一张临时表中,然后这张临时表自身的主键就是递增的,一页一页读出数据的时候直接可以通过主键来个范围查找。

延迟关联一定要理解索引的二次查找

有时候为了避免锁住表,可以将需要的数据放入临时表,然后释放原有表,剩下的操作交给临时表。

  • 如果一个事务比较大,锁住表的数据比较多,建议将需要的数据存到临时表中,之后通过临时表做操作。

检查索引失效的地方

  • 注意 WHERE 条件里索引字段的隐式转换导致索引失效,比如 VARCHAR类型跟 INT 类型比较,导致索引失效
  • WHERE 条件里出现函数时导致索引失效,这也算一种特殊的隐式转换导致索引失效。(有的 WHERE 条件里出现函数并没有导致索引失效那是因为 SELECT 的字段都是索引字段,这样直接走了索引)。

存储过程

  • 对于存储过程(不涉及逻辑),主要需要注意的是当出现事务时,事务要尽可能小(锁粒度);避免出现事务嵌套;不符合条件是要回滚;以及要加上异常处理的捕捉(SQLEXCEPTION)。

数据库监控(MySQL5.6)

暂时我们对于数据库的监控主要集中在慢查询和死锁的监控,而这两者的寸文件都比较大(都只存放在单一文件中),所以同时要配合上相应的工具进行分析。

  • 开启 MySQL 的慢查询日志和打印死锁日志的配置
### 慢查询
slow_query_log = ON
slow_query_log_file = 「路径」(可自己配置,也可以用默认配置)

### 死锁
innodb_print_all_deadlocks = ON
死锁日志记录在@@log_error的路径里,SELECT @@log_error 即可得到路径
  • 接下来是有关慢查询日志的分析,首先可以使用 MySQL 自带的工具 mysqldumpslow,使用mysqldumpslow 日志路径 就可以分析出相应数据(需要将 /usr/local/mysql/bin 配置在环境变量中才可以直接使用该命令)

部分截图:

[root@jstumv2ppgydxc ~]# mysqldumpslow /data/mysql/slow.log 

Reading mysql slow query log from /data/mysql/slow.log
Count: 174  Time=122.97s (21397s)  Lock=0.00s (0s)  Rows=0.0 (0), root[root]@[10.24.232.120]
  CALL SP_SALES_DELIVER_ALL(N)

Count: 2  Time=51.98s (103s)  Lock=0.00s (0s)  Rows=0.0 (0), u_wms_yace_wms[u_wms_yace_wms]@[10.24.232.120]
  CALL SP_STOCKOUT_ORDER_NEW_PRINT_BATCH(N,N,N,'S','S',N)

Count: 16  Time=43.49s (695s)  Lock=0.00s (0s)  Rows=0.0 (0), root[root]@[10.24.232.120]
  CALL SP_STOCKOUT_SALES_EXAMINE_YACE()

Count: 3  Time=42.91s (128s)  Lock=0.00s (0s)  Rows=0.0 (0), u_wms_yace_wms[u_wms_yace_wms]@[10.24.232.120]
  CALL SP_STOCKOUT_ORDER_CLEAR_POSITION('S')

同样可以使用 percona 发布的 percona toolkit 工具集中的 pt-query-digest,通过 pt-query-digest 日志路径也可以直接分析出

部分截图:

# Current date: Sat Oct 21 22:20:30 2017
# Hostname: jstumv2ppgydxc
# Files: /data/mysql/slow.log
# Overall: 3.17k total, 24 unique, 0.02 QPS, 0.27x concurrency ___________
# Time range: 2017-10-18 18:05:49 to 2017-10-20 18:14:29
# Attribute          total     min     max     avg     95%  stddev  median
# ============     ======= ======= ======= ======= ======= ======= =======
# Exec time         47165s    40us  15395s     15s     32s    263s      6s
# Lock time           19ms       0   663us     5us    47us    21us       0
# Rows sent         26.47k       0   2.93k    8.54       0  137.13       0
# Rows examine     127.95M       0   8.61M  41.29k   7.31k 323.35k       0
# Query size         6.48M      20   3.78k   2.09k   3.69k   1.55k  874.75

# Profile
# Rank Query ID           Response time    Calls R/Call   V/M   Item
# ==== ================== ================ ===== ======== ===== ==========
#    1 0x9E8F5EE69306720A 21397.2866 45.4%   174 122.9729 10... CALL SP_SALES_DELIVER_ALL
#    2 0x17294978B68E5993 10369.9098 22.0%  1462   7.0930  0.66 CREATE TABLE tmp_sales_trade
#    3 0x6185E750B8987C9F  8895.4684 18.9%  1253   7.0993  0.69 CREATE TABLE tmp_sales_order
#    4 0xD8639FBEF5336A31  1869.5757  4.0%    78  23.9689  6.47 CALL SP_SALES_TRADE_CHECK
#    5 0x8221D835D186C3C0  1459.2881  3.1%    39  37.4176 17.71 CALL SP_STOCKOUT_ORDER_ALLOCATE_POSITION
#    6 0x1DFDB7C755EF165F   742.0276  1.6%    49  15.1434 24.68 CALL SP_STOCKOUT_SALES_CONSIGN_YACE
#    7 0x15466970CA5A18BE   695.8046  1.5%    16  43.4878  3.13 CALL SP_STOCKOUT_SALES_EXAMINE_YACE
#    8 0x3BB79BA58D9F55C8   421.7239  0.9%    16  26.3577  3.05 CALL SP_STOCK_SALES_BATCH_WEIGHT_YACE
#    9 0x616597AC774D30AC   411.8184  0.9%    15  27.4546 13.40 CALL SP_STOCKOUT_ORDER_WORKER_REGISTER_YACE
#   10 0x3603F7E663B62543   326.7144  0.7%    13  25.1319  1.62 CALL SP_SALES_TRADE_CHECK_YACE
#   11 0xC52C19BBB184BA0B   150.0959  0.3%    19   7.8998  2.15 CALL SP_STOCKOUT_ORDER_SALES_PRINT_QUERY
# MISC 0xMISC               425.5899  0.9%    39  10.9126   0.0 <13 ITEMS>
  • 对于死锁信息 MySQL 只能直接存储过相应文件中,并没有合适的工具分析,而且开启后性能也有一定的影响,所以一般使用 pt-deadlock-logger 来分析,在监控的时候打开,将错误信息打印在屏幕上,并且可以在选择打印的同时将信息保存到指定的表里一份,不监控的时候关闭即可,比较方便。 命令 pt-deadlock-logger --create-dest-table --dest D=test,t=deadlocks u=pt,p=pt,P=3306,h=114.55.33.223,这时候如果出现死锁情况会直接打印在界面上并记录到 deadlocks 表中一份,下面是 test 库下,deadlocks 的表内容:
[pt@localhost][test]> select *from deadlocks limit 1\G
*************************** 1. row ***************************
   server: 114.55.33.223
       ts: 2017-10-20 10:48:49
   thread: 931047
   txn_id: 0
 txn_time: 0
     user: root
 hostname: 
       ip: 10.24.232.120
       db: d_wms_yace_wms
      tbl: stock_spec
      idx: PRIMARY
lock_type: RECORD
lock_mode: X
wait_hold: w
   victim: 0
    query: UPDATE stockout_order_detail sod ,stock_spec ss SET ss.allocating_num=ss.allocating_num-sod.num WHERE sod.stockout_id=P_StockoutId AND sod.spec_id=ss.spec_id AND ss.warehouse_id=P_WarehouseId AND owner_id=V_OwnerId
1 row in set (0.00 sec)

对于监控一定要在性能允许的基础上展开,如果性能消耗大则在测试环境开启就好。


这里稍微提及下 MySQL DML 使用方式上的注意点,以及导致发生锁的几种方式,不会详细的介绍各种锁类型的锁方式。

首先说一个需要非常注意的操作:

INSERT INTO SELECT

  • 对于 INSERT INTO SELECT 这个操作要尤其注意,因为它不仅仅锁 INTO 的表,还会锁 SELECT FROM 后的表(两个表产生的锁是记录锁还是表锁视情况而定)。
  • 条件允许的情况下,尽量通过 INSERT VALUES 来进行替代

你所忽视的互相锁住的注意点

都知道死锁的真正原因是事务互相持有的锁导致都无法释放,所以产生死锁需要释放一个重启一个事务来让另一个事务先执行完。最常遇到的死锁情况无疑是「当两个事务同时操作几条记录时,因为顺序的不一致产生的互相持有的锁(A-B-C,C-B-A)」。但有时候你却意识不到你导致了互相持有操作。

  • 「AB-BA 的锁」 对于操作同一个表,一个事务用了 GROUP BY,另一个什么都没用,当同时触发的时候就可能导致死锁,因为 GROUP BY操作会默认 ORDER BY 一下。

  • 「主键和二级索引引发的锁」 对于通过索引来锁住记录的情况,当一个事务 T1 用唯一索引锁住了 A 表中的记录 B,另一个事务 T2 通过主键同样锁住了A表中的记录B;这时如果 T2 又想通过唯一索引持有 A 表中 B 记录,T1 想通过主键持有 A 表中的 B 记录时就会产生死锁,所以对索引的使用也需要进行分析。

  • 「两个S-lock互相锁住」

    x 表中字段 a 是唯一索引

事务一 (T1) 事务二 (T2) 事务三 (T3)
begin; begin; begin;
DELETE FROM x WHERE a = 1;    
  INSERT INTO x (a) VALUES (1); – 等待锁 INSERT INTO x (a) VALUES (1); – 等待锁
commit;    

T1 commit时,T2, T3 的 INSERT 操作都获得 S-lock,然后都想对 a = 1 的记录加上 x-lock,却被互相的 x-lock 锁住

  • 「S锁到X锁不能直接继承过去」
事务一 (T1) 事务二 (T2)
begin; begin;
SELECT * FROM x WHERE a = 1 LOCK IN SHARE MODE; – 持有了s锁  
  DELETE FROM x WHERE a = 1; – 欲获得x锁,暂时等待
DELETE FROM x WHERE a = 1; – 也要请求x锁,但是不能从s锁直接继承过来,所以也等待,需要排在 T2 后面  
commit;  

这时候就形成了T2 -> T1, T1 -> T2 的情况,造成死锁。