数据库问题概述
索引:
索引分类:主键索引,普通索引,复合索引,唯一索引 技术名词:回表,最左匹配,索引覆盖,索引下推 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);
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)");
订单不需要取出来
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);
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;
for (ObCheckP checkP : listCheckPOrder) {
totalQtySmall += checkP.getGoodsQty();
}ObCheckMP checkMP = new ObCheckMP();
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);
listOutboundNo.add(cp.getOutboundNo());
}
}
ObCheckQuery tq = new ObCheckQuery(listCheckC.get(0));
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);
List<ObCheckMP> listCheckMP = new ArrayList<ObCheckMP>();
for (ObCheckM checkM : listCheckM) {
List<ObCheckP> listCheckPOrder = this.getUnCheckedListForCheckMP(checkM,
listCheckPAll);
int totalQtySmall = 0;
for (ObCheckP checkP : listCheckPOrder) {
totalQtySmall += checkP.getGoodsQty();
}
ObCheckMP checkMP = new ObCheckMP();
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);
}
}
private List<ObCheckM> checkOutboundStatus(List<ObCheckM> listCheckM, ObCheckQuery checkQuery)
{
String platformKey = checkQuery.getPlatformKey();
复核类型)
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)) {
订单不需要取出来
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;
public class TaskHashUtil {
private static final int TASK_HASH_BIT = 0x1f;
public static int generateHashCode() {
int i = (int) System.nanoTime();
return i & TASK_HASH_BIT;
}
public static Integer[] getHashCodeBySharding(Integer serverCount, Integer curServer) {
Map<Integer, Set<Integer>> shardingMap = new HashMap<>();
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-通过第三方系统解耦; 案例:
|