开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

华为 Java 开发编程军规,谁违反谁走

发表于 2020-12-28

1、引言

这个标准是衡量代码本身的缺陷,也是衡量一个研发人员本身的价值。华为作为一家全球化的 IT 公司,十几万员工,无论是人事管理,还是代码管理,都是一件不容易的事情,没有规范的约束,想想都是件可怕的事情。下面挑选了一些网上流传的编程规范,一起来学习下,以下内容不涉及基础的语法规范(请见 Refer),更侧重于一些编程习惯,如何提高程序的健壮性、可维护性等。(PS:以下内容未经官方考证,如阅读者出现不适,请选择立即关闭本页 -_-||| )

2、军规简介

军规一:【避免在程序中使用魔鬼数字,必须用有意义的常量来标识。】

军规二:【明确方法的功能,一个方法仅完成一个功能。】

军规三:【方法参数不能超过5个】

军规四:【方法调用尽量不要返回null,取而代之以抛出异常,或是返回特例对象(SPECIAL CASE object,SPECIAL CASE PATTERN);对于以集合或数组类型作为返回值的方法,取而代之以空集合或0长度数组。】

军规五:【在进行数据库操作或IO操作时,必须确保资源在使用完毕后得到释放,并且必须确保释放操作在finally中进行。】

军规六:【异常捕获不要直接catch (Exception ex) ,应该把异常细分处理。】

军规七:【对于if „ else if „(后续可能有多个else if …)这种类型的条件判断,最后必须包含一个else分支,避免出现分支遗漏造成错误;每个switch-case语句都必须保证有default,避免出现分支遗漏,造成错误。】

军规八:【覆写对象的equals()方法时必须同时覆写hashCode()方法。】

军规九:【禁止循环中创建新线程,尽量使用线程池。】

军规十:【在进行精确计算时(例如:货币计算)避免使用float和double,浮点数计算都是不精确的,必须使用BigDecimal或将浮点数运算转换为整型运算。】

3、军规说明

军规一:【避免在程序中使用魔鬼数字,必须用有意义的常量来标识。】 说明:是否是魔鬼数字要基于容易阅读和便于全局替换的原则。0、1作为某种专业领域物理量枚举数值时必须定义常量,严禁出现类似NUMBER_ZERO的“魔鬼常量”。

军规二:【明确方法的功能,一个方法仅完成一个功能。】 说明:方法功能太多,会增加方法的复杂度和依赖关系,不利于程序阅读和将来的持续维护,无论是方法还是类设计都应符合单一职责原则。

军规三:【方法参数不能超过5个】 说明:参数太多影响代码阅读和使用,为减少参数,首先要考虑这些参数的合理性,保持方法功能单一、优化方法设计,如果参数确实无法减少,可以将多个参数封装成一个类(对象),同时考虑在新的类(对象)中增加相应的行为,以期更符合OOP。

军规四:【方法调用尽量不要返回null,取而代之以抛出异常,或是返回特例对象(SPECIAL CASE object,SPECIAL CASE PATTERN);对于以集合或数组类型作为返回值的方法,取而代之以空集合或0长度数组。】 说明:返回null会增加不必要的空指针判断,遗漏判断也会导致严重的NullPointerException错误。

军规五:【在进行数据库操作或IO操作时,必须确保资源在使用完毕后得到释放,并且必须确保释放操作在finally中进行。】 说明:数据库操作、IO操作等需要关闭对象必须在try -catch-finally 的finally中close(),如果有多个IO对象需要关闭,需要分别对每个对象的close()方法进行try-catch,防止一个IO对象关闭失败其他IO对象都未关闭。

军规六:【异常捕获不要直接 catch(Exception ex) ,应该把异常细分处理。】 说明:catch (Exception ex)的结果会把RuntimeException异常捕获,RuntimeException是运行期异常,是程序本身考虑不周而抛出的异常,是程序的BUG,如无效参数、数组越界、被零除等,程序必须确保不能抛出RuntimeException异常,不允许显示捕获RuntimeException异常就是为了方便测试中容易发现程序问题。

军规七:【对于if „ else if „(后续可能有多个elseif …)这种类型的条件判断,最后必须包含一个else分支,避免出现分支遗漏造成错误;每个switch-case语句都必须保证有default,避免出现分支遗漏,造成错误。】

军规八:【覆写对象的equals()方法时必须同时覆写hashCode()方法。】 说明:equals和hashCode方法是对象在hash容器内高效工作的基础,正确的覆写这两个方法才能保证在hash容器内查找对象的正确性,同时一个好的hashCode方法能大幅提升hash容器效率。

军规九:【禁止循环中创建新线程,尽量使用线程池。】

军规十:【在进行精确计算时(例如:货币计算)避免使用float和double,浮点数计算都是不精确的,必须使用BigDecimal或将浮点数运算转换为整型运算。】 说明:浮点运算在一个范围很广的值域上提供了很好的近似,但是它不能产生精确的结果。二进制浮点对于精度计算是非常不适合的,因为它不可能将0.1——或者10的其它任何次负幂精确表示为一个长度有限的二进制小数。

具体案例请参考:浮点数加法引发的问题:浮点数的二进制表示

my.oschina.net/leejun2005/…

4、有关开发效率和协作的几点建议与心得体会

今天看到某同学写给团队成员的一封邮件,发现比较通用,分享出来吧:

1、小提交:

把大的任务拆分成多个独立小任务,每完成小任务确保无 Bug 后就可以提交合并到主分支甚至发布;频繁提交有利于自己把控项目进度、降低风险、同其他人协作和代码 Review ; 每天可以提交合并多次。每个小任务是 1-2 个小时可以完成的粒度,最大的一天完成。并行做多个任务的时候,优先做最短时间能够实现的任务。

2、命名规范:

尽量避免无意义的字符做变量 比如 a, b, t 。可以逐步改善,可以参考:google-styleguide.googlecode.com/svn/trunk/j…

3、避免过度设计: 能够用简单方式实现的功能,不引入复杂的类,对象,避免不必要的 new 对象,避免引入不必要的泛型、线程。开发初期冗余大于抽象和依赖。避免自己重新实现比较通用的组件和函数。调研多种实现方式的时候,选用做简单的实现方式。尽量少写代码。

4、Web 工程尽量避免在应用内部保存“状态”,这样可以适应频繁发布、重启无影响。

5、善于用打日志的方式调试,在程序关键点打日志。尽量少用断点方式,日志方式可以批量调试一批功能,效率相对高。

6、避免一屏显示不下的超大函数。

7、添加必要、简洁的注释:

循环中的 continue, break 尽量加上单行注释;尽量避免非函数结尾的 return,必要的时候加注释。类自动生成 toString() 方法,方便调试和打日志。

8、不把自己局限到做某个功能,每个人都是整个项目的 Owner ,尽量交叉 Review ,交叉开发。

9、遇到问题及时和其他人沟通,避免浪费时间。

10、从最终产品的目标审视自己细小的设计,熟悉自己负责部分的上下游代码。时刻关注最终产品(Web 界面和日志),发现 Bug 和可以改善的地方。

来源:blog.csdn.net/chenleixing/article/details/44173985

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

软件测试详解7—软件测试必备7大技能

发表于 2020-12-28

一、测试用例的编写

1.在测试中最重要的文档,他是测试工作的核心,是一组在测试时输入输出的标准,是软件需求的具体对照。编写测试用例,是测试人员的基本功,真正能写好的人并不多。

2.测试用例包含的内容:用例编号,用例名称,测试背景,前置条件,优先级,测试数据,测试步骤,预期结果,实际结果,备注。

3.当测试小伙伴拿到第一个需求文档的时候,进行分析,提取测试点,编写测试用例,然后叫上开发,产品以及相关人员进行用例评审。

4.编写测试用例常用的方法:等价类划分法,边界值分析法,错误推断法,流程图法等,

5.学会质疑需求,不要完全按照需求来写测试用例,要从用户角度去理解需求,看到需求之外的功能和体验。

二、http与https协议

1、http协议原理

HTTP协议是Hyper Text Transfer Protocol(超文本传输协议)的缩写,是用于从万维网(WWW:World Wide Web )服务器传输超文本到本地浏览器的传送协议。—-自行百度补脑

2、TCP和UDP区别

TCP和UDP是OSI模型中的运输层中的协议。TCP提供可靠的通信传输,而UDP则常被用于广播和细节控制交给应用的通信传输.。—自行百度补脑

3、get和post区别

1)Get,它用于获取信息,它只是获取、查询数据,也就是说它不会修改服务器上的数据,从这点来讲,它是数据安全的,而稍后会提到的Post它是可以修改数据的,所以这也是两者差别之一了。

2)Post,它是可以向服务器发送修改请求,从而修改服务器的,比方说,我们要在论坛上回贴、在博客上评论,这就要用到Post了,当然它也是可以仅仅获取数据的。

三、业务了解能力

首先明确一点一定要了解业务,只有了解业务,才能把功能测试做好,并且会有一定的提高。

什么时候接入自动化—–当你的系统趋于稳定的时候。

什么时候介入接口测试—–当接口开发完毕的时候。

什么时候介入性能测试—–当出现促销的时候,或者抢购的时候等等。

四、测试工具的使用

1、接口测试工具:apipost

使用apipost进行接口测试,输入url地址,请求方法,参数点击发送查看接口是否正确。
在这里插入图片描述2、性能测试工具:jmeter

使用jmeter对系统进行性能测试,对系统进行压力测试、负载测试、稳定性测试等
在这里插入图片描述

五、自动化测试代码编写能力

1、ui自动化编写

a、web自动化:一般是使用python+selenium对web网站进行ui自动化脚本的编写

b、app自动化:一般是使用python+appuim对移动端进行ui自动化脚本的编写

2、接口自动化编写

一般是使用python+requests进行接口自动化测试的
在这里插入图片描述

六、熟练使用SQL

1.常用的sql语句,如增删改查。

2.了解数据库的事务,会编写存储过程,熟练常用的系统函数。

3.了解并可以进行数据库的备份,迁移,还原,镜像等。

4.对sql语句进行调优,并对可以运行的语句监控查看性能。

5.了解数据库的集群。

七、Linux

1.Linux是测试人员的基本功,不需要掌握太难或者很不常见的Linux命令,正常能做到查看日志,定位问题就可以了。

2.初级测试人员工作是经常遇到,发现bug,开发不承认或者不愿意解决的情况,测试人员怎么摆脱这样的问题呢?

根据发现的bug日志级别,来查看日志,定位问题,那这里首先要说一下日志级别。日志级别越高,输出的信息越少。

info:代码info信息,不包括sql语句等一些debug信息。

warning :代码警告信息。

error : 程序本身报错信息

一般不符合需求的bug在debug中,程序本身报错的在error中。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

阿里云开源项目 OAM 负责人张磊入选「中国开源先锋 33

发表于 2020-12-28

来源|阿里巴巴云原生公众号

2020 年 12 月 23 日,由 SegmentFault 思否发起的第二届“中国技术先锋”年度评选结果揭晓,CNCF 应用交付领域小组 Co-chair、阿里云高级技术专家、OAM 开源项目负责人张磊入选 2020“中国开源先锋 33 人”年度榜单。

1.png

近两年,开源一直是技术圈非常火热的话题。本次评选结果是 SegmentFault 思否依托数百万开发者用户数据分析,以及各科技企业和个人在国内技术领域的行为、影响力指标产生, 旨在让那些助推中国开源生态发展道路上关键的“幕后英雄“走入大家的视野,从而带动中国本土开源文化的繁荣。

Kubernetes 老兵与首个云原生“开放应用模型”

在开源技术的支持和推动下,近年来云原生的理念不断丰富和落地,并迅速构建起以容器技术、容器编排技术为核心的生态,有数据显示,Kubernetes 已成为被企业选用最多的容器编排技术。

2.jpg

张磊可以说是 Kubernetes 领域最早期的玩家,从 2014 年就开始进入 Kubernetes 上游从事技术工作,2015 年即成为 Kubernetes 社区最早的一批 Maintainer 之一,并于 2016 年即被推选为 CNCF 官方大使。在 Kubernetes 社区中,张磊是 Kubernetes 容器运行时接口(CRI)和 KataContainers 运行时的早期设计者和维护者之一,也是 Kubernetes 等价类调度、拓扑资源管理等多个大颗粒核心特性的主要作者。

2018 年后加入阿里巴巴,张磊主导提出了基于 Kubernetes 的“云原生应用交付体系”,同年以最高票当选 CNCF 应用交付领域小组 co-chair,是目前 CNCF 七大领域小组中唯一一位华人 co-chair,主导小组将 Argo 等多个知名应用管理领域开源项目纳入 CNCF 孵化器当中,同时被推荐为 Argo 开源社区 TOC。

2019 年末,张磊团队主导阿里云联合微软 CTO Office 团队共同提出了“开放应用模型”开源项目 OAM (Open Application Model),这是业界第一个云原生应用交付与管理领域的标准模型与框架。

2020 年 11 月,OAM 模型的 Kubernetes 实现 KubeVela 正式开源,使 OAM 的理念得到进一步完善。KubeVela 是一个能够以 Kubernetes 原生的方式进行任意扩展的 PaaS 内核,平台工程师可以基于它构建出任意的垂直业务系统。事实上,现在的 OAM 模型和它的 Kubernetes 实现 KubeVela 项目,本身就是阿里云原生应用基础设施的核心组件,支撑着包括阿里云 EDAS 服务、阿里集团核心 PaaS、阿里云边缘计算平台、达摩院 AI PaaS 在内的多个互联网级平台的内核的运行与扩展。

解决云原生的“最后一公里”难题

今天在 OAM 社区中,有大量的贡献来自 Oracle、MasterCard、Upbound.io、腾讯、字节跳动、第四范式和满帮集团等十余家技术公司与团队,他们不仅是 OAM 社区重要的技术力量,很多还是 KubeVela 项目的早期发起者。在接下来的设计中,OAM 社区会以 KubeVela 为核心,在已经生产可用的平台层模型的基础上,继续建设面向开发者的用户侧模型,并且以此为基础通过 Dapr sidecar 和 Istio 来完善应用层中间件与流量治理能力,实现“让云原生应用交付轻松愉悦(Make shipping applications more enjoyable)”的目标。

3.png

“让业务研发专注于写代码的这种体验,说起来简单,宣传起来也很赞,但从云原生技术诞生到现在,在整个云原生生态的持续努力下,这件事情依然只解决了一小部分。而如今,OAM 及 KubeVela 项目的提出与发布,正是回答云原生‘最后一公里’的问题,也是整个云原生生态继续推动这件事情向终态前进的一个缩影,希望更多的开发者加入我们”,张磊表示。

欢迎更多的开发者一起参与到 OAM 项目的建设中来,你可以前往 OAM 官方网站 及 GitHub 项目地址更好地了解、学习和使用,也欢迎钉钉搜索群号:23310022,和近 2000 名开发者互动交流!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Java代码调用Oracle的存储过程 Java代码调用Or

发表于 2020-12-28

Java代码调用Oracle的存储过程,存储函数和包

java代码调用如下的存储过程和函数

1
2
3
4
5
6
7
8
sql复制代码create or replace procedure queryEmpinfo(eno in number,
pename out varchar2,
psal out number,
pjob out varchar2)
as
begin
select ename,sal,empjob into pename,psal,pjob from emp where empno=eno;
end;
1
2
3
4
5
6
7
8
9
10
11
12
13
sql复制代码--查询某个员工的年收入
create or replace function queryEmpIncome(eno in number)
return number
as
psal emp.sal%type;
pcomm emp.comm%type;
begin
select sal,comm into psal,pcomm from emp where empno=eno;

--返回年收入
return psal*12+nvl(pcomm,0);

end;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
sql复制代码--在out参数中使用光标
查询某个部门中所有员工的所有信息


--包头
CREATE OR REPLACE PACKAGE MYPACKAGE AS

type empcursor is ref cursor;
procedure queryEmpList(dno in number,empList out empcursor);

END MYPACKAGE;


--包体
CREATE OR REPLACE
PACKAGE BODY MYPACKAGE AS

procedure queryEmpList(dno in number,empList out empcursor) AS
BEGIN
open empList for select * from emp where deptno=dno;
END queryEmpList;

END MYPACKAGE;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
java复制代码import java.sql.CallableStatement;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;

import oracle.jdbc.driver.OracleCallableStatement;
import oracle.jdbc.driver.OracleTypes;

import org.junit.Test;

public class TestOracle {

/*
* CallableStatement 接口
* 调用存储函数,等号左边有一个返回值
* {?= call <procedure-name>[(<arg1>,<arg2>, ...)]}
* 调用存储过程. 没有返回值
{call <procedure-name>[(<arg1>,<arg2>, ...)]}

*
*/

/*存储过程 查询某个员工的姓名  月薪 职位
* create or replace procedure queryEmpinfo(eno in number,
pename out varchar2,
psal out number,
pjob out varchar2)
*/

@Test
public void testProcedure(){
//{call <procedure-name>[(<arg1>,<arg2>,...)]}
String sql = "{call queryEmpinfo(?,?,?,?)}";//4个问号中,第一个是输入参数,其余是输出参数
Connection conn = null;
//要用CallableStatement这个接口,用于执行 SQL 存储过程的接口
CallableStatement call = null;

try {
conn = JDBCUtils.getConnection();
call = conn.prepareCall(sql);
//对于in参数,需要赋值
call.setInt(1,7839);
//对于out参数,需要声明
call.registerOutParameter(2, OracleTypes.VARCHAR);//第二个是字符串
call.registerOutParameter(3, OracleTypes.NUMBER);//第三个是数字
call.registerOutParameter(4, OracleTypes.VARCHAR);//第四个是字符串

call.execute();
//取出结果
String name = call.getString(2);
double sal = call.getDouble(3);
String job = call.getString(4);
System.out.println(name+"\t"+sal+"\t"+job+"\t");
} catch (SQLException e) {
e.printStackTrace();
}finally{
JDBCUtils.release(conn, call, null);//没有最后一个参数就传入null
}
}

/*存储函数 查询某个员工的姓名,月薪和职位
* create or replace function queryEmpIncome(eno in number)
return number
*/
@Test
public void testFunction(){
//{?= call <procedure-name>[(<arg1>,<arg2>, ...)]}
//第一个问号是函数的返回值,第二个问号是输入参数. 返回值的作用和输出参数是一样的.
String sql = "{?=call QUERYEMPINCOME(?)}";//这个call后面的存储过程名或者是存储函数名大写或者是小写是没有要求的.
Connection conn = null;
//要用CallableStatement这个接口,用于执行 SQL 存储过程的接口
CallableStatement call = null;

try {
conn = JDBCUtils.getConnection();
call = conn.prepareCall(sql);

//对于in参数,赋值
call.setInt(2,7839);

//对于out参数,申明
call.registerOutParameter(1, OracleTypes.NUMBER);
call.execute();
//取出结果
//取出结果
double income = call.getDouble(1);
System.out.println(income);
} catch (SQLException e) {
e.printStackTrace();
}finally{
JDBCUtils.release(conn, call, null);//没有最后一个参数就传入null
}


}

/*
查询某个部门中所有员工的所有信息
包头
CREATE OR REPLACE PACKAGE MYPACKAGE AS

type empcursor is ref cursor;
procedure queryEmpList(dno in number,empList out empcursor);

END MYPACKAGE;


包体
CREATE OR REPLACE
PACKAGE BODY MYPACKAGE AS

procedure queryEmpList(dno in number,empList out empcursor) AS
BEGIN
open empList for select * from emp where deptno=dno;
END queryEmpList;

END MYPACKAGE;
*/
@Test
public void testCursor(){
//{call <procedure-name>[(<arg1>,<arg2>, ...)]}
String sql = "{call MYPACKAGE.queryEmpList(?,?)}";

Connection conn = null;
CallableStatement call = null;
//有游标,就有结果集
ResultSet rest = null;
try {
conn = JDBCUtils.getConnection();
call = conn.prepareCall(sql);

//对于in参数,赋值
call.setInt(1, 20);

//对于out参数,申明
call.registerOutParameter(2, OracleTypes.CURSOR);
call.execute();
//取出集合
//这个地方要强转!!!OracleCallableStatement是抽象类,继承了CallableStatement
//不强转没有getCursor()方法...
rest = ((OracleCallableStatement)call).getCursor(2);
while(rest.next()){
String name = rest.getString("ename");
double sal = rest.getDouble("sal");
System.out.println(name+"\t"+sal);
}
}catch (Exception e) {
e.printStackTrace();
}finally{
JDBCUtils.release(conn, call, rest);//上面打开了光标,再这个地方关闭结果集rest,也就关闭了光标
}
}
}

关于Oracle中的包对象,之前的存储函数中查询的是某一个员工的信息

1
2
3
4
5
6
7
8
sql复制代码create or replace procedure queryEmpinfo(eno in number,
pename out varchar2,
psal out number,
pjob out varchar2)
as
begin
select ename,sal,empjob into pename,psal,pjob from emp where empno=eno;
end;

但是①如果要查询一个员工的所有信息,而这个员工的信息对应的有几百列

在存储函数中括号的函数要把这几百列都声明出来?

②如果要查询某个部门中所有员工的所有信息…这个信息对应的是一个集合.

第二个问题解决了第一个问题也就解决了.

怎么在存储过程或者存储函数中返回一个集合.

学到现在有多少种方式可以代表一个集合?

第一个是表,第二个是select语句也可以.第三个是光标.

在out参数中使用光标.但是有一个要求,必须要声明一个包,包分为包头和包体.也是数据库的对象.跟表,视图,等是一样的是数据库的对象.

包头只负责声明,包体只负责实现.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
sql复制代码--在out参数中使用光标
--查询某个部门中所有员工的所有信息


--包头
CREATE OR REPLACE PACKAGE MYPACKAGE AS

type empcursor is ref cursor;
procedure queryEmpList(dno in number,empList out empcursor);

END MYPACKAGE;


--包体
CREATE OR REPLACE
PACKAGE BODY MYPACKAGE AS

procedure queryEmpList(dno in number,empList out empcursor) AS
BEGIN
open empList for select * from emp where deptno=dno;
END queryEmpList;

END MYPACKAGE;

分析图

参看包


包无法在plsqldeveloper和sqldeveloper等工具中右键运行….必须通过java代码应用程序来调用执行(代码在上面)

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

vivo 全球商城:订单中心架构设计与实践 一、背景 二、系

发表于 2020-12-28

一、背景

随着用户量级的快速增长,vivo 官方商城 v1.0 的单体架构逐渐暴露出弊端:模块愈发臃肿、开发效率低下、性能出现瓶颈、系统维护困难。

从2017年开始启动的 v2.0 架构升级,基于业务模块进行垂直的系统物理拆分,拆分出来业务线各司其职,提供服务化的能力,共同支撑主站业务。

订单模块是电商系统的交易核心,不断累积的数据即将达到单表存储瓶颈,系统难以支撑新品发布和大促活动期间的流量,服务化改造势在必行。

本文将介绍 vivo 商城 订单系统建设的过程中遇到的问题和解决方案,分享架构设计经验。

二、系统架构

将订单模块从商城拆分出来,独立为订单系统,使用独立的数据库,为商城相关系统提供订单、支付、物流、售后等标准化服务。

系统架构如下图所示:

三、技术挑战

3.1 数据量和高并发问题

首先面对的挑战来自存储系统:

  • 数据量问题

随着历史订单不断累积,MySQL中订单表数据量已达千万级。

我们知道InnoDB存储引擎的存储结构是B+树,查找时间复杂度是O(log n),因此当数据总量n变大时,检索速度必然会变慢, 不论如何加索引或者优化都无法解决,只能想办法减小单表数据量。

数据量大的解决方案有:数据归档、分表

  • 高并发问题

商城业务处于高速发展期,下单量屡创新高,业务复杂度也在提升,应用程序对MySQL的访问量越来越高。

单机MySQL的处理能力是有限的,当压力过大时,所有请求的访问速度都会下降,甚至有可能使数据库宕机。

并发量高的解决方案有:使用缓存、读写分离、分库

下面对这些方案进行简单描述:

  • 数据归档

订单数据具备时间属性,存在热尾效应,大部分情况下检索的都是最近的订单,而订单表里却存储了大量使用频率较低的老数据。

那么就可以将新老数据分开存储,将历史订单移入另一张表中,并对代码中的查询模块做一些相应改动,便能有效解决数据量大的问题。

  • 使用缓存

使用Redis作为MySQL的前置缓存,可以挡住大部分的查询请求,并降低响应时延。

缓存对商品系统这类与用户关系不大的系统效果特别好,但对订单系统而言,每个用户的订单数据都不一样,缓存命中率不算高,效果不是太好。

  • 读写分离

主库负责执行数据更新请求,然后将数据变更实时同步到所有从库,用多个从库来分担查询请求。

但订单数据的更新操作较多,下单高峰时主库的压力依然没有得到解决。且存在主从同步延迟,正常情况下延迟非常小,不超过1ms,但也会导致在某一个时刻的主从数据不一致。

那就需要对所有受影响的业务场景进行兼容处理,可能会做一些妥协,比如下单成功后先跳转到一个下单成功页,用户手动点击查看订单后才能看到这笔订单。

  • 分库

分库又包含垂直分库和水平分库。

**① 水平分库:**把同一个表的数据按一定规则拆到不同的数据库中,每个库可以放在不同的服务器上。

**② 垂直分库:**按照业务将表进行分类,分布到不同的数据库上面,每个库可以放在不同的服务器上,它的核心理念是专库专用。

  • 分表

分表又包含垂直分表和水平分表。

**① 水平分表:**在同一个数据库内,把一个表的数据按一定规则拆到多个表中。

**② 垂直分表:**将一个表按照字段分成多表,每个表存储其中一部分字段。

我们综合考虑了改造成本、效果和对现有业务的影响,决定直接使用最后一招:分库分表

3.2 分库分表技术选型

分库分表的技术选型主要从这几个方向考虑:

  1. 客户端sdk开源方案
  2. 中间件proxy开源方案
  3. 公司中间件团队提供的自研框架
  4. 自己动手造轮子

参考之前项目经验,并与公司中间件团队沟通后,采用了开源的 Sharding-JDBC 方案。现已更名为Sharding-Sphere。

  • Github:github.com/sharding-sp…
  • 文档:官方文档比较粗糙,但是网上资料、源码解析、demo比较丰富
  • 社区:活跃
  • 特点:jar包方式提供,属于client端分片,支持xa事务

3.2.1 分库分表策略

结合业务特性,选取用户标识作为分片键,通过计算用户标识的哈希值再取模来得到用户订单数据的库表编号.

假设共有n个库,每个库有m张表,

则库表编号的计算方式为:

  • 库序号:Hash(userId) / m % n
  • 表序号:Hash(userId) % m

路由过程如下图所示:

3.2.2 分库分表的局限性和应对方案

分库分表解决了数据量和并发问题,但它会极大限制数据库的查询能力,有一些之前很简单的关联查询,在分库分表之后可能就没法实现了,那就需要单独对这些Sharding-JDBC不支持的SQL进行改写。

除此之外,还遇到了这些挑战:

(1)全局唯一ID设计

分库分表后,数据库自增主键不再全局唯一,不能作为订单号来使用,但很多内部系统间的交互接口只有订单号,没有用户标识这个分片键,如何用订单号来找到对应的库表呢?

原来,我们在生成订单号时,就将库表编号隐含在其中了。这样就能在没有用户标识的场景下,从订单号中获取库表编号。

(2)历史订单号没有隐含库表信息

用一张表单独存储历史订单号和用户标识的映射关系,随着时间推移,这些订单逐渐不在系统间交互,就慢慢不再被用到。

(3)管理后台需要根据各种筛选条件,分页查询所有满足条件的订单

将订单数据冗余存储在搜索引擎Elasticsearch中,仅用于后台查询。

3.3 怎么做 MySQL 到 ES 的数据同步

上面说到为了便于管理后台的查询,我们将订单数据冗余存储在Elasticsearch中,那么,如何在MySQL的订单数据变更后,同步到ES中呢?

这里要考虑的是数据同步的时效性和一致性、对业务代码侵入小、不影响服务本身的性能等。

  • MQ方案

ES更新服务作为消费者,接收订单变更MQ消息后对ES进行更新

  • Binlog方案

ES更新服务借助canal等开源项目,把自己伪装成MySQL的从节点,接收Binlog并解析得到实时的数据变更信息,然后根据这个变更信息去更新ES。

其中BinLog方案比较通用,但实现起来也较为复杂,我们最终选用的是MQ方案。

因为ES数据只在管理后台使用,对数据可靠性和同步实时性的要求不是特别高。

考虑到宕机和消息丢失等极端情况,在后台增加了按某些条件手动同步ES数据的功能来进行补偿。

3.4 如何安全地更换数据库

如何将数据从原来的单实例数据库迁移到新的数据库集群,也是一大技术挑战

不但要确保数据的正确性,还要保证每执行一个步骤后,一旦出现问题,能快速地回滚到上一个步骤。

我们考虑了停机迁移和不停机迁移的两种方案:

(1)不停机迁移方案:

  • 把旧库的数据复制到新库中,上线一个同步程序,使用 Binlog等方案实时同步旧库数据到新库。
  • 上线双写订单新旧库服务,只读写旧库。
  • 开启双写,同时停止同步程序,开启对比补偿程序,确保新库数据和旧库一致。
  • 逐步将读请求切到新库上。
  • 读写都切换到新库上,对比补偿程序确保旧库数据和新库一致。
  • 下线旧库,下线订单双写功能,下线同步程序和对比补偿程序。

(2)停机迁移方案:

  • 上线新订单系统,执行迁移程序将两个月之前的订单同步到新库,并对数据进行稽核。
  • 将商城V1应用停机,确保旧库数据不再变化。
  • 执行迁移程序,将第一步未迁移的订单同步到新库并进行稽核。
  • 上线商城V2应用,开始测试验证,如果失败则回退到商城V1应用(新订单系统有双写旧库的开关)。

考虑到不停机方案的改造成本较高,而夜间停机方案的业务损失并不大,最终选用的是停机迁移方案。

3.5 分布式事务问题

电商的交易流程中,分布式事务是一个经典问题,比如:

  • 用户支付成功后,需要通知发货系统给用户发货。
  • 用户确认收货后,需要通知积分系统给用户发放购物奖励的积分。

我们是如何保证微服务架构下数据的一致性呢?

不同业务场景对数据一致性的要求不同,业界的主流方案中,用于解决强一致性的有两阶段提交(2PC)、三阶段提交(3PC),解决最终一致性的有TCC、本地消息、事务消息和最大努力通知等。

这里不对上述方案进行详细的描述,介绍一下我们正在使用的本地消息表方案:在本地事务中将要执行的异步操作记录在消息表中,如果执行失败,可以通过定时任务来补偿。

下图以订单完成后通知积分系统赠送积分为例。

3.6 系统安全和稳定性

  • 网络隔离

只有极少数第三方接口可通过外网访问,且都会验证签名,内部系统交互使用内网域名和RPC接口。

  • 并发锁

任何订单更新操作之前,会通过数据库行级锁加以限制,防止出现并发更新。

  • 幂等性

所有接口均具备幂等性,不用担心对方网络超时重试所造成的影响。

  • 熔断

使用Hystrix组件,对外部系统的实时调用添加熔断保护,防止某个系统故障的影响扩大到整个分布式系统中。

  • 监控和告警

通过配置日志平台的错误日志报警、调用链的服务分析告警,再加上公司各中间件和基础组件的监控告警功能,让我们能够能够第一时间发现系统异常。

3.7 踩过的坑

采用MQ消费的方式同步数据库的订单相关数据到ES中,遇到的写入数据不是订单最新数据问题

下图左边是原方案:

在消费订单数据同步的MQ时,如果线程A在先执行,查出数据,这时候订单数据被更新了,线程B开始执行同步操作,查出订单数据后先于线程A一步写入ES中,线程A执行写入时就会将线程B写入的数据覆盖,导致ES中的订单数据不是最新的。

解决方案是在查询订单数据时加行锁,整个业务执行在事务中,执行完成后再执行下一个线程。

sharding-jdbc 分组后排序分页查询出所有数据问题

示例:select a from temp group by a,b order by a desc limit 1,10。

执行是Sharding-jdbc里group by 和 order by 字段和顺序不一致是将10置为Integer.MAX_VALUE, 导致分页查询失效。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
java复制代码io.shardingsphere.core.routing.router.sharding.ParsingSQLRouter#processLimit

private void processLimit(final List<Object> parameters, final SelectStatement selectStatement, final boolean isSingleRouting) {
boolean isNeedFetchAll = (!selectStatement.getGroupByItems().isEmpty() || !selectStatement.getAggregationSelectItems().isEmpty()) && !selectStatement.isSameGroupByAndOrderByItems();
selectStatement.getLimit().processParameters(parameters, isNeedFetchAll, databaseType, isSingleRouting);
}

io.shardingsphere.core.parsing.parser.context.limit.Limit#processParameters

/**
* Fill parameters for rewrite limit.
*
* @param parameters parameters
* @param isFetchAll is fetch all data or not
* @param databaseType database type
* @param isSingleRouting is single routing or not
*/
public void processParameters(final List<Object> parameters, final boolean isFetchAll, final DatabaseType databaseType, final boolean isSingleRouting) {
fill(parameters);
rewrite(parameters, isFetchAll, databaseType, isSingleRouting);
}


private void rewrite(final List<Object> parameters, final boolean isFetchAll, final DatabaseType databaseType, final boolean isSingleRouting) {
int rewriteOffset = 0;
int rewriteRowCount;
if (isFetchAll) {
rewriteRowCount = Integer.MAX_VALUE;
} else if (isNeedRewriteRowCount(databaseType) && !isSingleRouting) {
rewriteRowCount = null == rowCount ? -1 : getOffsetValue() + rowCount.getValue();
} else {
rewriteRowCount = rowCount.getValue();
}
if (null != offset && offset.getIndex() > -1 && !isSingleRouting) {
parameters.set(offset.getIndex(), rewriteOffset);
}
if (null != rowCount && rowCount.getIndex() > -1) {
parameters.set(rowCount.getIndex(), rewriteRowCount);
}
}

)正确的写法应该是 select a from temp group by a desc ,b limit 1,10 ; 使用的版本是sharing-jdbc的3.1.1。

ES分页查询如果排序字段存在重复的值,最好加一个唯一的字段作为第二排序条件,避免分页查询时漏掉数据、查出重复数据,比如用的是订单创建时间作为唯一排序条件,同一时间如果存在很多数据,就会导致查询的订单存在遗漏或重复,需要增加一个唯一值作为第二排序条件或者直接使用唯一值作为排序条件。

四、成果

  • 一次性上线成功,稳定运行了一年多
  • 核心服务性能提升十倍以上
  • 系统解耦,迭代效率大幅提升
  • 能够支撑商城至少五年的高速发展

五、结语

我们在系统设计时并没有一味追求前沿技术和思想,面对问题时也不是直接采用主流电商的解决方案,而是根据业务实际状况来选取最合适的办法。

个人觉得,一个好的系统不是在一开始就被大牛设计出来的,一定是随着业务的发展和演进逐渐被迭代出来的,持续预判业务发展方向,提前制定架构演进方案,简单来说就是:走到业务的前面去!

作者:vivo官网商城开发团队

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

MySQL - 定时数据备份

发表于 2020-12-28

数据备份真的很重要, 因为可能有一天数据会被莫名其妙的删掉了.

本文将介绍如何在 CentOS 7 上使用 crontab 和 mysqldump 定时备份 mysql 的数据.

更换阿里源

1
2
3
4
5
6
7
8
9
bash复制代码# 备份 repo 文件
cp /etc/yum.repos.d/CentOS-Base.repo /etc/yum.repos.d/CentOS-Base.repo.bak

# 下载
wget -O /etc/yum.repos.d/CentOS-Base.repo http://mirrors.aliyun.com/repo/Centos-7.repo

yum clean all # 清除缓存
yum makecache # 生成缓存
yum update

安装 mysqldump

1
bash复制代码yum install -y mariadb

定时任务

由于 MySQL 是运行在 Docker 容器内的, 所以本地是不能使用 socket 进行连接的, 需要使用参数 -h127.0.0.1.

1
2
cron复制代码# 定时任务中的 % 需要使用斜杆进行转义
0 1 * * * mysqldump -uzhangsan -ppassword -h127.0.0.1 dbName > /data/dbName_`date '+\%Y\%m\%d\%H\%M\%S'`.sql 2>&1

原文链接: goworker.cn/posts/mysql…

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

SSH客户端常用工具SecureCRT操作

发表于 2020-12-27

1.1 SecureCRT工具介绍

SecureCRT是一款支持SSH(SSH1和SSH2)协议的终端仿真软件,常被用来
运行于Windows下远程登录UNIX或Linux服务器。

SecureCRT软件功能强大,不仅仅支持SSH协议,同时还支持Telnet、RLogin、Serial和TAPI等协议,它有非常多的功能,这里就不一一介绍了,常用功能可见下文介绍。

与SecureCRT类似功能的SSH软件还有Xshell、Putty等。SecureCRT、Xshell、Putty等都仅仅是客户端软件,一般用于Windows客户端计算机,因此,无论选择哪款客户端SSH工具都是可以的。

1.2 SecureCRT工具安装

由于是在Windows下安装,安装步骤简单,按提示一路下一步就可以安装完成。这提供SecureCRT5.1.3下载地址。

链接:pan.baidu.com/s/1c2OwWqW 密码:qe4e。

注意:SecureCRT需要注册之后才能使用 具体注册码在下载文件的 说明.txt

1.3配置SecureCRT连接Linux主机

①安装SecureCRT之后打开,并且单击快速链接按钮如图1.1所示
这里写图片描述

②选在协议SSH2,然后填入Linux主机IP,端口号,用户名
这里写图片描述

③这里写图片描述
以上三步骤如果顺利的话就可以连接到LInux主机

1.4调整SecureCRT终端显示和回滚缓冲区大小

为了更加方便的使用SecureCRT,首先得对终端进行一些调整,步骤如下:
通过SecureCRT顶端菜单中的“选项”→“会话选项”,打开会话选项窗口,然后单击左侧菜单“终端”→“仿真”,并勾选右边的“ANSI颜色”,再到终端右边选择“Linux”,最后设置缓冲区大小,单击“确定”完成设置

这里写图片描述

具体调整参数如下

① 调整“终端”→“仿真”的ANSI颜色,并且最终将终端选为Linux(也可以选择Xterm)目的是让Linux命令行看起来更舒服,如果是开发Shell、Python程序,更有利于代码展示。

② 调整回滚区的目的是为了当操作内容过多时,想看操作过得记录,可以向上翻得更远一些

1.5调整字体及光标颜色

通过SecureCRT顶端菜单中的“选项”→“会话选项”,打开会话选项窗口,然后单击左侧菜单“终端”→“外观”

这里写图片描述

在上图中单击“颜色”,在基本颜色出选择光标颜色,这儿选择绿色,利于保护眼睛
这里写图片描述

1.6 配置记录SSH操作日志及输出

设置在SecureCRT中记录执行命令及屏幕输出日志,可以更加方便的查看过去操作过的配置、命令及结果输出(日志文件格式可以设置为%H_%Y%M%D.log,这样设置之后,日志文件名将以主机IP_年、月、日的形式记录)。

具体设置如下图

这里写图片描述
配置说明
① 在上图中一定要勾选“在连接上启动记录”,如果不勾选则不会记录

② 要勾选“追加到文件”

1.7 配置本地机器上传和下载目录

在会话管理界面,单击左侧菜单“Xmodem/Zmodem”,然后可在右侧目录中设置上传和下载目录(上传和下载目录可以是同一个,但必须是系统中有的目录)

这里写图片描述

设置完毕之后就可以通过SecureCRT的命令行实现客户端计算机和Linux主机的文件上传。

下面是相关上传和下载命令的使用
① rz、sz命令的安装

*安装 系统时选择包含rz、sz命令包组、即(Dial-up networking Support)

*安装系统之后执行
yum install lrzsz -y

或yum groupinstall"Dial-up Networking Support"-y 命令来安装

②上传命令rz

上传文件时,执行rz命令,如果希望是覆盖服务器上的同文件内容,可以加-y参数,输入rz -y 后回车,会打开一个上传文件的窗口,然后选择文件上传即可

③下载命令sz

下载文件时,执行sz filename,如果希望覆盖本地同名文件,则可以输入-y参数控制sz -y filename,默认下载地址即为刚才配置的下载路径

④使用rz、sz命令注意事项

  • 只能上传和下载文件,不能上传和下载目录,如果是上传和下载目录需要打包成文件之后再传
  • 上传文件可以是计算机里任意文件、下载文件会下载到SecureCRT配置的对应路径
  • 执行rz命令按钮回车后出现的窗口中,一定不要勾最下方的“以ASCII方式上传文件”,否则会遇到问题
    这里写图片描述

1.8 调整命令行颜色方案(目录和注释)

默认情况下命令行界面目录和文件内容注释都是深蓝色的,看不清楚,这时候就需要我们调整颜色

“选项”→“全局选项”,然后在全局选项左侧单击“ANSI颜色”,右侧就出现相应颜色,将右边颜色选为浅蓝色

这里写图片描述

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

想把Java代码写的更漂亮么?了解一下Try吧

发表于 2020-12-27

原创:小姐姐味道(微信公众号ID:xjjdog),欢迎分享,转载请保留出处。任何不保留此声明的转载都是抄袭。

Java的Optional非常好用。我们一般使用Optional做非空处理,省去if的处理。主要的目的,就是为了解决Java中臭名昭著的空指针异常。

比如我们在平常的编码中,经常遇到对输入参数的非空判断。

1
2
3
4
5
6
java复制代码public void getXXX(Map<String, String> params) {
Map<String, String> map = params;
if (map == params) {
map = new HashMap<>();
}
}

这种代码一多,我们的程序就会慢慢变成shit mountain。这个时候就可以使用Optional进行改造。

1
2
3
java复制代码public void getXXX(Map<String, String> params) {
Map<String, String> map = Optional.ofNullable(params).orElse(new HashMap<>());
}

代码行数少了,逻辑清晰,同时自己的绩效也降低了 :)。

  1. 复杂例子

看一个比较复杂的例子。

假如我们需要的数据层次比较深。

1
java复制代码String cityCode = customer.getAddress().getCity().getCityCode().substring(0,3);

这样获取是不合理的,因为其中的某一环,可能是空,会抛出空指针的。所以,我们需要一层层的进行判断。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码public void getCityCode(Customer customer) {
String cityCode = "000";
if (customer != null) {
Address address = customer.getAddress();
if (null != address) {
City city = address.getCity();
if (city != null) {
String code = city.getCityCode();
if (null != code && code.length() >= 3) {
cityCode = code.substring(0, 3);
}
}
}
}
System.out.println(cityCode);
}

使用Optional的lambda语法,我们可以把代码改成下面这样:

1
2
3
4
5
6
7
8
9
java复制代码public void getCityCode(Customer customer) {
String cityCode = Optional.ofNullable(customer)
.map(c -> c.getAddress())
.map(a -> a.getCity())
.map(c -> c.getCityCode())
.filter(s -> s.length() >= 3)
.map(s -> s.substring(0, 3))
.orElse("000");
}

代码是不是颜值很高?

颜值虽高,下面还是要点一些偏门的重点内容。

  1. Optional的隐秘内容

其实,早在Java8发布之前(2014),guava就有了类似的工具,但由于当时并没有lambda语法,所以只能做些简单的应用。

Guava的optional支持序列化,可以在RPC框架方法中返回,但是一般很少用。

Java的Optional却根本无法序列化。
为什么java8的Optional没有实现序列化,这里有个讨论,可以看看mail.openjdk.java.net/pipermail/j…

另外Java8比Guava多了ifPresent、map、 filter、 flatMap、 orElseThrow这些方法。鉴于现在使用Guava Optional的人越来越少,不提也罢。

Optional会对GC有一定压力,如果开发底层框架,还是慎重使用,netty就曾经过测试,最后放弃了Optional。

但我还是喜欢用。谁让国内大多数都是cruder呢?

  1. Try为何物?

长期使用使用Java编码的Javaer,在见了Scala、Kotlin一类的语言后,会有一种惊艳的感觉。但这些包实在是太大了,引入有一定的成本,只能眼巴巴的馋她们的身子。

但是,Java 标准库对函数式编程的 API 支持相对比较有限。有没有一种轻量级的方式,来增强我们的Java库呢?要是能和Lambda表达式结合起来,那就更妙了。Vavr就是这样一个简单的Jar包,让我们的代码,写起来更加流畅。

它的maven坐标是:

1
2
3
4
5
xml复制代码<dependency>
<groupId>io.vavr</groupId>
<artifactId>vavr</artifactId>
<version>0.10.3</version>
</dependency>

下面是一段伟大的睡眠排序法的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码public class SleepSort implements Runnable {
private int num;
public SleepSort(int num) {
this.num = num;
}
@Override
public void run() {
try {
Thread.sleep(num * 10);
} catch (Exception e) {
e.printStackTrace();
}
System.out.print(num + " ");
}
public static void main(String[] args) {
int[] nums = {5, 22, 10, 7, 59, 3, 16, 4, 11, 8, 14, 24, 27, 25, 26, 28, 23, 99};
Arrays.stream(nums).forEach(n->new Thread(new SleepSort(n)).start());
}
}

其中的Run部分,太多无用的信息,我们可以使用Try来改造。

我们可以简化为下面两行:

1
2
java复制代码Try.run(()->Thread.sleep(num*10))
.andThen(()->System.out.print(num + " "));

它支持非常多的方法,可以完成大多数后续的业务处理。比如,在onFailure方法里,加入对异常信息的日志记录。

而常见的jackson json的处理,可以简化成下面的代码:

1
java复制代码String json = Try.of(() -> objectMapper.writeValueAsString(str)).getOrElse("{}");

Try就是这么好用。最重要的是,vavr的大小只有800多kb。

  1. vavr的更多操作

vavr支持Tuple(元组)、Option、Try、Either、集合便捷操作、多元函数、柯里化方法(curring)等。

可以看一下vavr版本的if else。下面是四个分支的代码。里面这些奇怪的符号,证明它也只是语法糖。

1
2
3
4
5
6
7
8
java复制代码public String vavrMatch(String input) {
return Match(input).of(
Case($("a"), "a1"),
Case($("b"), "b2"),
Case($("c"), "c3"),
Case($(), "unknown")
);
}

再比如,你想要定义一个函数,而不是一个类,在Java中可以使用Function。但可惜的是,Java的Function只支持一个参数。

使用Vavr的Function,最多支持22个参数!

再比如,你想要在一个方法中,返回多个值。这个,在python中很容易实现,在Java中就不得不定义一个Class去接收。

元组,就可以支持多个返回值的组合。比如下面的代码:

1
2
3
4
5
6
7
8
java复制代码// (Java, 8)
Tuple2<String, Integer> java8 = Tuple.of("Java", 8);

// "Java"
String s = java8._1;

// 8
Integer i = java8._2;

vavr支持一次性返回8个值。

另外,还有lazy等小工具。比如延迟获取值。

1
2
3
4
5
java复制代码Lazy<Double> lazy = Lazy.of(Math::random);
lazy.isEvaluated(); // = false
lazy.get(); // = 0.123 (random generated)
lazy.isEvaluated(); // = true
lazy.get(); // = 0.123 (memoized)

这样的扩展方法有很多。但我最常用的,还是Try和元组。它让代码变的更加优雅,表达意图也更加清晰。

哦对了。resilience4j就重度使用了vavr,就是那个Hystrix不再更新之后,官方推荐的那个熔断组件。

作者简介:小姐姐味道 (xjjdog),一个不允许程序员走弯路的公众号。聚焦基础架构和Linux。十年架构,日百亿流量,与你探讨高并发世界,给你不一样的味道。我的个人微信xjjdog0,欢迎添加好友,​进一步交流。​

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

String s = new String("xyz")创建

发表于 2020-12-26

从面试题说起

1
java复制代码String s = new String("xyz"); 创建了几个实例?

这是一道很经典的面试题,在一本所谓的Java宝典上,我看到的“标准答案”是这样的:

1
java复制代码两个,一个堆区的“xyz”,一个栈区指向“xyz”的s。

这个所谓的“标准答案”槽点太多,后面我们慢慢分析。

虽然答案很离谱,但是我觉得这个问题本身也不具有什么意义,因为问题没有既定义“创建”的具体含义,又没有指定“创建”的时间,是运行时吗?包不包括类加载的时候?有没有上下文代码语境?也没有定义实例是指什么实例,是指Java实例吗?还是单指String实例?包不包括JVM中的C++实例?

显然,这个问题是一个“有问题的问题”。这个答案也是一个“有问题的答案”。

String结构

在分析之前,为了方便后面画内存图,我们需要对Java中的String结构有一个大致了解:

image-20201224220819475

从上图可以看出,String类有三个属性:

  • value:char数组,用于用于存储字符。
  • hash:缓存字符串的哈希码,默认为0(String的hash值在真正调用hashCode方法的时候才会去计算)。
  • serialVersionUID:序列化用的。

正常的问题与合理的解释

在上面的题干上加上一些限定词,可以得到一个新的问题:

1
java复制代码String s = new String("xyz");创建几个String实例?

对于这个问题,在网上能找到一些比较高赞的答案:

1
2
3
4
5
java复制代码两个。
一个是字符串字面量"xyz"所对应的、存在于全局共享的常量池中的实例,
另一个是通过new String(String)创建并初始化的、内容(字符)与"xyz"相同的实例。
考虑到如果常量池中如果有这个字符串,就只会创建一个。
同时在栈区还会有一个对new出来的String实例的s。

考虑到了栈与堆,提到了常量池,我认为这已经达到大部分面试官对这个题目答案的期许了,或许这也是面试官想要考察的点。

但这个答案也仅是比较合理,并不完全正确。

首先,我不理解的是为什么很多答主总是用“常量池”来代替“字符串常量池”,在Java体系中,其实是有三个常量池的,三个常量池的概念和用处都不相同,混淆在一起容易给别人造成误解。

其次,就算答主说的“常量池”就是“字符串常量池”,可“字符串常量池”中存的是String实例的引用,而不是字符串,这是有很大区别的。而且这个答案是没有考虑代码执行的环境。

这些问题,下面都会一一分析。

分清变量和实例

我们先回到开头的问题与“标准答案” :

1
2
java复制代码问题:String s = new String("xyz"); 创建了几个实例?
答案:两个,一个堆区的“xyz”,一个栈区指向“xyz”的s

很明显写答案的人没有把变量和实例分清楚。在Java里,变量就是变量,类型的变量只是对某个对象实例或者null的,不是实例本身。声明变量的个数跟创建实例的个数没有必然关系。

举个例子:

1
2
3
4
java复制代码String s1 = "xyz";  
String s2 = s1.concat("");
String s3 = null;
new String(s1);

这段代码会涉及3个String类型的变量:

  • s1,指向下面String实例的1
  • s2,指向与s1相同
  • s3,值为null,不指向任何实例

以及3个String实例:

  • “xyz”字面量对应的驻留的字符串常量的String实例
  • “”字面量对应的驻留的字符串常量的String实例
  • 通过new String(String)创建的新String实例,没有任何变量指向它

类加载

对于String s = new String(“xyz”);创建几个String实例?这个问题。

似乎网上的所有答案都把类加载过程和实际执行过程合在一起分析的。

看起来好像是没有什么问题的,因为想要执行某个代码片段,其所在的类必然要被加载,而且对于同一个类加载器,最多加载一次。

但是我们看一下这段代码的字节码:

image-20201226155615626

似乎只出现了一次new java/lang/String,也就是只创建了一个String实例。也就是说原问题中的代码在每执行一次只会新创建一个String实例。 这里的ldc指令只是把先前在类加载过程中已经创建好的一个String实例(”xyz”)的一个引用压到操作数栈顶而已,并没有创建新的String实例。

不是应该有两个实例吗?还有一个String实例是在什么时候创建的呢?

我们都知道类加载的解析阶段是Java虚拟机将常量池内的符号引用替换为直接引用的过程,根据JVM规范,符合规范的JVM实现应该在类加载的过程中创建并驻留一个String实例作为常量来对应”xyz”字面量,具体是在类加载的解析阶段进行的。这个常量是全局共享的,只在先前尚未有内容相同的字符串驻留过的前提下才需要创建新的String实例。

所以你可以理解成,在类加载的解析阶段,其实已经创建了一个String实例,执行代码的时候,又new了一个String实例。当然,你把两者放在一起讨论并不会有什么问题。

JVM优化

以上讨论都只是针对规范所定义的Java语言与Java虚拟机而言。概念上是如此,但实际的JVM实现可以做得更优化,原问题中的代码片段有可能在实际执行的时候一个String实例也不会完整创建(没有分配空间)。

不结合上下文代码来看就直接说是“标准答案”就是耍流氓。

我们看下这段代码:

image-20201226130324887

运行这段代码,会不断的创建String对象吃内存,然后频繁的造成GC。

对于这个结论相信大家都没有意见,我们加上-XX:+PrintGC -XX:-DoEscapeAnalysis打印日志,关闭逃逸分析(JDK8默认开启此优化,我们先关闭)

image-20201226130324887

运行一下看看:

image-20201226130324887

结果确实如我们所料,不断的创建String对象吃内存导致频繁GC。

我们现在将-XX:-DoEscapeAnalysis改成-XX:+DoEscapeAnalysis,重新跑一下这段代码:

image-20201226130324887

神奇的事情发生了,继续跑下去也没有再打出GC日志了。难道新创建String对象都不吃内存了么?

实际情况是:经过HotSpot VM的的优化后,newString()方法不会新创建String实例了。这样自然不吃内存,也就不再触发GC了。

现在再来看开篇的那个问题,不结合具体情况,还能简单的说String s = new String(“xyz”);会创建两个String实例吗?

我只是举了一个逃逸分析的例子,HotSpot VM还有很多像这样的优化,比如方法内联、标量替换和无用代码削除。

klass-oop

如果题干上没有加上“Java”实例的定语,那JVM中的oop实例我们也不应该忽略。

为了后面能更好的说清楚这一点,需要补充一下klass-opp模型的知识。先做一个约定,全文只要涉及JVM具体实现的内容都是基于Jdk8中HotSpot VM展开的。

HotSpot VM是基于C++实现,而C++是一门面向对象的语言,本身是具备面向对象基本特征的,所以Java中的对象表示,最简单的做法是为每个Java类生成一个C++类与之对应。但HotSpot VM并没有这么做,而是设计了一套klass-oop模型。

klass,它是Java类的元信息在JVM中的存在形式。一个Java类被JVM类加载器加载之后,就是以klass的形式存在于JVM之中。

image-20201225180727729

oop,它是Java对象在JVM中的存在形式。每创建一个新的对象,在JVM内部就会相应地创建一个对应类型的OOP对象。

其中instanceOopDesc表示非数组对象,arrayOopDesc表示数组对象;

而objArrayOopDesc表示引用类型数组对象,typeArrayOopDesc表示基本类型数组对象。

举个例子:Java中String类的一个实例,在JVM中会有一个对应的instanceOopDesc实例。

image-20201225180727729

字符串常量池

在Java体系中,有三种常量池:

  • class字节码中的常量池:存在于硬盘上。主要存放两大类常量:字面量、符号引用。
  • 运行时常量池:方法区的一部分。我们常说的常量池,就是指这一块区域:方法区中的运行时常量池。
  • 字符串常量池:存在于堆区。这个常量池在JVM层面就是一个StringTable,只存储对java.lang.String实例的引用,而不存储String对象的内容。一般我们说一个字符串进入了字符串常量池其实是说在这个StringTable中保存了对它的引用,反之,如果说没有在其中就是说StringTable中没有对它的引用。

今天,我们要了解的是字符串常量池。

字符串常量池,即String Pool。在JVM中对应的类是StringTable,底层实现是一个Hashtable。利用的是哈希思想。

image-20201225180504262

下面这段代码,是往字符串常量池添加字符串方法。虽然是C++代码,但我相信学过Java的人都能看懂,至少也能明白这段代码干了什么事情。会通过String的内容+长度生成的hash值定位下标index,然后将Java的String类的实例对应的instanceOopDesc封装成HashtableEntry作为存储结构存储到常量池。

image-20201225190419815

补充完字符串常量池的知识之后,我们再回到文章开头的那一题:

String s = new String(“xyz”);创建了几个实例?

我们画一个内存图,图中省略了两个String对应的instanceOopDesc实例。

image-20201226115834730

不难得出答案:

1
2
3
4
5
arduino复制代码如果包括JVM中的C++实例的话,
有两个Java的String实例,
两个String实例对应的instanceOopDesc实例,
还有一个char[]数组对应的typeArrayOopDesc实例。
加一起一共是5个,也可以说2个String实例加上3个oop实例。

总结

String s = new String(“xyz”); 创建了几个实例?

通过以上的分析,我们会发现,每在这道题目的题干上每加一个定语,这道题目就会有不同的答案。

是否考虑类加载过程,是否考虑JVM优化,是否包括对应的oop实例等等等等,每个点都值得聊一聊的。

下次有人问你,你不妨把这篇的文章分享给他。

写在最后

为了写这一篇文章,我翻看了很多@RednaxelaFX前辈和周志明前辈的博客,过程中收益良多。在这里感谢前辈们为国内JVM的科普与发展做出的贡献!

还有一个很有趣的故事,我在查找“如何通过HSDB来了解String”相关资料的时候,看到一篇写的很好的文章,惊呼国内还有这么多低调的大神,后来添加了文章旁边的公众号,发现这个大神原来是PerfMa的创始人“寒泉子”李嘉鹏前辈,冒犯了冒犯了!

最后的最后

本人才疏学浅,文章难免会有纰漏,如果你发现了,欢迎提出,我会对其修正。

感谢您的阅读,您的点赞和留言都是对我的鼓励和支持。

如果你有什么想和我交流的,可以关注我的微信公众号“CoderW”,非常欢迎并感谢您的关注!


文中涉及代码:github.com/xiaoyingzhi…

JVM Spec Java SE 8Edition:docs.oracle.com/javase/spec…

参考文章:isfeasible.cn/posts/view/…

参考文章:www.iteye.com/blog/rednax…

参考文章:lovestblog.cn/blog/2014/0…

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

商城项目-分布式高级-08-购物车功能实现 购物车

发表于 2020-12-26

购物车

离线购物车

  • 离线的时候保存着用户没有登录时的购物车信息
  • 等用户登录后,离线购物车的内容自动合并到登录用户的购物车内
  • 离线购物车清空

vo封装

购物车的各个属性都需要计算

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
java复制代码@Data
public class Cart {
List<CartItem> items;
private Integer countNum; // 商品数量
private Integer countType; // 商品类型的个数
private BigDecimal totalAmount; // 当前购物车总价格
private BigDecimal reduce = new BigDecimal(0); // 优惠价格

public Integer getCountNum() {
int count = 0;
if (items != null && items.size() > 0) {
for (CartItem item : items) {
count += item.getCount();
}
}
setCountNum(count);
return count;
}

public void setCountNum(Integer countNum) {
this.countNum = countNum;
}

public Integer getCountType() {
int count = 0;
if (items != null && items.size() > 0) {
for (CartItem item : items) {
count += 1;
}
}
setCountType(count);
return countType;
}

public void setCountType(Integer countType) {
this.countType = countType;
}

public BigDecimal getTotalAmount() {
BigDecimal count = new BigDecimal(0);
if (items != null && items.size() > 0) {
for (CartItem item : items) {
count = count.add(item.getTotalPrice();
}
}
count = count.subtract(reduce);
setTotalAmount(count);
return totalAmount;
}

public void setTotalAmount(BigDecimal totalAmount) {
this.totalAmount = totalAmount;
}

public BigDecimal getReduce() {
return reduce;
}

public void setReduce(BigDecimal reduce) {
this.reduce = reduce;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码@Data
public class CartItem {

private Long skuId;
private Boolean check = true;
private String title;
private String image;
private List<String> skuAttr;
private BigDecimal price;
private Integer count;
private BigDecimal totalPrice;

public BigDecimal getTotalPrice() {
totalPrice = price.multiply(new BigDecimal(count));
return totalPrice;
}
}

拦截器判断用户是否登录(threadLocal)

  1. 拦截器判断用户是否登录
  2. 登录保存用户id
  3. 没登录保存用户user-key
  4. 保存用户信息,共享出去

拦截器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
java复制代码@Component
public class CartInterceptor implements HandlerInterceptor {

// 共享数据
public static ThreadLocal<UserInfo> userInfoLocal = new ThreadLocal<>();

/**
* 方法执行前
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
UserInfo userInfo = new UserInfo();

// 封装userInfo
HttpSession session = request.getSession();
MemberVo user = (MemberVo) session.getAttribute(AuthConstant.LOGIN_USER);
if (user != null) {
// 获取登录用户的购物车 -> userId
userInfo.setUserId(user.getId());
}
// 获取离线购物车 -> user-key
Cookie[] cookies = request.getCookies();
if (cookies != null && cookies.length > 0) {
for (Cookie cookie : cookies) {
if (cookie.getName().equals(CartConstant.User_COOKIE_NAME)) {
userInfo.setUserKey(cookie.getValue());
userInfo.setTemp(true);
break;
}
}
}
// 用户第一次登录分配一个随机的user-key
if (StringUtils.isBlank(userInfo.getUserKey())) {
userInfo.setUserKey(UUID.randomUUID().toString());
}
// 目标方法执行前
userInfoLocal.set(userInfo);
return true;
}

/**
* 方法执行后
*/
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
UserInfo userInfo = userInfoLocal.get();

// 如果是false就表明是第一次
if (!userInfo.isTemp()) {
Cookie cookie = new Cookie(CartConstant.User_COOKIE_NAME, userInfo.getUserKey());
cookie.setDomain("localhost");
cookie.setMaxAge(CartConstant.COOKIE_TTL);
response.addCookie(cookie);
}
}
}

注册拦截器

1
2
3
4
5
6
7
8
9
java复制代码@Configuration
public class WebConfig implements WebMvcConfigurer {

@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注册拦截器 -> 拦截所有请求
registry.addInterceptor(new CartInterceptor()).addPathPatterns("/**");
}
}

购物车功能(redis保存,异步编排)

controller方法

1
2
3
4
5
6
java复制代码@GetMapping("/addToCart")
public String addToCart(@RequestParam String skuId, @RequestParam Integer num, Model model) throws ExecutionException, InterruptedException {
CartItem cartItem = cartService.addToCart(skuId, num);
model.addAttribute("item", cartItem);
return "success";
}

service

运用了线程池以及异步编排

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
java复制代码@Override
public CartItem addToCart(String skuId, Integer num) throws ExecutionException, InterruptedException {
BoundHashOperations<String, Object, Object> ops = getCartOps();
CartItem cartItem;

// 判断这个商品在购物车中是否存在
Object o = ops.get(JSON.toJSONString(skuId)); // fix 保存格式为json 所以读取格式也要是json
if (Objects.isNull(o)) {
cartItem = new CartItem();
// 添加新商品:
// 1.查询当前要添加的商品信息
CompletableFuture<Void> getSkuInfoTask = CompletableFuture.runAsync(() -> {
R r = productFeignService.info(Long.parseLong(skuId)); // 远程调用
SkuInfoEntity info = BeanUtil.toBean(r.get("skuInfo"), SkuInfoEntity.class);
cartItem.setSkuId(info.getSkuId());
cartItem.setCheck(true);
cartItem.setTitle(info.getSkuTitle());
cartItem.setImage(info.getSkuDefaultImg());
cartItem.setPrice(info.getPrice());
cartItem.setCount(num);
cartItem.setTotalPrice(info.getPrice().multiply(new BigDecimal(num)));
}, thread);
// 2.查询属性信息
CompletableFuture<Void> getAttrTask = CompletableFuture.runAsync(() -> {
List<String> value = productFeignService.getSkuSaleAttrValue(skuId.toString()); // 远程调用
cartItem.setSkuAttr(value);
}, thread);

CompletableFuture.allOf(getAttrTask, getSkuInfoTask).get();
} else {
// 1.修改数量
cartItem = (CartItem) o;
cartItem.setCount(cartItem.getCount() + num);
cartItem.setTotalPrice(cartItem.getTotalPrice());
}
// 3.保存到redis中
ops.put(JSON.toJSONString(skuId), cartItem);

return cartItem;
}

获取购物车功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码private static final String cart_prefix = "cart:";

/**
* 获取购物车
*
* @return {@link BoundHashOperations<String, Object, Object>}
*/
private BoundHashOperations<String, Object, Object> getCartOps() {
UserInfo user = CartInterceptor.userInfoLocal.get();

// 1.生成redis中的key
StringBuilder cartKey = new StringBuilder(cart_prefix);
if (user.getUserId() != null) {
cartKey.append(user.getUserId());
} else {
cartKey.append(user.getUserKey());
}

BoundHashOperations<String, Object, Object> ops = redisTemplate.boundHashOps(cartKey.toString());
return ops;
}

功能测试

image-20201224190631928

image-20201224190658760

发送请求后:

image-20201224190715301

image-20201224190727885

解决页面刷新,再次发送请求的问题

1
2
3
4
5
6
java复制代码@Override
public CartItem getCartItem(String skuId) {
BoundHashOperations<String, Object, Object> ops = getCartOps();
String s = (String) ops.get(JSON.toJSONString(skuId));
return JSON.parseObject(s, new TypeReference<CartItem>() {});
}

image-20201224203550749

增加用户登录后合并购物车功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码/**
* 购物车列表
* 浏览器有一个cookie:user-key,用来表示用户的身份
* 登录:按session
* 没有登录:user-key
* 第一次:创建user-key
*
* @return {@link String}
*/
@GetMapping("/cartList.html")
public String cartList(Model model) throws ExecutionException, InterruptedException {
// 获取当前登录用户的信息
Cart cart = cartService.getCart();
model.addAttribute("cart",cart);
return "cartList";
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
java复制代码@Override
public Cart getCart() throws ExecutionException, InterruptedException {
UserInfo user = CartInterceptor.userInfoLocal.get();
Cart cart = new Cart();

// 1.获取离线购物车
List<CartItem> items = getCartItems(cart_prefix+user.getUserKey());
// 判断离线购物车中是否有内容
if (items != null && items.size() > 0) {
// 2.获取登录购物车
Long userId = user.getUserId();
if (userId != null) {
// 3.用戶已经登录->合并购物车->清空离线购物车
for (CartItem cartItem : items) {
addItemToCart(cartItem.getSkuId().toString(),cartItem.getCount()); // 合并购物车
}
deleteCart(cart_prefix+ user.getUserKey()); // 清空离线购物车
items = getCartItems(cart_prefix + userId); // 获取合并后的购物车内容
}
}
cart.setItems(items);

return cart;
}

/**
* 删除购物车
*
* @param key user key
*/
private void deleteCart(String key) {
redisTemplate.delete(key);
}

/**
* 根据购物项的key,获取对应购物项
*
* @param key 关键
* @return {@link List<CartItem>}
*/
private List<CartItem> getCartItems(String key) {
BoundHashOperations<String, Object, Object> ops = redisTemplate.boundHashOps(key);
List<Object> values = ops.values();
if (values != null && values.size() > 0)
return values.stream()
.map(s -> (CartItem) s)
.collect(Collectors.toList());
return null;
}

修复用户登录后获取购物车失败

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
java复制代码@Override
public Cart getCart() throws ExecutionException, InterruptedException {
UserInfo user = CartInterceptor.userInfoLocal.get();
System.out.println(user);
Cart cart = new Cart();

// 1.获取离线购物车
List<CartItem> items = getCartItems(cart_prefix + user.getUserKey());
// 判断离线购物车中是否有内容

// 2.获取登录购物车
Long userId = user.getUserId();
if (userId != null) {
// 3.用戶已经登录->合并购物车->清空离线购物车
if (items != null && items.size() > 0) {
for (CartItem cartItem : items) {
addItemToCart(cartItem.getSkuId().toString(), cartItem.getCount()); // 合并购物车
}
}
deleteCart(cart_prefix + user.getUserKey()); // 清空离线购物车
items = getCartItems(cart_prefix + userId); // 获取合并后的购物车内容
}

cart.setItems(items);

return cart;
}

image-20201226174312259

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…747748749…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%