IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 大数据 -> 线上故障之-数据库问题 -> 正文阅读

[大数据]线上故障之-数据库问题

数据库问题概述

索引:

索引分类:主键索引,普通索引,复合索引,唯一索引
技术名词:回表,最左匹配,索引覆盖,索引下推
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;
解决方法:

  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-通过第三方系统解耦;
案例:
在这里插入图片描述

  大数据 最新文章
实现Kafka至少消费一次
亚马逊云科技:还在苦于ETL?Zero ETL的时代
初探MapReduce
【SpringBoot框架篇】32.基于注解+redis实现
Elasticsearch:如何减少 Elasticsearch 集
Go redis操作
Redis面试题
专题五 Redis高并发场景
基于GBase8s和Calcite的多数据源查询
Redis——底层数据结构原理
上一篇文章      下一篇文章      查看所有文章
加:2022-03-13 21:52:47  更:2022-03-13 21:53:14 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 8:58:23-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码