线上故障之-数据库问题
- 数据库问题概述
- 索引:
- 高可用
- 一些需要注意的事项
- 处理问题的一些技巧
- 一般大厂数据库规约
- 死锁
- 构建+合并代码+jar包问题+环境隔离
- 连接池
- 容易忽略的点
- 开发规范TOP10-晋升考核内容指导篇
数据库问题概述
索引:
索引分类:主键索引,普通索引,复合索引,唯一索引
技术名词:回表,最左匹配,索引覆盖,索引下推
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");
@Test
public void dateTest(){
ExecutorService executorService = Executors.newFixedThreadPool(500);
for (int i = 0; i < 500; i++) {
executorService.execute(new Runnable() {
@Override
public 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 {
@Test
public void dateTest1(){
ExecutorService executorService = Executors.newFixedThreadPool(500);
for (int i = 0; i < 500; i++) {
executorService.execute(new Runnable() {
@Override
public 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;
解决方法:
- 统一使用一种类型Integer或Long;
- 将数值转换成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的循环调用
- 设计及代码评审时不能凭经验想当然,如对网络数据包大小等内容,一定要结合业务通过实
测的方式估算,必须要进行边界限制 - 模块设计、部署方案需要根据业务及系统负载重新规划部署,避免交叉影响
- 系统压测要进行极端条件测试,对系统抗压能力用数据说话
- 面对突发状况,能根据具体业务单元负载情况,支持细粒度隔离降级,动态控制调节流量
- 获取兄弟系统及平台架构部支持,共同打造支持快速隔离、弹性扩容集群|
总结:
第一条的关键是频次对系统的影响,实现同样的功能,减少交互次数,降低性能开销;
初次设计更直接的方式是逐条进行,这种情况下,养成对实现进行重构的习惯。
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使用:
- 尽量不用表关联,如果使用表关联,不要超过3个表join;
- 热点数据尽量使用Redis;(比如基础资料)
- 尽量不用子查询,不用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-通过第三方系统解耦;
案例: