线上故障之-数据库问题

  • 数据库问题概述
    • 索引:
    • 高可用
    • 一些需要注意的事项
    • 处理问题的一些技巧
    • 一般大厂数据库规约
      • 一、基础规范
      • 二、命名规范
      • 三、表设计规范
      • 四、字段设计规范
      • 五、索引设计规范
    • 死锁
      • 场景:
      • 问题场景:
      • 基础:
      • 表结构:
      • 分析:
      • 解决方式:
    • 构建+合并代码+jar包问题+环境隔离
    • 连接池
      • 问题分析:
      • 解决办法:
    • 容易忽略的点
      • 那些年踩过的坑(技术篇)
    • 开发规范TOP10-晋升考核内容指导篇
      • 1. 禁止在大循环中调用Service,SQL,Redis
        • 1.1. Service的循环调用
        • 1.2. SQL的循环调用
        • 1.3. Redis的循环调用
      • 2. 禁止3B:Big Transaction,Big SQL,Big Batch
        • 2.1. Big Transaction
        • 2.2. Big SQL
        • 2.3. Big Batch
      • 3. 禁止全表扫描SQL和select *,update所 有列
        • 3.1. 全表扫描
        • 3.2. select *
        • 3.3. update所有列
      • 4. 禁止Worker扫描业务表
        • 4.1. 带来的问题
        • 4.2. 正确的做法
        • 4.3. 举例
        • 4.4. 数据转历史
      • 5. 禁止没有边界限制的创建大量对象、Net IO
        • 5.1. 创建大量对象
        • 5.2. Net IO
      • 6. 禁止输入参数不做校验及服务直接抛出异 常
        • 6.1. 参数校验
        • 6.2. 服务异常
      • 7. 禁止服务及UI按钮不做防重入
        • 7.1. 服务防重
        • 7.2. UI防重入
      • 8. 禁止一次性查询或导出全部数据,禁止单 次操作数据超过5000条
      • 9. 禁止线上服务不接入方法性能监控和存活 监控
        • 9.1. 服务监控设计
      • 10. 禁止服务产生底层依赖上层,强依赖 弱、循环依赖
        • 10.1. 依赖设计

数据库问题概述

索引:

索引分类:主键索引,普通索引,复合索引,唯一索引
技术名词:回表,最左匹配,索引覆盖,索引下推
explain

id:select查询的序列号,包含一组数字,表示查询中执行select子句或操作表的顺序
select_type:
SIMPLE:简单的 select 查询,查询中不包含子查询或者UNION
PRIMARY:查询中若包含任何复杂的子部分,最外层查询则被标记为Primary
DERIVED:在FROM列表中包含的子查询被标记为DERIVED(衍生),MySQL会递归执行这些子查询, 把结果放在临时表
里。
SUBQUERY:在SELECT或WHERE列表中包含了子查询
DEPENDENT SUBQUERY:在SELECT或WHERE列表中包含了子查询,子查询基于外层
UNCACHEABLE SUBQUREY:无法被缓存的子查询
UNION:若第二个SELECT出现在UNION之后,则被标记为UNION;若UNION包含在FROM子句的子查询中,外层SELECT将
被标记为:DERIVED
UNION RESULT:从UNION表获取结果的SELECT
table:显示这一行的数据是关于哪张表的
type:
system:表只有一行记录(等于系统表),这是const类型的特列,平时不会出现,这个也可以忽略不计
const:表示通过索引一次就找到了,const用于比较primary key或者unique索引。因为只匹配一行数据,所以很快
如将主键置于where列表中,MySQL就能将该查询转换为一个常量
eq_ref:唯一性索引扫描,对于每个索引键,表中只有一条记录与之匹配。常见于主键或唯一索引扫描
ref:非唯一性索引扫描,返回匹配某个单独值的所有行.本质上也是一种索引访问,它返回所有匹配某个单独值的
行,然而,它可能会找到多个符合条件的行,所以他应该属于查找和扫描的混合体
range:只检索给定范围的行,使用一个索引来选择行。key 列显示使用了哪个索引一般就是在你的where语句中出
现了between、<、>、in等的查询这种范围扫描索引扫描比全表扫描要好,因为它只需要开始于索引的某一点,而结束
语另一点,不用扫描全部索引。
index:Full Index Scan,index与ALL区别为index类型只遍历索引树。这通常比ALL快,因为索引文件通常比数据
文件小。(也就是说虽然all和Index都是读全表,但index是从索引中读取的,而all是从硬盘中读的)
all:Full Table Scan,将遍历全表以找到匹配的行
index_merge:在查询过程中需要多个索引组合使用,通常出现在有 or 的关键字的sql中
ref_or_null:对于某个字段既需要关联条件,也需要null值得情况下。查询优化器会选择用ref_or_null连接查
询。
index_subquery:利用索引来关联子查询,不再全表扫描。
unique_subquery :该联接类型类似于index_subquery。 子查询中的唯一索引
备注:type显示的是访问类型,是较为重要的一个指标,结果值从最好到最坏依次是:
system > const > eq_ref > ref > fulltext > ref_or_null > index_merge > unique_subquery >
index_subquery > range(尽量保证) > index > ALL
system>const>eq_ref>ref>range>index>ALL
一般来说,得保证查询至少达到range级别,最好能达到ref。
possible_keys:显示可能应用在这张表中的索引,一个或多个。查询涉及到的字段上若存在索引,则该索引将被列
出,但不一定被查询实际使用
key:实际使用的索引。如果为NULL,则没有使用索引,查询中若使用了覆盖索引,则该索引和查询的select字段重叠
key_len:表示索引中使用的字节数,可通过该列计算查询中使用的索引的长度。 key_len字段能够帮你检查是否充分
的利用上了索引
ref:显示索引的哪一列被使用了,如果可能的话,是一个常数。哪些列或常量被用于查找索引列上的值
rows:rows列显示MySQL认为它执行查询时必须检查的行数。越少越好
Extra:包含不适合在其他列中显示但十分重要的额外信息
Using filesort:说明mysql会对数据使用一个外部的索引排序,而不是按照表内的索引顺序进行读取。MySQL中
无法利用索引完成的排序操作称为“文件排序”
Using temporary:使了用临时表保存中间结果,MySQL在对查询结果排序时使用临时表。常见于排序 order by 和
分组查询 group by。
USING index:表示相应的select操作中使用了覆盖索引(Covering Index),避免访问了表的数据行,效率不错!
如果同时出现using where,表明索引被用来执行索引键值的查找;如果没有同时出现using where,表明索引只是用来
读取数据而非利用索引执行查找。
Using where:表明使用了where过滤
using join buffer:使用了连接缓存:
impossible where:where子句的值总是false,不能用来获取任何元组
select tables optimized away:在没有GROUPBY子句的情况下,基于索引优化MIN/MAX操作或者对于MyISAM存储引
擎优化COUNT(*)操作,不必等到执行阶段再进行计算,查询执行计划生成的阶段即完成优化。 

索引失效:

全值匹配我最爱
最佳左前缀法则
不在索引列上做任何操作(计算、函数、(自动or手动)类型转换),会导致索引失效而转向全表扫描
存储引擎不能使用索引中范围条件右边的列
尽量使用覆盖索引(只访问索引的查询(索引列和查询列一致)),减少select *
mysql 在使用不等于(!= 或者<>)的时候无法使用索引会导致全表扫描
is not null 也无法使用索引,但是is null是可以使用索引的
like以通配符开头('%abc...')mysql索引失效会变成全表扫描的操作
隐式类型转换索引失效
少用or,用它来连接时会索引失效

高可用

主从,分库分表

一些需要注意的事项

1-count(*),count(1),count(主键id),count(字段),到底用谁?

count(可空字段)

扫描全表,读到server层,判断字段可空,拿出该字段所有值,判断每一个值是否为空,不为空则累加

count(非空字段)与count(主键 id)

扫描全表,读到server层,判断字段不可空,按行累加。

count(1)

扫描全表,但不取值,server层收到的每一行都是1,判断不可能是null,按值累加。

注意:count(1)执行速度比count(主键 id)快的原因:从引擎返回 id 会涉及到解析数据行,以及拷贝字段值的操作。

count(*)

MySQL 执行count()在优化器做了专门优化。因为count()返回的行一定不是空。扫描全表,但是不取值,按行累加。

看到这里,你会说优化器就不能自己判断一下吗,主键 id 肯定是非空的,为什么不能按照 count() 来处理,多么简单的优化。当然 MySQL 专门针对这个语句进行优化也不是不可以。但是这种需要专门优化的情况太多了,而且 MySQL 已经优化过 count() 了,你直接使用这种语句就可以了。

性能对比结论

count(可空字段) < count(非空字段) = count(主键 id) < count(1) ≈ count(*)

2-普通索引和主键索引到底有没有区别?
普通索引找到位置后,还会往后查找一次,主键索引不会(因为唯一)。
查询来说差不多的,跨页的话会有点区别。
更新的话,唯一索引效率低一些,要判断唯一,总体来说普通更高些

redo log 主要节省的是随机写磁盘的IO(顺序写)
change buffer主要节省随机读磁盘的IO消耗

处理问题的一些技巧

1-慢sql定位:开启慢日志
2-大事务处理:SELECT * FROM information_schema.INNODB_TRX
3-降低死锁概率:控制并发度
比如场景:
1.用户A余额支付金额给商家B:update t set money = money-100 where user =‘A’;
2.商家B余额增加:update t set money = money+100 where user =‘B’;
3.A生成订单日志:insert …

如何设计三条语句的顺序:3->1>2;
降低了锁的概率,商家会有多个更新,放最后。

一般大厂数据库规约

一、基础规范

(1)必须使用InnoDB存储引擎
解读:支持事务、行级锁、并发性能更好、CPU及内存缓存页优化使得资源利用率更高
(2)必须使用UTF8字符集
解读:万国码,无需转码,无乱码风险,节省空间
(3)数据表、数据字段必须加入中文注释
解读:N年后谁知道这个r1,r2,r3字段是干嘛的
(4)禁止使用存储过程、视图、触发器、Event
解读:高并发大数据的互联网业务,架构设计思路是“解放数据库CPU,将计算转移到服务层”,并发量大的情况下,
这些功能很可能将数据库拖死,业务逻辑放到服务层具备更好的扩展性,能够轻易实现“增机器就加性能”。数据库擅
长存储与索引,CPU计算还是上移吧
(5)禁止存储大文件或者大照片
解读:为何要让数据库做它不擅长的事情?大文件和照片存储在文件系统,数据库里存URI多好

二、命名规范

(6)只允许使用内网域名,而不是ip连接数据库
(7)线上环境、开发环境、测试环境数据库内网域名遵循命名规范

业务名称:xxx
线上环境:my10000m.mysql.jddb.com
开发环境:yf10000m.mysql.jddb.com
测试环境:test10000m.mysql.jddb.com
从库在名称后加-s标识,备库在名称后加-ss标识
线上从库:my10000sa.mysql.jddb.com
(8)库名、表名、字段名:小写,下划线风格,不超过32个字符,必须见名知意,禁止拼音英文混用
(9)表名t_xxx,非唯一索引名idx_xxx,唯一索引名uniq_xxx

三、表设计规范

(10)单实例表数目必须小于500
(11)单表列数目必须小于30
(12)表必须有主键,例如自增主键

解读:
a)主键递增,数据行写入可以提高插入性能,可以避免page分裂,减少表碎片提升空间和内存的使用
b)主键要选择较短的数据类型, Innodb引擎普通索引都会保存主键的值,较短的数据类型可以有效的减少索引的磁盘空间,提高索引的缓存效率
c) 无主键的表删除,在row模式的主从架构,会导致备库夯住
(13)禁止使用外键,如果有外键完整性约束,需要应用程序控制
解读:外键会导致表与表之间耦合,update与delete操作都会涉及相关联的表,十分影响sql 的性能,甚至会造成死 锁。高并发情况下容易造成数据库性能,大数据高并发业务场景数据库使用以性能优先

四、字段设计规范

(14)必须把字段定义为NOT NULL并且提供默认值
解读:
a)null的列使索引/索引统计/值比较都更加复杂,对MySQL来说更难优化
b)null 这种类型MySQL内部需要进行特殊处理,增加数据库处理记录的复杂性;同等条件下,表中有较多空字段的时
候,数据库的处理性能会降低很多
c)null值需要更多的存储空,无论是表还是索引中每行中的null的列都需要额外的空间来标识
d)对null 的处理时候,只能采用is null或is not null,而不能采用=、in、<、<>、!=、not in这些操作符号。如:
where name!=’shenjian’,如果存在name为null值的记录,查询结果就不会包含name为null值的记录
(15)禁止使用TEXT、BLOB类型
解读:会浪费更多的磁盘和内存空间,非必要的大量的大字段查询会淘汰掉热数据,导致内存命中率急剧降低,影响数 据库性能
(16)禁止使用小数存储货币
解读:使用整数吧,小数容易导致钱对不上
(17)必须使用varchar(20)存储手机号
解读:
a)涉及到区号或者国家代号,可能出现±()
b)手机号会去做数学运算么?
c)varchar可以支持模糊查询,例如:like“138%”
(18)禁止使用ENUM,可使用TINYINT代替
(19) status禁止这么用,你要写成有逻辑意义得字段,比如用户状态,userStatus,is_delete ,不要存成int类型,而是用TINYINT
解读:
a)增加新的ENUM值要做DDL操作
b)ENUM的内部实际存储就是整数,你以为自己定义的是字符串?

五、索引设计规范

(19)单表索引建议控制在5个以内
(20)单索引字段数不允许超过5个

解读:字段超过5个时,实际已经起不到有效过滤数据的作用了
(21)禁止在更新十分频繁、区分度不高的属性上建立索引
解读:
a)更新会变更B+树,更新频繁的字段建立索引会大大降低数据库性能
b)“性别”这种区分度不大的属性,建立索引是没有什么意义的,不能有效过滤数据,性能与全表扫描类似
(22)建立组合索引,必须把区分度高的字段放在前面
解读:能够更加有效的过滤数据
六、SQL使用规范
*(23)禁止使用SELECT ,只获取必要的字段,需要显示说明列属性
解读:
a)读取不需要的列会增加CPU、IO、NET消耗
b)不能有效的利用覆盖索引
c)使用SELECT *容易在增加或者删除字段后出现程序BUG
(24)禁止使用INSERT INTO t_xxx VALUES(xxx),必须显示指定插入的列属性
解读:容易在增加或者删除字段后出现程序BUG
(25)禁止使用属性隐式转换
解读:SELECT uid FROM t_user WHERE phone=13800000000 会导致全表扫描,而不能命中phone索引,猜猜为什么?
(这个线上问题不止出现过一次)
(26)禁止在WHERE条件的属性上使用函数或者表达式
解读:SELECT uid FROM t_user WHERE from_unixtime(day)>=‘2017-01-15’ 会导致全表扫描
正确的写法是:SELECT uid FROM t_user WHERE day>= unix_timestamp(‘2017-01-15 00:00:00’)
(27)禁止负向查询,以及%开头的模糊查询
解读:
a)负向查询条件:NOT、!=、<>、!<、!>、NOT IN、NOT LIKE等,会导致全表扫描
b)%开头的模糊查询,会导致全表扫描
(28)禁止大表使用JOIN查询,禁止大表使用子查询
解读:会产生临时表,消耗较多内存与CPU,极大影响数据库性能
(29)禁止使用OR条件,必须改为IN查询解读:旧版本Mysql的OR查询是不能命中索引的,即使能命中索引,为何要让数据库耗费更多的CPU帮助实施查询优化 呢?
(30)应用程序必须捕获SQL异常,并有相应处理
(31)同表的增删字段、索引合并一条DDL语句执行,提高执行效率,减少与数据库的交互。
总结:大数据量高并发的互联网业务,极大影响数据库性能的都不让用,不让用哟。

死锁

场景:

运单的上游生产是以子单的形式下发到运单,然后进行自营补全,自营补全的时候会查订单中间件的信息,里面包含父订单,如果有父订单的触发父子单任务。
父子单任务里面会根据子订单查询订单中间件接口,查出一个list<父单,子单> 存储到数据库。

问题场景:

存在一种场景,如果一个子单在很短时间内下发会触发多个父子单任务,这个时候会出现同时插入多条重复数据的问题,造成Insert场景下的死锁。
另外问了下订单中间件的人,他们说每次查询到的父子单的list可能不是一个顺序,也就是说会存在事务1插入1、2两条数据,事务2会反着插入2、1

基础:

死锁分析首先需要看懂MySql死锁日志 http://keithlan.github.io/2017/06/05/innodb_l
ocks_1/ 这个博客的一系列文章讲解的很清楚。

表结构:

CREATE TABLE order_parent_child_0 (
ID bigint(20) NOT NULL COMMENT ‘主键编码’,
PARENT_ORDER_ID varchar(50) DEFAULT NULL COMMENT ‘主订单号’,
CHILD_ORDER_ID varchar(50) DEFAULT NULL COMMENT ‘子订单号’,
CREATE_TIME timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT ‘表分区’,
FIRST_TIME timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP
COMMENT ‘创建时间’,
UPDATE_TIME timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP
COMMENT ‘更新时间’,
UPDATE_USER varchar(50) DEFAULT NULL COMMENT ‘更新人’,
CREATE_USER varchar(50) DEFAULT NULL COMMENT ‘创建人’,
SYS_VERSION tinyint(4) DEFAULT ‘0’ COMMENT ‘系统版本号’,
YN int(11) DEFAULT NULL COMMENT ‘是否删除’,
TS timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE
CURRENT_TIMESTAMP COMMENT ‘时间戳’,
PRIMARY KEY ( ID , CREATE_TIME ),
UNIQUE KEY uniq_parent_child ( PARENT_ORDER_ID , CHILD_ORDER_ID , CREATE_TIME ) USING
BTREE,
KEY idx_parent_order_id ( PARENT_ORDER_ID )
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT=‘父子单表’;

分析:

原因是存在两个事务 同时插入相同的数据,且数据的插入顺序是相反的。
具体点:
参考上面说的问题场景,每个子单都会触发父子单任务补全。
存在两个同订单号的子单号:父单号:6, 一个子单号为1,另一个子单号为2,那么就会存在两个事务:
事务一:
Insert into (parent_order_id, child_order_id) values(6,1);
Insert into (parent_order_id, child_order_id) values(6,2);
Commit;
事务二:
Insert into (parent_order_id, child_order_id) values(6,2);
Insert into (parent_order_id, child_order_id) values(6,1);
Commit;
此时会导致死锁。

解决方式:

1、 在插入之前以父订单号为Key做Redis分布式锁,缓存时间为5s,获取到锁的再到数据库中根据父单号查询下有没有数据,没有数据进行插入,获取不到锁的直接丢弃。(如果插入异常,还有worker自动跑)《线上版本》

构建+合并代码+jar包问题+环境隔离

git提交注释规范:
commitType(JIRA号):description

一般配合辅助检测工具 Checkstyle

连接池

所有用户都不能获取数据,线上服务不可用。

问题分析:

查看线上日志,报timeout waiting for connection from pool,定位到问题在自实现的
httpclient连接池中,连接池大小设置为50,
当大量并发访问服务时连接池资源消耗殆尽,导致获取连接超时。

解决办法:

由于该项目的pv 及并发量并不太多,两台4核8G服务器资源完全能胜任,重构代码使用
httpclient自带连接池,不限制
连接池大小,使用空间资源换取时间性能。
问题代码:

HttpClient httpClient = HttpPool.getHttpClient();HttpResponse httpResponse = httpClient.execute(get);
public class HttpPool {private static final Log log = LogFactory.getLog(HttpPool.class);private static ThreadSafeClientConnManager cm = null;static {SchemeRegistry schemeRegistry = new SchemeRegistry();schemeRegistry.register(new Scheme("http", 80, PlainSocketFactory.getSocketFactory()));cm = new ThreadSafeClientConnManager(schemeRegistry);try {int maxTotal = 100;cm.setMaxTotal(maxTotal);} catch (NumberFormatException e) {log.error("Key[httpclient.max_total] Not Found in systemConfig.properties", e);}try {int defaultMaxConnection = 50;cm.setDefaultMaxPerRoute(defaultMaxConnection);} catch (NumberFormatException e) {log.error("Key[httpclient.default_max_connection] Not Found in systemConfig.properties", e); }}public static HttpClient getHttpClient() {HttpParams params = new BasicHttpParams();params.setParameter(CoreProtocolPNames.PROTOCOL_VERSION,HttpVersion.HTTP_1_1);params.setParameter(CoreConnectionPNames.CONNECTION_TIMEOUT, 3000); // 3000ms return new DefaultHttpClient(cm, params);}public static void release() {if (cm != null) {cm.shutdown();}}
}


修改后的代码:

    HttpClient httpClient = getHttpClient(get);private HttpClient getHttpClient(HttpGet get) {CloseableHttpClient httpClient = HttpClients.createDefault();RequestConfig requestConfig = RequestConfig.custom().setSocketTimeout(15 * 1000).setConnectTimeout(15 * 1000).setConnectionRequestTimeout(15 * 1000).build();get.setConfig(requestConfig);return httpClient;}

容易忽略的点

那些年踩过的坑(技术篇)

高并发下的SimpleDateFormat
第一坑:

public class DateUtilTest {private static final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");@Testpublic void dateTest(){ExecutorService executorService = Executors.newFixedThreadPool(500);for (int i = 0; i < 500; i++) {executorService.execute(new Runnable() {@Overridepublic void run() {for (int j = 0; j < 1000000; j++) {try {DATE_FORMAT.parse("2016-12-12 12:12:12");} catch (ParseException e) {e.printStackTrace();}}}});}}
}

代码运行结果: Exception in thread “pool-1-thread-2” java.lang.NumberFormatException:
For input string: “”
at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
at java.lang.Long.parseLong(Long.java:453)
at java.lang.Long.parseLong(Long.java:483)
at java.text.DigitList.getLong(DigitList.java:194)
原因:日期格式化的类是非同步的,建议为每一个线程创建独立的格式化实例,如果多个线程并发访问同一个格式化实例,就必须在外部添加同步机制。

正确写法:

public class DateUtilTest {@Testpublic void dateTest1(){ExecutorService executorService = Executors.newFixedThreadPool(500);for (int i = 0; i < 500; i++) {executorService.execute(new Runnable() {@Overridepublic void run() {for (int j = 0; j < 1000000; j++) {try {new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse("2016-12-12 12:12:12"); } catch (ParseException e) {e.printStackTrace();}}}});}}
}

小结
高并发所引发的问题往往很难解决,因为它无法稳定重现,如比本文中的问题,如果不是在高并发的情况下,可能你的程序运行半年甚至更久,都不一定能出现几次解析失败的异常。就算偶尔出现,你也可能任务是日期格式错误,从而忽略掉它本身的机制。详情请看https://blog.csdn.net/farrell_zeng/article/details/54408616

<低级错误>
构建HashMap<Integer,Object>时,key类型为Integer;
但是再获取时get(Long)时 key使用的是Long类型,导致数据获取为空。
问题原因:
HashMap中key是否相等的判断依据:hashCode和equals方法,哈希值相同且equals返回true才认为同一个key。
32bit以下的数值Integer和Long的hashCode是相同的;但是由于类型不同,equals方法返回false。
也就是new Integer(888) 与new Long(888)是两个不同的key;
解决方法:

  1. 统一使用一种类型Integer或Long;
  2. 将数值转换成String类型作为key,彻底避免。
    判定Integer对象相等尽量不要使用==,建议使用equals或者int值比较
    分析:虽然JDK内部会存储-128 – 127的Integer缓存对象,但是通过new Integer()创建的对象与缓存的对象不是一个。
    使用equals方法或者int值比较
    字符串判断相等要用Objects.equals();

开发规范TOP10-晋升考核内容指导篇

该规范 曾作为很多团队职级晋升技术参考标准之一。
适用范围:针对生产系统,不含监控与报表类应用

1. 禁止在大循环中调用Service,SQL,Redis

1.1. Service的循环调用

服务本身:提供批量接口;
调用方:尽可能的以批量方式调用取代逐条调用,减少系统开销;
案例:
修改前的代码:

 /*** 获取容器中的订单列表和具体订单的商品明细列表 **/public List<ObCheckMP> getListCheckMP(List<ObCheckC> listCheckC, ObCheckQuery checkQuery) {try {//根据商品明细提取容器内的全部订单主档明细 List<ObCheckM> listCheckM = new ArrayList<ObCheckM>();Map<String, ObCheckP> map = new HashMap<String, ObCheckP>();for (ObCheckP cp : listCheckPAll) {if (!map.containsKey(cp.getOutboundNo())) {map.put(cp.getOutboundNo(), cp);ObCheckQuery tq = new ObCheckQuery(cp);
//订的复核类型 tq.setCheckType(checkType);
//在循环内查询数据库 listCheckM.add(checkMManager.queryBeanByOutboundNo(tq));}}if (null == listCheckM || listCheckM.size() == 0) {throw new UserMessage("此容器中不存未复核的订单!");}List<ObCheckMP> listCheckMP = new ArrayList<ObCheckMP>();for (ObCheckM checkM : listCheckM) {ObCheckQuery otmQuery = new ObCheckQuery(checkM);
//在循环内调用外围接口 List<String> listStatus = orderStatusService.getActiveStatusAll(otmQuery);if (null != listStatus && listStatus.size() > 0) {if(orderStatusService.checkOrderStatus(ConstantFields.OTM_STATUS_CHECK, listStatus) ||orderStatusService.checkOrderStatus(ConstantFields.OTM_STATUS_PACK, listStatus)) {log.info("订单:" + checkM.getOutboundNo() + "已经复核完成! (OTM)");
//复核台标记为空,或复核台标记不是rebinwall复核台的,复核完成的 订单不需要取出来if (StringUtils.isBlank(platformKey) ||!platformKey.equals(ConstantFields.PLATFORM_NO_REBINWALL)) {//单据状态服务已经显示复核或打包完成的 continue;}}if (checkM.getStatus() != ConstantFields.STATUS_CANCEL &&checkM.getStatus() != ConstantFields.STATUS_TROUBLE) {if(orderStatusService.checkOrderStatus(ConstantFields.STATUS_TROUBLE, listStatus)) {//校验订单是否被转病单 checkM.setStatus(ConstantFields.STATUS_TROUBLE);
//checkM.setYN(1); //转病单后,订单标记为为删除
//在循环查询数据库 checkMManager.updateCheckM(checkM);} else {otmQuery.setWaveNo("");//校验订单是否取消
//在循环内调用外围接口 listStatus =orderStatusService.getActiveStatusAll(otmQuery);if(orderStatusService.checkOrderStatus(ConstantFields.STATUS_CANCEL, listStatus)) {//校验订单是否被取消 checkM.setStatus(ConstantFields.STATUS_CANCEL);
//在循环内查询数据库 checkMManager.updateCheckM(checkM);}}}}if (checkM.getStatus() == ConstantFields.STATUS_INIT) {checkM.setStatus(ConstantFields.STATUS_CHECKING);checkM.setUpdateUser(checkQuery.getOperateUser());checkMManager.updateCheckM(checkM); //在循环内查询数据库 }List<ObCheckP> listCheckPOrder = this.getUnCheckedListForCheckMP(checkM,listCheckPAll);int totalQtySmall = 0;
//计算订单中小件商品总数量(不区分SKU),显示在订单明细列表中 for (ObCheckP checkP : listCheckPOrder) {//容器中订单的小件商品总数量 totalQtySmall += checkP.getGoodsQty();}ObCheckMP checkMP = new ObCheckMP();
//订单商品的SKU数量 checkM.setSkuQty(listCheckPOrder.size());checkM.setTotalQtySmall(totalQtySmall);checkMP.setCheckM(checkM);checkMP.setListCheckP(listCheckPOrder);listCheckMP.add(checkMP);}return listCheckMP;} catch (Exception ex) {throw new RuntimeException("获取容器中的订单和订单的商品明细异常!" +ex.getMessage(), ex);}}

以上代码的弊端:
假设一个容器内有50个订单的话,在根据容器的商品明细获取订单主档需要调用50次数据库查
询。判断单据状态需要调用50次单据状态服务,订单状态校验验证完后,
修改本地订单主档的状态也需要调用50次。另外,调用单据状态时,单据状态也要和数据库调用
50次。
总共调用200次数据库操作。最大的问题在于调用50次外围的单据状态,如果每次调用都有所延迟
的话,50次的调用延迟就想到可怕了,可能导致超时而无法继续。
修改后的代码如下:

    /*** 获取容器中的订单列表和具体订单的商品明细列表 */public List<ObCheckMP> getListCheckMP(List<ObCheckC> listCheckC, ObCheckQuery checkQuery) {try {//根据商品明细提取容器内的全部订单的订单号 Map<String, ObCheckP> map = new HashMap<String, ObCheckP>();List<String> listOutboundNo = new ArrayList<String>();for (ObCheckP cp : listCheckPAll) {if (!map.containsKey(cp.getOutboundNo())) {map.put(cp.getOutboundNo(), cp);
//将订单号放在集合里 查询采用in的方式 listOutboundNo.add(cp.getOutboundNo());}}
//按订单号获取出订单主档信息 ObCheckQuery tq = new ObCheckQuery(listCheckC.get(0));
//复核类型(rebinwall和一般订单的复核类型一样) tq.setCheckType(checkQuery.getCheckType());tq.setListOutboundNo(listOutboundNo);
//只需要一次查询数据库操作 List<ObCheckM> listCheckM = checkMManager.getListByQueryBean(tq);if (null == listCheckM || listCheckM.size() == 0) {throw new UserMessage("此容器中不存未复核的订单!");}
//抽出一个方法,单独校验单据状态,方法内调用一次单据状态,然后在内存里判断每个订单 的状态List<ObCheckM> listCheckingM = this.checkOutboundStatus(listCheckM, checkQuery);
//批量更新订单的主档信息(调用一次数据库)checkMManager.updateCheckM(listCheckingM, null);
//封装返回客户端对象 List<ObCheckMP> listCheckMP = new ArrayList<ObCheckMP>();for (ObCheckM checkM : listCheckM) {//提取当前订单的未复核商品明细,并合并相同SKU的商品记录 List<ObCheckP> listCheckPOrder = this.getUnCheckedListForCheckMP(checkM,listCheckPAll);
//计算订单中小件商品总数量(不区分SKU),显示在订单明细列表中 int totalQtySmall = 0;for (ObCheckP checkP : listCheckPOrder) {//容器中订单的小件商品总数量 totalQtySmall += checkP.getGoodsQty();}ObCheckMP checkMP = new ObCheckMP();checkM.setSkuQty(listCheckPOrder.size()); //订单商品的SKU数量 checkM.setTotalQtySmall(totalQtySmall);checkMP.setCheckM(checkM);checkMP.setListCheckP(listCheckPOrder);listCheckMP.add(checkMP);}return listCheckMP;} catch (Exception ex) {throw new RuntimeException("获取容器中的订单和订单的商品明细异常!" +ex.getMessage(), ex);}}/*** - 校验订单的单据状态 * <p> * - @param listCheckM * <p> * - @param checkQuery ** @return*/private List<ObCheckM> checkOutboundStatus(List<ObCheckM> listCheckM, ObCheckQuery checkQuery){String platformKey = checkQuery.getPlatformKey(); //复核台标识(区分rebinwall和一般订单的 复核类型)List<ObCheckM> listCheckingM = new ArrayList<ObCheckM>();List<ReceiptTrack> listTrack = orderStatusService.getActiveStatusAll(listCheckM); //调用 一次单据状态服务
//以下在内存校验定的的状态 (在内存的判断时间可以忽略) for (ObCheckM checkM : listCheckM) {if (null != listTrack && listTrack.size() > 0) {if (orderStatusService.checkOrderStatus("", checkM.getOutboundNo(),ConstantFields.OTM_STATUS_CHECK, listTrack) ||orderStatusService.checkOrderStatus("", checkM.getOutboundNo(),ConstantFields.OTM_STATUS_PACK, listTrack)) {log.info("订单:" + checkM.getOutboundNo() + "已经复核完成!(OTM)");if (StringUtils.isBlank(platformKey) ||!platformKey.equals(ConstantFields.PLATFORM_NO_REBINWALL)) {//复核台标记为空,或复核台标记不是rebinwall复核台的,复核完成的 订单不需要取出来listCheckM.remove(checkM);//将复核完成的订单从列表中移除 continue; //单据状态服务已经显示复核或打包完成的} }if (checkM.getStatus() != ConstantFields.STATUS_CANCEL && checkM.getStatus()!= ConstantFields.STATUS_TROUBLE) {//校验订单是否被转病单 if (orderStatusService.checkOrderStatus(checkM.getWaveNo(),checkM.getOutboundNo(), ConstantFields.STATUS_TROUBLE, listTrack)) {checkM.setStatus(ConstantFields.STATUS_TROUBLE);listCheckingM.add(checkM);} else {//校验订单是否被取消 if (orderStatusService.checkOrderStatus("",checkM.getOutboundNo(), ConstantFields.STATUS_CANCEL, listTrack)) {checkM.setStatus(ConstantFields.STATUS_CANCEL);listCheckingM.add(checkM);}}}}if (checkM.getStatus() == ConstantFields.STATUS_INIT) {checkM.setStatus(ConstantFields.STATUS_CHECKING);checkM.setUpdateUser(checkQuery.getOperateUser());listCheckingM.add(checkM);}}return listCheckingM;}

分析:
修改完后,只需要调用2次数据操作,和一次单据状态服务。

1.2. SQL的循环调用

主要针对查询,尽可能的将逐条查询转化为一次查询一个批次,减少与数据库交互次数。

1.3. Redis的循环调用

  1. 设计及代码评审时不能凭经验想当然,如对网络数据包大小等内容,一定要结合业务通过实
    测的方式估算,必须要进行边界限制
  2. 模块设计、部署方案需要根据业务及系统负载重新规划部署,避免交叉影响
  3. 系统压测要进行极端条件测试,对系统抗压能力用数据说话
  4. 面对突发状况,能根据具体业务单元负载情况,支持细粒度隔离降级,动态控制调节流量
  5. 获取兄弟系统及平台架构部支持,共同打造支持快速隔离、弹性扩容集群|
    总结:
    第一条的关键是频次对系统的影响,实现同样的功能,减少交互次数,降低性能开销;
    初次设计更直接的方式是逐条进行,这种情况下,养成对实现进行重构的习惯。

2. 禁止3B:Big Transaction,Big SQL,Big Batch

2.1. Big Transaction

注意点:
6. 对数据库操作必须使用事务,不能使用自动提交,尽量使用声明式事务;
7. 让事务尽可能的小,在Service层组装数据,在manager层处理事务;
8. 不要在事务里调用服务(服务可能阻塞);
9. 不要在事务里调用Redis;
10. 在事务中批量更新要排序,确保多事务并发时,避免资源锁等待。
单表(记录顺序):
A,B,C
C,B,A
多表(表顺序)
A→B
B→A

详解:
无论是Oracle、SqlServer还是Mysql,大事务是一定要避免的,大事务容易造成锁资源的长时间占用,从而降低并发性能,增大死锁概率。如下是几种大事务的典型场景:
1- @Transactional打在Class上,这样类中的所有方法均在事务边界内,容易造成大事务,@Transactional应该控制更精细一些,打到方法级;
2- 在一个事务中要更新多张表,在更新每一张表之前都要处理一堆业务逻辑(查询、运算、调用
服务等等),正确的做法应该是将查询、运算和服务调用逻辑提到事务外,事务边界内尽可能只处
理表更新操作;

2.2. Big SQL

SQL使用:

  1. 尽量不用表关联,如果使用表关联,不要超过3个表join;
  2. 热点数据尽量使用Redis;(比如基础资料)
  3. 尽量不用子查询,不用Exist,不在条件列上使用函数;

2.3. Big Batch

大批量的查询输出很容易将内存打爆,报表或者打印要分批处理。

3. 禁止全表扫描SQL和select *,update所 有列

3.1. 全表扫描

什么是全表扫描?
在数据库中,对无索引表查询或有索引但SQL不能有效利用进行查询的过程称为全表扫描。
全表扫描会搜寻数据库表中的每一条记录,直到所有符合给定条件的记录返回。

3.2. select *

查询需要的字段;
当需要查询全部字段时,写出全部字段名;

3.3. update所有列

1-不能写通用的update SQL,按业务更新;
2-按物理主键或业务主键进行update操作;

4. 禁止Worker扫描业务表

worker框架只限定:Tbschedule, xxl-job等其他分布式任务调度;
Quartz调度在新系统中不再使用;

4.1. 带来的问题

1-增加对业务表的访问压力;
2-如果涉及更新,影响并发性能;

4.2. 正确的做法

1-建立独立的任务表;
2-数据量大:基于时间做分区索引;
3-处理完成的任务可以删除或转历史,保证任务表数据量比较少

import java.util.HashMap;import java.util.HashSet;import java.util.Map;import java.util.Set;
/*** @title: TaskHashUtil* @projectName okr_performance* @description: 定时任务hashCode工具* @date 2020/8/19 14:36 */
public class TaskHashUtil {/*** 任务执行hash位数:31 */private static final int TASK_HASH_BIT = 0x1f;/*** 生成任务记录的hashCode * @return 返回0-31之间的int */public static int generateHashCode() {int i = (int) System.nanoTime();return i & TASK_HASH_BIT;}/*** 根据集群分片信息获取hashCode * @param serverCount 分片数 * @param curServer 当前分片 * @return*/public static Integer[] getHashCodeBySharding(Integer serverCount, Integer curServer) {Map<Integer, Set<Integer>> shardingMap = new HashMap<>();
//生成一个大小等于分片数的map
//每个map元素的value中包含不重复的0-31的随机数
//所有map元素的value覆盖0-31的每个数for (int i = 0; i < 32; i++) {int key = i % serverCount;if (!shardingMap.containsKey(key)) {Set<Integer> v = new HashSet<>();v.add(i);shardingMap.put(key, v);} else {shardingMap.get(key).add(i);}}Integer[] arr = new Integer[] {};return shardingMap.get(curServer).toArray(arr);}public static void main(String[] args){getHashCodeBySharding(6, 1);}
}

4.3. 举例

比如单据审核后,推送财务为例:
错误的做法是用worker扫描单据表,往财务推送数据
正确的做法是单据审核时,在任务表增加一个往财务推送数据的任务,任务可以包含数据,也可以
只存相关ID,worker扫描这个任务表,给财务推送数据,推送完成可以删除任务也可以把任务转
历史。

4.4. 数据转历史

不建议使用worker进行数据转历史,因为这样会出现worker扫业务表,同时删除生产数据性能不
好。
建议的做法:把表设计为热,凉,冷三个级别,以CRM的事件表为例
在生产系统中设计为两个表:
热表:只存未关闭的事件,可以做表分区,分区字段根据业务定;
凉表:存已关闭的事件,用时间做表分区;
事件关闭时,把事件从热表转到凉表。
凉表的数据转历史采用分区置换的方式,从而避免使用worker转历史。

5. 禁止没有边界限制的创建大量对象、Net IO

5.1. 创建大量对象

在大循环中创建大对象,很容易耗尽Java堆内存,根据内存状况设定一个安全阀值,有效控制其
无限增长。
几种情况:
持续向容器对象中插入对象,不做clear;
创建数据连接,网络连接不释放;
资源不释放;
COE:
异常数据引起创建大量实例
程序bug引起创建大量实例
案例1:
CRM调用服务超时,服务没有释放文件句柄,导致内存溢出。
案例2:(死循环)
xx服务平台JVM进程Crash问题
问题描述:xx服务平台部署了4台应用服务器,2013年10月30日,上午出现其中2台应用服务器的
JVM进程Crash,下午又出现4台服务器的JVM进程几乎同时Crash,查看tomcat日志无任何异常
或错误信息,只是日志突然中断,从监控看进程消失前包括cpu、memory、load、thread、tcp
等状态一切正常,看不出任何征兆。
问题原因:通过Core dump文件定位发现在进程消失前报了signal 11错误,初步断定和死循环相
关,后来从代码中发现在特殊的数据条件下存在一个死循环。
总结:递归要慎用,在正常条件下不会死循环,但在极端环境下就可能出现死循环。

5.2. Net IO

Net IO往往容易被我们忽略,在服务调用、存取缓存等场景下 ,都需要充分预估Net IO并设定安
全阀值,比如服务调用的返回值大小阀值,返回记录数量阀值等,读取缓存的频率要尽可能控制在
最小次数,每次读取的Value的大小安全阀值等等。

6. 禁止输入参数不做校验及服务直接抛出异 常

6.1. 参数校验

案例:
2012/8/5: POP系统Worker向青龙运单系统推送订单号和包裹数量,推送一条脏数据(订单号:282297153,包裹数量:282297153),造成运单系统要生成2亿8千万以上包裹对象,从而导致Tomcat内存溢出,无法提供服务;POP系统Worker调用失败后,会重复推送数据(没有次数限制),导致负载均衡下其它运单Tomcat相继在调用下内存溢出,从而整个系统故障。
这个案例本身涉及多个方面:
1-错误的调用;
2-参数未做校验;
3-创建对象未做限制,导致内存溢出;

6.2. 服务异常

异常处理是系统内的一种错误处理机制,一般不用于跨系统调用;
通过定义错误码方式是处理跨系统错误的正确方式;
返回错误码,更便于调用方根据不同的返回值进行不同的处理,抛出异常的方式实际上是将底层的
实现细节暴露给调用方。

7. 禁止服务及UI按钮不做防重入

7.1. 服务防重

MQ消息重复:
在服务端防止重复数据被多次被插入到数据库。
常用的办法:

7.2. UI防重入

在用户操作完成后,应当将界面变为不可操作状态(比如:按钮不可点击等,不再相应enter事件
等等)。

8. 禁止一次性查询或导出全部数据,禁止单 次操作数据超过5000条

1-分页SQL来处理,异步方式,控制导出权限;
2-设定fetchsize的方式;
3-禁止从生产主库导出;
MYSQL中,fetchsize启用的前提条件: 1.MySQL版本在5.0以上,MySQL的JDBC驱动更新到最
新版本(至少5.0以上)
2.Statement一定是TYPE_FORWARD_ONLY的,并发级别是CONCUR_READ_ONLY(即创建Statement的默认参数)
3.以下两句语句选一即可: 1).
statement.setFetchSize(Integer.MIN_VALUE); 2).
((com.mysql.jdbc.Statement)stat).enableStreamingResults();
mysql JDBC连接参数:
“useCursorFetch=true&defaultFetchSize=2000”

9. 禁止线上服务不接入方法性能监控和存活 监控

9.1. 服务监控设计

线上的服务一律要接监控方法性能监控和存活监控,存活监控包括端口存活监控和URL存活监控。
服务监控从三个层面出发考虑:
系统外部接口:系统对外提供的接口或服务。
系统交互:系统依赖的外部接口,各个子API及调用关系, 系统中的任何场景。
系统自身:各个子的进程,系统内各个模块的API及调用关系,系统依赖的第三方组件。

10. 禁止服务产生底层依赖上层,强依赖 弱、循环依赖

10.1. 依赖设计

1-上层可以直接调用底层;
2-底层需要用到上层的数据,通过可以异步方式(如MQ),不要直接取调用上层服务;
3-平台级服务不要去调用弱的客户端服务,反之可以;
4-依赖关系要清晰,避免产生循环依赖。
平行系统之间的强依赖问题:
1-降级;
2-通过第三方系统解耦;
案例:

线上故障之-数据库问题相关推荐

  1. java基础巩固-宇宙第一AiYWM:为了维持生计,做项目经验之~SSM项目错误集锦Part3(项目蹦+pg数据库坏+100%-->线上故障排查经验【业务bug第一步一定是先看日志,写好日志】)~整起

    项目中遇到的一个问题:项目忽然蹦了,用我们的域名登陆不上去了. 根据之前的经验,一般比如我们项目登不上去了或者数据库不上数据了(数据不更新),直接在Xshell上远程reboot一下,再重启一下tom ...

  2. JAVA 线上故障排查套路,从 CPU、磁盘、内存、网络到GC 一条龙!

    点击上方蓝色"方志朋",选择"设为星标" 回复"666"获取独家整理的学习资料! 线上故障主要会包括cpu.磁盘.内存以及网络问题,而大多数 ...

  3. JAVA 线上故障排查完整套路,从 CPU、磁盘、内存、网络、GC 一条龙!

    点击上方"方志朋",选择"设为星标" 回复"666"获取新整理的面试文章 作者:fredal https://fredal.xin/java ...

  4. JAVA 线上故障排查完整套路!牛掰!

    点击上方"方志朋",选择"设为星标" 回复"666"获取新整理的面试文章 来源丨8rr.co/kV3R 线上故障主要会包括 CPU.磁盘.内 ...

  5. 线上故障如何快速排查?来看这套技巧大全

    简介:有哪些常见的线上故障?如何快速定位问题?本文详细总结工作中的经验,从服务器.Java应用.数据库.Redis.网络和业务六个层面分享线上故障排查的思路和技巧.较长,同学们可收藏后再看. 前言 线 ...

  6. idea本地跑如何看gc日志_线上故障如何快速排查?来看这套技巧大全

    简介:有哪些常见的线上故障?如何快速定位问题?本文详细总结工作中的经验,从服务器.Java应用.数据库.Redis.网络和业务六个层面分享线上故障排查的思路和技巧.较长,同学们可收藏后再看. 前言 线 ...

  7. du -sh 如何找到最大的文件夹_线上故障如何快速排查?来看这套技巧大全

    简介:有哪些常见的线上故障?如何快速定位问题?本文详细总结工作中的经验,从服务器.Java应用.数据库.Redis.网络和业务六个层面分享线上故障排查的思路和技巧.较长,同学们可收藏后再看. 前言 线 ...

  8. 如何避免大规模线上故障?

    作者 | 曹春晖       责编 | 欧阳姝黎 Fail at Scale 是 Facebook 2015 年在 acm queue 上发表的一篇文章.主要写了常见的线上故障和应对方法,内容还是比较 ...

  9. 深入理解MySQL8中死锁及线上故障解决

    深入理解MySQL8中死锁及线上故障解决 一.什么是死锁 死锁是指两个或两个以上的事务在执行过程中,因争夺锁资源而造成的一种互相等待的现象. 若无外力作用,事务都将无法推进下去. 解决死锁问题最简单的 ...

最新文章

  1. ​Xamarin iOS教程之视图显示图像
  2. js笔记 - ajax中的get和post说明
  3. shell实例第4讲:批量创建用户
  4. Java多线程两种实现方式的对比
  5. 互联网经济与实体经济
  6. java拆分单元格_Java 拆分Excel单元格数据为多列
  7. 首款“印度制造”的微处理器 AJIT 面世!
  8. github上一些酷炫效果
  9. 操作系统概述(发展,特性,功能)
  10. iit delhi_向印度最聪明的人学习—这里有来自IIT的300项免费课程即将开始
  11. 【通通免费】分享3个超级实用的电脑小工具,一个都不能少哦!
  12. HTML中的盒子模型
  13. 输入一个字符,是小写转换为大写,大写转换为小写,其他字符原样输出
  14. Linux解压压缩包到同名目录,里面的文件会自动覆盖吗?
  15. 互联网公司平均薪资Top8 , 阿里勇夺第一。
  16. Android学习笔记-传感器开发之利用传感器和Tween开发简易指南针
  17. 在线PNG图片压缩工具推荐——TinyPng
  18. python校正人脸_Python 进行人脸校正
  19. How Does Linphone Toggle SIP Contact?
  20. python 生信分析_用python做生物信息数据分析(2-pysam)

热门文章

  1. 【Unity资源】(贴图和材质)
  2. 关于原生html和js上传文件的处理
  3. AISAS消费者行为模式
  4. 51单片机汇编_冒泡法排序
  5. iPhone九宫格的实现
  6. MySQL保存微信呢称和QQ呢称上有很多火星文和emoji表情图片,解决mysql配置utf8mb4 的问题
  7. 网小鱼Java的bug小集锦0014
  8. 执行、管理、领导做不好,都有懒的因素
  9. 正项级数的积分审敛法,p级数的敛散性
  10. [置顶] 技术人血泪史:七种IT失误让你直接走人