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

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


  • 首页

  • 归档

  • 搜索

MYSQL 八大优化方案 1、选取最适用的字段属性 2、使用

发表于 2021-11-18

关于数据库优化,网上有不少资料和方法,但是不少质量参差不齐,有些总结的不够到位,内容冗杂。

  偶尔发现了这篇文章,总结得很经典,文章流量也很大,所以拿到自己的总结文集中,积累优质文章,提升个人能力,希望对大家今后开发中也有帮助

1、选取最适用的字段属性

MySQL可以很好的支持大数据量的存取,但是一般说来,数据库中的表越小,在它上面执行的查询也就会越快。因此,在创建表的时候,为了获得更好的性能,我们可以将表中字段的宽度设得尽可能小。

例如,在定义邮政编码这个字段时,如果将其设置为CHAR(255),显然给数据库增加了不必要的空间,甚至使用VARCHAR这种类型也是多余的,因为CHAR(6)就可以很好的完成任务了。同样的,如果可以的话,我们应该使用MEDIUMINT而不是BIGIN来定义整型字段。

另外一个提高效率的方法是在可能的情况下,应该尽量把字段设置为NOT NULL,这样在将来执行查询的时候,数据库不用去比较NULL值。

对于某些文本字段,例如“省份”或者“性别”,我们可以将它们定义为ENUM类型。因为在MySQL中,ENUM类型被当作数值型数据来处理,而数值型数据被处理起来的速度要比文本类型快得多。这样,我们又可以提高数据库的性能。

2、使用连接(JOIN)来代替子查询(Sub-Queries)

MySQL从4.1开始支持SQL的子查询。这个技术可以使用SELECT语句来创建一个单列的查询结果,然后把这个结果作为过滤条件用在另一个查询中。例如,我们要将客户基本信息表中没有任何订单的客户删除掉,就可以利用子查询先从销售信息表中将所有发出订单的客户ID取出来,然后将结果传递给主查询,如下所示:

image.png
使用子查询可以一次性的完成很多逻辑上需要多个步骤才能完成的SQL操作,同时也可以避免事务或者表锁死,并且写起来也很容易。但是,有些情况下,子查询可以被更有效率的连接(JOIN)..替代。例如,假设我们要将所有没有订单记录的用户取出来,可以用下面这个查询完成:

image.png

如果使用连接(JOIN)..来完成这个查询工作,速度将会快很多。 尤其是当salesinfo表中对CustomerID建有索引的话,性能将会更好,查询如下:

image.png

连接(JOIN)..之所以更有效率一些,是因为MySQL不需要在内存中创建临时表来完成这个逻辑上的需要两个步骤的查询工作。

\

3、使用联合(UNION)来代替手动创建的临时表

MySQL从4.0的版本开始支持union查询,它可以把需要使用临时表的两条或更多的select查询合并的一个查询中。在客户端的查询会话结束的时候,临时表会被自动删除,从而保证数据库整齐、高效。使用union来创建查询的时候,我们只需要用UNION作为关键字把多个select语句连接起来就可以了,要注意的是所有select语句中的字段数目要想同。下面的例子就演示了一个使用UNION的查询。

image.png

4、事务

尽管我们可以使用子查询(Sub-Queries)、连接(JOIN)和联合(UNION)来创建各种各样的查询,但不是所有的数据库操作都可以只用一条或少数几条SQL语句就可以完成的。更多的时候是需要用到一系列的语句来完成某种工作。但是在这种情况下,当这个语句块中的某一条语句运行出错的时候,整个语句块的操作就会变得不确定起来。设想一下,要把某个数据同时插入两个相关联的表中,可能会出现这样的情况:第一个表中成功更新后,数据库突然出现意外状况,造成第二个表中的操作没有完成,这样,就会造成数据的不完整,甚至会破坏数据库中的数据。要避免这种情况,就应该使用事务,它的作用是:要么语句块中每条语句都操作成功,要么都失败。换句话说,就是可以保持数据库中数据的一致性和完整性。事物以BEGIN关键字开始,COMMIT关键字结束。在这之间的一条SQL操作失败,那么,ROLLBACK命令就可以把数据库恢复到BEGIN开始之前的状态。

image.png

事务的另一个重要作用是当多个用户同时使用相同的数据源时,它可以利用锁定数据库的方法来为用户提供一种安全的访问方式,这样可以保证用户的操作不被其它的用户所干扰。

5、锁定表

尽管事务是维护数据库完整性的一个非常好的方法,但却因为它的独占性,有时会影响数据库的性能,尤其是在很大的应用系统中。由于在事务执行的过程中,数据库将会被锁定,因此其它的用户请求只能暂时等待直到该事务结束。如果一个数据库系统只有少数几个用户来使用,事务造成的影响不会成为一个太大的问题;但假设有成千上万的用户同时访问一个数据库系统,例如访问一个电子商务网站,就会产生比较严重的响应延迟。

其实,有些情况下我们可以通过锁定表的方法来获得更好的性能。下面的例子就用锁定表的方法来完成前面一个例子中事务的功能。

image.png

这里,我们用一个select语句取出初始数据,通过一些计算,用update语句将新值更新到表中。包含有WRITE关键字的LOCKTABLE语句可以保证在UNLOCKTABLES命令被执行之前,不会有其它的访问来对inventory进行插入、更新或者删除的操作。

6、使用外键

锁定表的方法可以维护数据的完整性,但是它却不能保证数据的关联性。这个时候我们就可以使用外键。

例如,外键可以保证每一条销售记录都指向某一个存在的客户。在这里,外键可以把customerinfo表中的customerid映射到salesinfo表中customerid,任何一条没有合法customerid的记录都不会被更新或插入到salesinfo中。

image.png

注意例子中的参数“on delete cascade”。该参数保证当customerinfo表中的一条客户记录被删除的时候,salesinfo表中所有与该客户相关的记录也会被自动删除。如果要在MySQL中使用外键,一定要记住在创建表的时候将表的类型定义为事务安全表InnoDB类型。该类型不是MySQL表的默认类型。定义的方法是在CREATE TABLE语句中加上engine=INNODB。如例中所示。

7、使用索引

索引是提高数据库性能的常用方法,它可以令数据库服务器以比没有索引快得多的速度检索特定的行,尤其是在查询语句当中包含有MAX(),MIN()和ORDERBY这些命令的时候,性能提高更为明显。

那该对哪些字段建立索引呢?

一般说来,索引应建立在那些将用于JOIN,WHERE判断和ORDERBY排序的字段上。尽量不要对数据库中某个含有大量重复的值的字段建立索引。对于一个ENUM类型的字段来说,出现大量重复值是很有可能的情况

例如customerinfo中的“province”..字段,在这样的字段上建立索引将不会有什么帮助;相反,还有可能降低数据库的性能。我们在创建表的时候可以同时创建合适的索引,也可以使用ALTERTABLE或CREATEINDEX在以后创建索引。此外,MySQL从版本3.23.23开始支持全文索引和搜索。全文索引在MySQL中是一个FULLTEXT类型索引,但仅能用于MyISAM类型的表。对于一个大的数据库,将数据装载到一个没有FULLTEXT索引的表中,然后再使用ALTERTABLE或CREATEINDEX创建索引,将是非常快的。但如果将数据装载到一个已经有FULLTEXT索引的表中,执行过程将会非常慢。

8、优化的查询语句

绝大多数情况下,使用索引可以提高查询的速度,但如果SQL语句使用不恰当的话,索引将无法发挥它应有的作用。

下面是应该注意的几个方面。

a、 首先,最好是在相同类型的字段间进行比较的操作

在MySQL3.23版之前,这甚至是一个必须的条件。例如不能将一个建有索引的INT字段和BIGINT字段进行比较;但是作为特殊的情况,在CHAR类型的字段和VARCHAR类型字段的字段大小相同的时候,可以将它们进行比较。

b、 其次,在建有索引的字段上尽量不要使用函数进行操作

例如,在一个DATE类型的字段上使用YEAE()函数时,将会使索引不能发挥应有的作用。所以,下面的两个查询虽然返回的结果一样,但后者要比前者快得多。

c、第三,在搜索字符型字段时,我们有时会使用LIKE关键字和通配符,这种做法虽然简单,但却也是以牺牲系统性能为代价的

例如下面的查询将会比较表中的每一条记录

image.png

但是如果换用下面的查询,返回的结果一样,但速度就要快上很多:

image.png

最后,应该注意避免在查询中让MySQL进行自动类型转换,因为转换过程也会使索引变得不起作用。
以上就是这篇文章的全部内容了,想要了解更多Java相关知识的同学可以自行观看教学视频。欢迎大家评论私信互动,一起学习共同进步~

本文转载自: 掘金

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

基于Netty手写实现Apache Dubbo框架进阶篇(带

发表于 2021-11-18

「这是我参与11月更文挑战的第18天,活动详情查看:2021最后一次更文挑战」

file

阅读这篇文章之前,建议先阅读和这篇文章关联的内容。

1. 详细剖析分布式微服务架构下网络通信的底层实现原理(图解)

2. (年薪60W的技巧)工作了5年,你真的理解Netty以及为什么要用吗?(深度干货)

3. 深度解析Netty中的核心组件(图解+实例)

4. BAT面试必问细节:关于Netty中的ByteBuf详解

5. 通过大量实战案例分解Netty中是如何解决拆包黏包问题的?

6. 基于Netty实现自定义消息通信协议(协议设计及解析应用实战)

7. 全网最详细最齐全的序列化技术及深度解析与应用实战

8. 手把手教你基于Netty实现一个基础的RPC框架(通俗易懂)

在本篇文章中,我们继续围绕Netty手写实现RPC基础篇进行优化,主要引入几个点

  • 集成spring,实现注解驱动配置
  • 集成zookeeper,实现服务注册
  • 增加负载均衡实现

源代码,加「跟着Mic学架构」微信号,回复『rpc』获取。

增加注解驱动

主要涉及到的修改模块

  • netty-rpc-protocol
  • netty-rpc-provider

netty-rpc-protocol

当前模块主要修改的类如下。

image-20210908163139333

图7-1
下面针对netty-rpc-protocol模块的修改如下

增加注解驱动

这个注解的作用是用来指定某些服务为远程服务

1
2
3
4
5
6
java复制代码@Target(ElementType.TYPE)// Target说明了Annotation所修饰的对象范围, TYPE:用于描述类、接口(包括注解类型) 或enum声明
@Retention(RetentionPolicy.RUNTIME)// Reteniton的作用是定义被它所注解的注解保留多久,保留至运行时。所以我们可以通过反射去获取注解信息。
@Component
public @interface GpRemoteService {

}

SpringRpcProviderBean

这个类主要用来在启动NettyServer,以及保存bean的映射关系

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复制代码@Slf4j
public class SpringRpcProviderBean implements InitializingBean, BeanPostProcessor {

private final int serverPort;
private final String serverAddress;
public SpringRpcProviderBean(int serverPort) throws UnknownHostException {
this.serverPort = serverPort;
InetAddress address=InetAddress.getLocalHost();
this.serverAddress=address.getHostAddress();
}

@Override
public void afterPropertiesSet() throws Exception {
log.info("begin deploy Netty Server to host {},on port {}",this.serverAddress,this.serverPort);
new Thread(()->{
try {
new NettyServer(this.serverAddress,this.serverPort).startNettyServer();
} catch (Exception e) {
log.error("start Netty Server Occur Exception,",e);
e.printStackTrace();
}
}).start();
}

//bean实例化后调用
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if(bean.getClass().isAnnotationPresent(GpRemoteService.class)){ //针对存在该注解的服务进行发布
Method[] methods=bean.getClass().getDeclaredMethods();
for(Method method: methods){ //保存需要发布的bean的映射
String key=bean.getClass().getInterfaces()[0].getName()+"."+method.getName();
BeanMethod beanMethod=new BeanMethod();
beanMethod.setBean(bean);
beanMethod.setMethod(method);
Mediator.beanMethodMap.put(key,beanMethod);
}
}
return bean;
}
}

Mediator

主要管理bean以及调用

BeanMethod

1
2
3
4
5
java复制代码@Data
public class BeanMethod {
private Object bean;
private Method method;
}

Mediator

负责持有发布bean的管理,以及bean的反射调用

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
java复制代码public class Mediator {
public static Map<String,BeanMethod> beanMethodMap=new ConcurrentHashMap<>();

private volatile static Mediator instance=null;

private Mediator(){
}

public static Mediator getInstance(){
if(instance==null){
synchronized (Mediator.class){
if(instance==null){
instance=new Mediator();
}
}
}
return instance;
}
public Object processor(RpcRequest rpcRequest){
String key=rpcRequest.getClassName()+"."+rpcRequest.getMethodName();
BeanMethod beanMethod=beanMethodMap.get(key);
if(beanMethod==null){
return null;
}
Object bean=beanMethod.getBean();
Method method=beanMethod.getMethod();
try {
return method.invoke(bean,rpcRequest.getParams());
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
return null;
}
}

RpcServerProperties

定义配置属性

1
2
3
4
5
6
java复制代码@Data
@ConfigurationProperties(prefix = "gp.rpc")
public class RpcServerProperties {

private int servicePort;
}

RpcProviderAutoConfiguration

定义自动配置类

1
2
3
4
5
6
7
8
9
java复制代码@Configuration
@EnableConfigurationProperties(RpcServerProperties.class)
public class RpcProviderAutoConfiguration {

@Bean
public SpringRpcProviderBean rpcProviderBean(RpcServerProperties rpcServerProperties) throws UnknownHostException {
return new SpringRpcProviderBean(rpcServerProperties.getServicePort());
}
}

修改RpcServerHandler

修改调用方式,直接使用Mediator的调用即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码public class RpcServerHandler extends SimpleChannelInboundHandler<RpcProtocol<RpcRequest>> {

@Override
protected void channelRead0(ChannelHandlerContext ctx, RpcProtocol<RpcRequest> msg) throws Exception {
RpcProtocol resProtocol=new RpcProtocol<>();
Header header=msg.getHeader();
header.setReqType(ReqType.RESPONSE.code());
Object result=Mediator.getInstance().processor(msg.getContent()); //主要修改这个部分
resProtocol.setHeader(header);
RpcResponse response=new RpcResponse();
response.setData(result);
response.setMsg("success");
resProtocol.setContent(response);

ctx.writeAndFlush(resProtocol);
}
}

netty-rpc-provider

这个模块中主要修改两个部分

  • application.properties
  • NettyRpcProviderMain

NettyRpcProviderMain

1
2
3
4
5
6
7
8
9
java复制代码@ComponentScan(basePackages = {"com.example.spring.annotation","com.example.spring.service","com.example.service"})
@SpringBootApplication
public class NettyRpcProviderMain {

public static void main(String[] args) throws Exception {
SpringApplication.run(NettyRpcProviderMain.class, args);
//去掉原来的实例化部分
}
}

application.properties

增加一个配置属性。

1
java复制代码gp.rpc.servicePort=20880

UserServiceImpl

把当前服务发布出去。

1
2
3
4
5
6
7
8
9
java复制代码@GpRemoteService //表示将当前服务发布成远程服务
@Slf4j
public class UserServiceImpl implements IUserService {
@Override
public String saveUser(String name) {
log.info("begin saveUser:"+name);
return "Save User Success!";
}
}

修改客户端的注解驱动

客户端同样也需要通过注解的方式来引用服务,这样就能够彻底的屏蔽掉远程通信的细节内容,代码结构如图7-2所示

image-20210908180518683

图7-2
增加客户端注解


在netty-rpc-protocol模块的annotation目录下创建下面这个注解。

1
2
3
4
5
java复制代码@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@Autowired
public @interface GpRemoteReference {
}

SpringRpcReferenceBean

定义工厂Bean,用来构建远程通信的代理

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
java复制代码public class SpringRpcReferenceBean implements FactoryBean<Object> {

private Class<?> interfaceClass;
private Object object;
private String serviceAddress;
private int servicePort;

@Override
public Object getObject() throws Exception {
return object;
}

public void init(){
this.object= Proxy.newProxyInstance(this.interfaceClass.getClassLoader(),
new Class<?>[]{this.interfaceClass},
new RpcInvokerProxy(this.serviceAddress,this.servicePort));
}

@Override
public Class<?> getObjectType() {
return this.interfaceClass;
}

public void setInterfaceClass(Class<?> interfaceClass) {
this.interfaceClass = interfaceClass;
}

public void setServiceAddress(String serviceAddress) {
this.serviceAddress = serviceAddress;
}

public void setServicePort(int servicePort) {
this.servicePort = servicePort;
}
}

SpringRpcReferencePostProcessor

用来实现远程Bean的动态代理注入:

  • BeanClassLoaderAware: 获取Bean的类装载器
  • BeanFactoryPostProcessor:在spring容器加载了bean的定义文件之后,在bean实例化之前执行
  • ApplicationContextAware: 获取上下文对象ApplicationContenxt
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
java复制代码@Slf4j
public class SpringRpcReferencePostProcessor implements ApplicationContextAware, BeanClassLoaderAware, BeanFactoryPostProcessor {
private ApplicationContext context;
private ClassLoader classLoader;
private RpcClientProperties clientProperties;

public SpringRpcReferencePostProcessor(RpcClientProperties clientProperties) {
this.clientProperties = clientProperties;
}

//保存发布的引用bean信息
private final Map<String, BeanDefinition> rpcRefBeanDefinitions=new ConcurrentHashMap<>();

@Override
public void setBeanClassLoader(ClassLoader classLoader) {
this.classLoader=classLoader;
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.context=applicationContext;
}

@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
for (String beanDefinitionname:beanFactory.getBeanDefinitionNames()){
//遍历bean定义,然后获取到加载的bean,遍历这些bean中的字段,是否携带GpRemoteReference注解
//如果有,则需要构建一个动态代理实现
BeanDefinition beanDefinition=beanFactory.getBeanDefinition(beanDefinitionname);
String beanClassName=beanDefinition.getBeanClassName();
if(beanClassName!=null){
//和forName方法相同,内部就是直接调用的forName方法
Class<?> clazz=ClassUtils.resolveClassName(beanClassName,this.classLoader);
//针对当前类中的指定字段,动态创建一个Bean
ReflectionUtils.doWithFields(clazz,this::parseRpcReference);
}
}
//将@GpRemoteReference注解的bean,构建一个动态代理对象
BeanDefinitionRegistry registry=(BeanDefinitionRegistry)beanFactory;
this.rpcRefBeanDefinitions.forEach((beanName,beanDefinition)->{
if(context.containsBean(beanName)){
log.warn("SpringContext already register bean {}",beanName);
return;
}
//把动态创建的bean注册到容器中
registry.registerBeanDefinition(beanName,beanDefinition);
log.info("registered RpcReferenceBean {} success.",beanName);
});
}
private void parseRpcReference(Field field){
GpRemoteReference gpRemoteReference=AnnotationUtils.getAnnotation(field,GpRemoteReference.class);
if(gpRemoteReference!=null) {
BeanDefinitionBuilder builder=BeanDefinitionBuilder.genericBeanDefinition(SpringRpcReferenceBean.class);
builder.setInitMethodName(RpcConstant.INIT_METHOD_NAME);
builder.addPropertyValue("interfaceClass",field.getType());
builder.addPropertyValue("serviceAddress",clientProperties.getServiceAddress());
builder.addPropertyValue("servicePort",clientProperties.getServicePort());
BeanDefinition beanDefinition=builder.getBeanDefinition();
rpcRefBeanDefinitions.put(field.getName(),beanDefinition);
}
}
}

需要在RpcConstant常量中增加一个INIT_METHOD_NAME属性

1
2
3
4
5
6
7
8
java复制代码public class RpcConstant {
//header部分的总字节数
public final static int HEAD_TOTAL_LEN=16;
//魔数
public final static short MAGIC=0xca;

public static final String INIT_METHOD_NAME = "init";
}

RpcClientProperties

1
2
3
4
5
6
7
java复制代码@Data
public class RpcClientProperties {

private String serviceAddress="192.168.1.102";

private int servicePort=20880;
}

RpcRefernceAutoConfiguration

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码@Configuration
public class RpcRefernceAutoConfiguration implements EnvironmentAware{

@Bean
public SpringRpcReferencePostProcessor postProcessor(){
String address=environment.getProperty("gp.serviceAddress");
int port=Integer.parseInt(environment.getProperty("gp.servicePort"));
RpcClientProperties rc=new RpcClientProperties();
rc.setServiceAddress(address);
rc.setServicePort(port);
return new SpringRpcReferencePostProcessor(rc);
}

private Environment environment;

@Override
public void setEnvironment(Environment environment) {
this.environment=environment;
}
}

netty-rpc-consumer

修改netty-rpc-consumer模块

  • 把该模块变成一个spring boot项目
  • 增加web依赖
  • 添加测试类

image-20210908183814586

图7-3 netty-rpc-consumer模块
引入jar包依赖


1
2
3
4
5
6
7
8
xml复制代码<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

HelloController

1
2
3
4
5
6
7
8
9
10
11
java复制代码@RestController
public class HelloController {

@GpRemoteReference
private IUserService userService;

@GetMapping("/test")
public String test(){
return userService.saveUser("Mic");
}
}

NettyConsumerMain

1
2
3
4
5
6
7
java复制代码@ComponentScan(basePackages = {"com.example.spring.annotation","com.example.controller","com.example.spring.reference"})
@SpringBootApplication
public class NettyConsumerMain {
public static void main(String[] args) {
SpringApplication.run(NettyConsumerMain.class, args);
}
}

application.properties

1
2
properties复制代码gp.serviceAddress=192.168.1.102
servicePort.servicePort=20880

访问测试

  • 启动Netty-Rpc-Server
  • 启动Netty-Rpc-Consumer

如果启动过程没有任何问题,则可以访问HelloController来测试远程服务的访问。

引入注册中心

创建一个netty-rpc-registry模块,代码结构如图7-4所示。

image-20210909174008427

引入相关依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
xml复制代码<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-framework</artifactId>
<version>4.2.0</version>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>4.2.0</version>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-x-discovery</artifactId>
<version>4.2.0</version>
</dependency>

IRegistryService

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
java复制代码public interface IRegistryService {

/**
* 注册服务
* @param serviceInfo
* @throws Exception
*/
void register(ServiceInfo serviceInfo) throws Exception;

/**
* 取消注册
* @param serviceInfo
* @throws Exception
*/
void unRegister(ServiceInfo serviceInfo) throws Exception;

/**
* 动态发现服务
* @param serviceName
* @return
* @throws Exception
*/
ServiceInfo discovery(String serviceName) throws Exception;
}

ServiceInfo

1
2
3
4
5
6
java复制代码@Data
public class ServiceInfo {
private String serviceName;
private String serviceAddress;
private int servicePort;
}

ZookeeperRegistryService

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
java复制代码@Slf4j
public class ZookeeperRegistryService implements IRegistryService {

private static final String REGISTRY_PATH="/registry";
//Curator中提供的服务注册与发现的组件封装,它对此抽象出了ServiceInstance、
// ServiceProvider、ServiceDiscovery三个接口,通过它我们可以很轻易的实现Service Discovery
private final ServiceDiscovery<ServiceInfo> serviceDiscovery;

private ILoadBalance<ServiceInstance<ServiceInfo>> loadBalance;

public ZookeeperRegistryService(String registryAddress) throws Exception {
CuratorFramework client= CuratorFrameworkFactory
.newClient(registryAddress,new ExponentialBackoffRetry(1000,3));
JsonInstanceSerializer<ServiceInfo> serializer=new JsonInstanceSerializer<>(ServiceInfo.class);
this.serviceDiscovery= ServiceDiscoveryBuilder.builder(ServiceInfo.class)
.client(client)
.serializer(serializer)
.basePath(REGISTRY_PATH)
.build();
this.serviceDiscovery.start();
loadBalance=new RandomLoadBalance();
}

@Override
public void register(ServiceInfo serviceInfo) throws Exception {
log.info("开始注册服务,{}",serviceInfo);
ServiceInstance<ServiceInfo> serviceInstance=ServiceInstance
.<ServiceInfo>builder().name(serviceInfo.getServiceName())
.address(serviceInfo.getServiceAddress())
.port(serviceInfo.getServicePort())
.payload(serviceInfo)
.build();
serviceDiscovery.registerService(serviceInstance);
}

@Override
public void unRegister(ServiceInfo serviceInfo) throws Exception {
ServiceInstance<ServiceInfo> serviceInstance=ServiceInstance.<ServiceInfo>builder()
.name(serviceInfo.getServiceName())
.address(serviceInfo.getServiceAddress())
.port(serviceInfo.getServicePort())
.payload(serviceInfo)
.build();
serviceDiscovery.unregisterService(serviceInstance);
}

@Override
public ServiceInfo discovery(String serviceName) throws Exception {
Collection<ServiceInstance<ServiceInfo>> serviceInstances= serviceDiscovery
.queryForInstances(serviceName);
//通过负载均衡返回某个具体实例
ServiceInstance<ServiceInfo> serviceInstance=loadBalance.select((List<ServiceInstance<ServiceInfo>>)serviceInstances);
if(serviceInstance!=null){
return serviceInstance.getPayload();
}
return null;
}
}

引入负载均衡算法

由于服务端发现服务时可能有多个,所以需要用到负载均衡算法来实现

ILoadBalance

1
2
3
4
java复制代码public interface ILoadBalance<T> {

T select(List<T> servers);
}

AbstractLoadBalance

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码public abstract class AbstractLoadBanalce implements ILoadBalance<ServiceInstance<ServiceInfo>> {

@Override
public ServiceInstance<ServiceInfo> select(List<ServiceInstance<ServiceInfo>> servers){
if(servers==null||servers.size()==0){
return null;
}
if(servers.size()==1){
return servers.get(0);
}
return doSelect(servers);
}

protected abstract ServiceInstance<ServiceInfo> doSelect(List<ServiceInstance<ServiceInfo>> servers);
}

RandomLoadBalance

1
2
3
4
5
6
7
8
java复制代码public class RandomLoadBalance extends AbstractLoadBanalce {
@Override
protected ServiceInstance<ServiceInfo> doSelect(List<ServiceInstance<ServiceInfo>> servers) {
int length=servers.size();
Random random=new Random();
return servers.get(random.nextInt(length));
}
}

RegistryType

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
java复制代码public enum RegistryType {

ZOOKEEPER((byte)0),
EUREKA((byte)1);

private byte code;

RegistryType(byte code) {
this.code=code;
}

public byte code(){
return this.code;
}

public static RegistryType findByCode(byte code) {
for (RegistryType rt : RegistryType.values()) {
if (rt.code() == code) {
return rt;
}
}
return null;
}
}

RegistryFactory

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码public class RegistryFactory {

public static IRegistryService createRegistryService(String address,RegistryType registryType){
IRegistryService registryService=null;
try {
switch (registryType) {
case ZOOKEEPER:
registryService = new ZookeeperRegistryService(address);
break;
case EUREKA:
//TODO
break;
default:
registryService = new ZookeeperRegistryService(address);
break;
}
}catch (Exception e){
e.printStackTrace();
}
return registryService;
}
}

修改服务端增加服务注册

修改netty-rpc-protocol模块,加入注册中心的支持

SpringRpcProviderBean

按照下面case标注部分,表示要修改的内容

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
java复制代码@Slf4j
public class SpringRpcProviderBean implements InitializingBean, BeanPostProcessor {

private final int serverPort;
private final String serverAddress;
private final IRegistryService registryService; //修改部分,增加注册中心实现
public SpringRpcProviderBean(int serverPort,IRegistryService registryService) throws UnknownHostException {
this.serverPort = serverPort;
InetAddress address=InetAddress.getLocalHost();
this.serverAddress=address.getHostAddress();
this.registryService=registryService; //修改部分,增加注册中心实现
}

@Override
public void afterPropertiesSet() throws Exception {
log.info("begin deploy Netty Server to host {},on port {}",this.serverAddress,this.serverPort);
new Thread(()->{
try {
new NettyServer(this.serverAddress,this.serverPort).startNettyServer();
} catch (Exception e) {
log.error("start Netty Server Occur Exception,",e);
e.printStackTrace();
}
}).start();
}

@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if(bean.getClass().isAnnotationPresent(GpRemoteService.class)){ //针对存在该注解的服务进行发布
Method[] methods=bean.getClass().getDeclaredMethods();
for(Method method: methods){
String serviceName=bean.getClass().getInterfaces()[0].getName();
String key=serviceName+"."+method.getName();
BeanMethod beanMethod=new BeanMethod();
beanMethod.setBean(bean);
beanMethod.setMethod(method);
Mediator.beanMethodMap.put(key,beanMethod);
try {
//修改部分,增加注册中心实现
ServiceInfo serviceInfo = new ServiceInfo();
serviceInfo.setServiceAddress(this.serverAddress);
serviceInfo.setServicePort(this.serverPort);
serviceInfo.setServiceName(serviceName);
registryService.register(serviceInfo);//修改部分,增加注册中心实现
}catch (Exception e){
log.error("register service {} faild",serviceName,e);
}
}
}
return bean;
}
}

RpcServerProperties

修改RpcServerProperties,增加注册中心的配置

1
2
3
4
5
6
7
8
9
10
java复制代码@Data
@ConfigurationProperties(prefix = "gp.rpc")
public class RpcServerProperties {

private int servicePort;

private byte registerType;

private String registryAddress;
}

RpcProviderAutoConfiguration

增加注册中心的注入。

1
2
3
4
5
6
7
8
9
10
11
java复制代码@Configuration
@EnableConfigurationProperties(RpcServerProperties.class)
public class RpcProviderAutoConfiguration {

@Bean
public SpringRpcProviderBean rpcProviderBean(RpcServerProperties rpcServerProperties) throws UnknownHostException {
//添加注册中心
IRegistryService registryService=RegistryFactory.createRegistryService(rpcServerProperties.getRegistryAddress(), RegistryType.findByCode(rpcServerProperties.getRegisterType()));
return new SpringRpcProviderBean(rpcServerProperties.getServicePort(),registryService);
}
}

application.properties

修改netty-rpc-provider中的application.properties。

1
2
3
properties复制代码gp.rpc.servicePort=20880
gp.rpc.registerType=0
gp.rpc.registryAddress=192.168.221.128:2181

修改客户端,增加服务发现

客户端需要修改的地方较多,下面这些修改的代码,都是netty-rpc-protocol模块中的类。

RpcClientProperties

增加注册中心类型和注册中心地址的选项

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码@Data
public class RpcClientProperties {

private String serviceAddress="192.168.1.102";

private int servicePort=20880;

private byte registryType;

private String registryAddress;

}

修改NettyClient

原本是静态地址,现在修改成了从注册中心获取地址

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
java复制代码@Slf4j
public class NettyClient {
private final Bootstrap bootstrap;
private final EventLoopGroup eventLoopGroup=new NioEventLoopGroup();
/* private String serviceAddress;
private int servicePort;*/
public NettyClient(){
log.info("begin init NettyClient");
bootstrap=new Bootstrap();
bootstrap.group(eventLoopGroup)
.channel(NioSocketChannel.class)
.handler(new RpcClientInitializer());
/* this.serviceAddress=serviceAddress;
this.servicePort=servicePort;*/
}

public void sendRequest(RpcProtocol<RpcRequest> protocol, IRegistryService registryService) throws Exception {
ServiceInfo serviceInfo=registryService.discovery(protocol.getContent().getClassName());
ChannelFuture future=bootstrap.connect(serviceInfo.getServiceAddress(),serviceInfo.getServicePort()).sync();
future.addListener(listener->{
if(future.isSuccess()){
log.info("connect rpc server {} success.",serviceInfo.getServiceAddress());
}else{
log.error("connect rpc server {} failed .",serviceInfo.getServiceAddress());
future.cause().printStackTrace();
eventLoopGroup.shutdownGracefully();
}
});
log.info("begin transfer data");
future.channel().writeAndFlush(protocol);
}
}

修改RpcInvokerProxy

将静态ip和地址,修改成IRegistryService

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
java复制代码@Slf4j
public class RpcInvokerProxy implements InvocationHandler {

/* private String serviceAddress;
private int servicePort;*/

IRegistryService registryService;

public RpcInvokerProxy(IRegistryService registryService) {
/* this.serviceAddress = serviceAddress;
this.servicePort = servicePort;*/
this.registryService=registryService;
}

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
log.info("begin invoke target server");
//组装参数
RpcProtocol<RpcRequest> protocol=new RpcProtocol<>();
long requestId= RequestHolder.REQUEST_ID.incrementAndGet();
Header header=new Header(RpcConstant.MAGIC, SerialType.JSON_SERIAL.code(), ReqType.REQUEST.code(),requestId,0);
protocol.setHeader(header);
RpcRequest request=new RpcRequest();
request.setClassName(method.getDeclaringClass().getName());
request.setMethodName(method.getName());
request.setParameterTypes(method.getParameterTypes());
request.setParams(args);
protocol.setContent(request);
//发送请求
NettyClient nettyClient=new NettyClient();
//构建异步数据处理
RpcFuture<RpcResponse> future=new RpcFuture<>(new DefaultPromise<>(new DefaultEventLoop()));
RequestHolder.REQUEST_MAP.put(requestId,future);
nettyClient.sendRequest(protocol,this.registryService);
return future.getPromise().get().getData();
}
}

SpringRpcReferenceBean

修改引用bean,增加注册中心配置

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
java复制代码public class SpringRpcReferenceBean implements FactoryBean<Object> {

private Class<?> interfaceClass;
private Object object;
/* private String serviceAddress;
private int servicePort;*/
//修改增加注册中心
private byte registryType;
private String registryAddress;

@Override
public Object getObject() throws Exception {
return object;
}

public void init(){
//修改增加注册中心
IRegistryService registryService= RegistryFactory.createRegistryService(this.registryAddress, RegistryType.findByCode(this.registryType));
this.object= Proxy.newProxyInstance(this.interfaceClass.getClassLoader(),
new Class<?>[]{this.interfaceClass},
new RpcInvokerProxy(registryService));
}

@Override
public Class<?> getObjectType() {
return this.interfaceClass;
}

public void setInterfaceClass(Class<?> interfaceClass) {
this.interfaceClass = interfaceClass;
}

/* public void setServiceAddress(String serviceAddress) {
this.serviceAddress = serviceAddress;
}

public void setServicePort(int servicePort) {
this.servicePort = servicePort;
}*/

public void setRegistryType(byte registryType) {
this.registryType = registryType;
}

public void setRegistryAddress(String registryAddress) {
this.registryAddress = registryAddress;
}
}

SpringRpcReferencePostProcessor

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
java复制代码@Slf4j
public class SpringRpcReferencePostProcessor implements ApplicationContextAware, BeanClassLoaderAware, BeanFactoryPostProcessor {
private ApplicationContext context;
private ClassLoader classLoader;
private RpcClientProperties clientProperties;

public SpringRpcReferencePostProcessor(RpcClientProperties clientProperties) {
this.clientProperties = clientProperties;
}

//保存发布的引用bean信息
private final Map<String, BeanDefinition> rpcRefBeanDefinitions=new ConcurrentHashMap<>();

@Override
public void setBeanClassLoader(ClassLoader classLoader) {
this.classLoader=classLoader;
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.context=applicationContext;
}

@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
for (String beanDefinitionname:beanFactory.getBeanDefinitionNames()){
//遍历bean定义,然后获取到加载的bean,遍历这些bean中的字段,是否携带GpRemoteReference注解
//如果有,则需要构建一个动态代理实现
BeanDefinition beanDefinition=beanFactory.getBeanDefinition(beanDefinitionname);
String beanClassName=beanDefinition.getBeanClassName();
if(beanClassName!=null){
Class<?> clazz=ClassUtils.resolveClassName(beanClassName,this.classLoader);
ReflectionUtils.doWithFields(clazz,this::parseRpcReference);
}
}
//将@GpRemoteReference注解的bean,构建一个动态代理对象
BeanDefinitionRegistry registry=(BeanDefinitionRegistry)beanFactory;
this.rpcRefBeanDefinitions.forEach((beanName,beanDefinition)->{
if(context.containsBean(beanName)){
log.warn("SpringContext already register bean {}",beanName);
return;
}
registry.registerBeanDefinition(beanName,beanDefinition);
log.info("registered RpcReferenceBean {} success.",beanName);
});
}
private void parseRpcReference(Field field){
GpRemoteReference gpRemoteReference=AnnotationUtils.getAnnotation(field,GpRemoteReference.class);
if(gpRemoteReference!=null) {
BeanDefinitionBuilder builder=BeanDefinitionBuilder.genericBeanDefinition(SpringRpcReferenceBean.class);
builder.setInitMethodName(RpcConstant.INIT_METHOD_NAME);
builder.addPropertyValue("interfaceClass",field.getType());
/*builder.addPropertyValue("serviceAddress",clientProperties.getServiceAddress());
builder.addPropertyValue("servicePort",clientProperties.getServicePort());*/
builder.addPropertyValue("registryType",clientProperties.getRegistryType());
builder.addPropertyValue("registryAddress",clientProperties.getRegistryAddress());
BeanDefinition beanDefinition=builder.getBeanDefinition();
rpcRefBeanDefinitions.put(field.getName(),beanDefinition);
}
}
}

RpcRefernceAutoConfiguration

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码@Configuration
public class RpcRefernceAutoConfiguration implements EnvironmentAware{

@Bean
public SpringRpcReferencePostProcessor postProcessor(){
String address=environment.getProperty("gp.serviceAddress");
int port=Integer.parseInt(environment.getProperty("gp.servicePort"));
RpcClientProperties rc=new RpcClientProperties();
rc.setServiceAddress(address);
rc.setServicePort(port);
rc.setRegistryType(Byte.parseByte(environment.getProperty("gp.registryType")));
rc.setRegistryAddress(environment.getProperty("gp.registryAddress"));
return new SpringRpcReferencePostProcessor(rc);
}

private Environment environment;

@Override
public void setEnvironment(Environment environment) {
this.environment=environment;
}
}

application.properties

修改netty-rpc-consumer模块中的配置

1
2
3
4
5
properties复制代码gp.serviceAddress=192.168.1.102
gp.servicePort=20880

gp.registryType=0
gp.registryAddress=192.168.221.128:2181

负载均衡的测试

增加一个服务端的启动类,并且修改端口。然后客户端不需要重启的情况下刷新浏览器,即可看到负载均衡的效果。

image-20210909202149527

图7-5
需要源码的同学,请关注公众号[跟着Mic学架构],回复关键字[rpc],即可获得

版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Mic带你学架构!
如果本篇文章对您有帮助,还请帮忙点个关注和赞,您的坚持是我不断创作的动力。欢迎关注同名微信公众号获取更多技术干货!

本文转载自: 掘金

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

Go面试题:Go有引用变量和引用传递么?

发表于 2021-11-18

前言

在Go中如果使用过map和channel,就会发现把map和channel作为函数参数传递,不需要在函数形参里对map和channel加指针标记*就可以在函数体内改变外部map和channel的值。

这会给人一种错觉:map和channel难道是类似C++的引用变量,函数传参的时候使用的是引用传递?

比如下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
go复制代码// example1.go
package main
​
import "fmt"
​
func changeMap(data map[string]interface{}) {
data["c"] = 3
}
​
func main() {
counter := map[string]interface{}{"a": 1, "b": 2}
fmt.Println("begin:", counter)
changeMap(counter)
fmt.Println("after:", counter)
}

程序运行的结果是:

1
2
arduino复制代码begin: map[a:1 b:2]
after: map[a:1 b:2 c:3]

上面的例子里,函数changeMap改变了外部的map类型counter的值。

那map传参是使用的引用传递么?带着这个问题,我们先回顾下什么是引用变量和引用传递。

什么是引用变量(reference variable)和引用传递(pass-by-reference)

我们先回顾下C++里的引用变量和引用传递。看下面的例子:

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
c复制代码// example2.cpp
#include <iostream>
​
using namespace std;
​
/*函数changeValue使用引用传递*/
void changeValue(int &n) {
n = 2;
}
​
int main() {
int a = 1;
/*
b是引用变量,引用的是变量a
*/
int &b = a;
cout << "a=" << a << " address:" << &a << endl;
cout << "b=" << b << " address:" << &b << endl;
/*
调用changeValue会改变外部实参a的值
*/
changeValue(a);
cout << "a=" << a << " address:" << &a << endl;
cout << "b=" << b << " address:" << &b << endl;
}

程序的运行结果是:

1
2
3
4
ini复制代码a=1 address:0x7ffee7aa776c
b=1 address:0x7ffee7aa776c
a=2 address:0x7ffee7aa776c
b=2 address:0x7ffee7aa776c

在这个例子里,变量b是引用变量,引用的是变量a。引用变量就好比是原变量的一个别名,引用变量和引用传递的特点如下:

  • 引用变量和原变量的内存地址一样。就像上面的例子里引用变量b和原变量a的内存地址相同。
  • 函数使用引用传递,可以改变外部实参的值。就像上面的例子里,changeValue函数使用了引用传递,改变了外部实参a的值。
  • 对原变量的值的修改也会改变引用变量的值。就像上面的例子里,changeValue函数对a的修改,也改变了引用变量b的值。

Go有引用变量(reference variable)和引用传递(pass-by-reference)么?

先给出结论:Go语言里没有引用变量和引用传递。

在Go语言里,不可能有2个变量有相同的内存地址,也就不存在引用变量了。

注意:这里说的是不可能2个变量有相同的内存地址,但是2个变量指向同一个内存地址是可以的,这2个是不一样的。参考下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
go复制代码// example3.go
package main
​
import "fmt"
​
func main() {
a := 10
var p1 *int = &a
var p2 *int = &a
fmt.Println("p1 value:", p1, " address:", &p1)
fmt.Println("p2 value:", p2, " address:", &p2)
}

程序运行结果是:

1
2
less复制代码p1 value: 0xc0000ac008  address: 0xc0000ae018
p2 value: 0xc0000ac008 address: 0xc0000ae020

可以看出,变量p1和p2的值相同,都指向变量a的内存地址。但是变量p1和p2自己本身的内存地址是不一样的。而C++里的引用变量和原变量的内存地址是相同的。

因此,在Go语言里是不存在引用变量的,也就自然没有引用传递了。

有map不是使用引用传递的反例么

看下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
go复制代码// example4.go
package main
​
import "fmt"
​
func initMap(data map[string]int) {
data = make(map[string]int)
fmt.Println("in function initMap, data == nil:", data == nil)
}
​
func main() {
var data map[string]int
fmt.Println("before init, data == nil:", data == nil)
initMap(data)
fmt.Println("after init, data == nil:", data == nil)
}
​

大家可以先思考一会,想想程序运行结果是什么。

程序实际运行结果如下:

1
2
3
ini复制代码before init, data == nil: true
in function initMap, data == nil: false
after init, data == nil: true

可以看出,函数initMap并没有改变外部实参data的值,因此也证明了map并不是引用变量。

那问题来了,为啥map作为函数参数不是使用的引用传递,但是在本文最开头举的例子里,却可以改变外部实参的值呢?

map究竟是什么?

结论是:map变量是指向runtime.hmap的指针

当我们使用下面的代码初始化map的时候

1
go复制代码data := make(map[string]int)

Go编译器会把make调用转成对runtime.makemap的调用,我们来看看runtime.makemap的源代码实现。

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
scss复制代码298  // makemap implements Go map creation for make(map[k]v, hint).
299 // If the compiler has determined that the map or the first bucket
300 // can be created on the stack, h and/or bucket may be non-nil.
301 // If h != nil, the map can be created directly in h.
302 // If h.buckets != nil, bucket pointed to can be used as the first bucket.
303 func makemap(t *maptype, hint int, h *hmap) *hmap {
304 mem, overflow := math.MulUintptr(uintptr(hint), t.bucket.size)
305 if overflow || mem > maxAlloc {
306 hint = 0
307 }
308
309 // initialize Hmap
310 if h == nil {
311 h = new(hmap)
312 }
313 h.hash0 = fastrand()
314
315 // Find the size parameter B which will hold the requested # of elements.
316 // For hint < 0 overLoadFactor returns false since hint < bucketCnt.
317 B := uint8(0)
318 for overLoadFactor(hint, B) {
319 B++
320 }
321 h.B = B
322
323 // allocate initial hash table
324 // if B == 0, the buckets field is allocated lazily later (in mapassign)
325 // If hint is large zeroing this memory could take a while.
326 if h.B != 0 {
327 var nextOverflow *bmap
328 h.buckets, nextOverflow = makeBucketArray(t, h.B, nil)
329 if nextOverflow != nil {
330 h.extra = new(mapextra)
331 h.extra.nextOverflow = nextOverflow
332 }
333 }
334
335 return h
336 }

从上面的源代码可以看出,runtime.makemap返回的是一个指向runtime.hmap结构的指针。

我们也可以通过下面的例子,来验证map变量到底是不是指针。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
go复制代码// example5.go
package main
​
import (
"fmt"
"unsafe"
)
​
func main() {
data := make(map[string]int)
var p uintptr
fmt.Println("data size:", unsafe.Sizeof(data))
fmt.Println("pointer size:", unsafe.Sizeof(p))
}

程序运行结果是:

1
2
arduino复制代码data size: 8
pointer size: 8

map的size和指针的size一样,都是8个字节。

思考更为深入的读者,看到这里,可能还会有一个疑问:

既然map是指针,那为什么make()函数的说明里,有这么一句Unlike new, make’s return type is the same as the type of its argument, not a pointer to it.

The make built-in function allocates and initializes an object of type slice, map, or chan (only). Like new, the first argument is a type, not a value. Unlike new, make’s return type is the same as the type of its argument, not a pointer to it. The specification of the result depends on the type:

如果map是指针,那make返回的不应该是*map[string]int么,为啥官方文档里说的是not a pointer to it.

这里其实也有Go语言历史上的一个演变过程,看看Go作者之一Ian Taylor的说法:

In the very early days what we call maps now were written as pointers, so you wrote *map[int]int. We moved away from that when we realized that no one ever wrote map without writing *map. That simplified many things but it left this issue behind as a complication.

所以,在Go语言早期,的确对于map是使用过指针形式的,但是最后Go设计者们发现,几乎没有人使用map不加指针,因此就直接去掉了形式上的指针符号*。

总结

map和channel,本质上都是指针,指向Go runtime结构。带着这个思路,我们再回顾下之前讲过的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
go复制代码// example4.go
package main
​
import "fmt"
​
func initMap(data map[string]int) {
data = make(map[string]int)
fmt.Println("in function initMap, data == nil:", data == nil)
}
​
func main() {
var data map[string]int
fmt.Println("before init, data == nil:", data == nil)
initMap(data)
fmt.Println("after init, data == nil:", data == nil)
}

既然map是一个指针,因此在函数initMap里,

1
go复制代码data = make(map[string]int)

这一句等于把data这个指针,进行了重新赋值,函数内部的data指针不再指向外部实参data对应的runtime.hmap结构体的内存地址。

因此在函数体内对data的修改,并没有影响外部实参data以及data对应的runtime.hmap结构体的值。

程序实际运行结果如下:

1
2
3
ini复制代码before init, data == nil: true
in function initMap, data == nil: false
after init, data == nil: true

代码

相关代码和说明开源在GitHub:

github.com/jincheng9/g…

也可以搜索公众号:coding进阶,关注更多Go知识。

References

  • dave.cheney.net/2017/04/29/…

本文转载自: 掘金

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

RabbitMQ--死信队列/延迟队列--使用/原理

发表于 2021-11-18

简介
本文介绍RabbitMQ的死信队列和延迟队列。

本内容也是Java后端面试中常见的问题。

死信队列

简介

DLX,全称为Dead-Letter-Exchange,可以称之为死信交换器,也有人称之为死信邮箱。当消息在一个队列中变成死信(dead message)之后,它能被重新被发送到另一个交换器中,这个交换器就是DLX,绑定DLX的队列就称之为死信队列。

以下几种情况会导致消息变成死信:

消息被拒绝(Basic.Reject/Basic.Nack),并且设置requeue参数为false;

消息过期;

队列达到最大长度。

DLX是一个正常的交换器,和一般的交换器没有区别,它能在任何的队列上被指定,实际上就是设置某个队列的属性。当这个队列中存在死信时,RabbitMQ就会自动地将这个消息重新发布到设置的DLX上去,进而被路由到另一个队列,即死信队列。可以监听这个队列中的消息以进行相应的处理,这个特性与将消息的TTL设置为0配合使用可以弥补immediate参数的功能。

为队列添加DLX的方法

法1:代码方式

//创建 DLX: dlx_exchange
channel.exchangeDeclare(“dlx_exchange”, “direct” );
Map<String, Object> args = new HashMap<String, Object>;
args.put(“x-dead-letter-exchange”, “dlx_exchange”);
//为队列myqueue添加DLX
channel.queueDeclare(“myqueue”, false, false, false, args);
也可以为这个DLX指定路由键。(如果没有特殊指定,则使用原队列的路由键)

args.put(“x-dead-letter-routing-key”,”dlx-routing-key”);
法2:命令方式

rabbitmqctl set_policy DLX “.*“ ‘{“dead-letter-exchange”:”dlx_exchange”}’ –apply-to queues
示例
代码

channel.exchangeDeclare(“exchange.dlx”, “direct”, true);
channel.exchangeDeclare(“exchange.normal”, “fanout”, true);
Map<String, Object> args = new HashMap<String, Object>();
args.put(“x-message-ttl”, 10000);
args.put(“x-dead-letter-exchange” , “exchange.dlx”);
args.put(“x-dead-letter-routing-key” , “routingkey”);
channel.queueDeclare(“queue.normal” , true, false, false, args);
channel.queueBind(“queue.normal”, “exchange.normal”, “”);
channel.queueDeclare(“queue.dlx”, true, false, false, null);
channel.queueBind(“queue.dlx”, “exchange.dlx” , “routingkey”);
channel.basicPublish(“exchange.normal” , “rk”,
MessageProperties.PERSISTENT_TEXT_PLAIN, “dlx”.getBytes());

这里创建了两个交换器exchange.normal和exchange.dlx,分别绑定两个队列queue.normal和queue.dlx。

Web管理页面结果

由下图(图1-1)的Web管理页面可以看出,两个队列都被标记了“D”,这个是durable的缩写,即设置了队列持久化。queue.normal这个队列还配置了TTL、DLX和DLK,其中DLX指的是
x-dead-letter-routing-key这个属性。

36b18d126d114051a357515435fda9eb.png

案例分析

参考下图(图1-2),生产者首先发送一条携带路由键为“rk”的消息,然后经过交换器exchange.normal顺利地存储到队列queue.normal中。由于队列queue.normal设置了过期时间为10s,在这10s内没有消费者消费这条消息,那么判定这条消息为过期。由于设置了DLX,过期之时,消息被丢给交换器exchange.dlx中,这时找到与exchange.dlx匹配的队列queue.dlx,最后消息被存储在queue.dk这个死信队列中。

980dfbf70a4049379e8d5d83d54b7bd1.png

对于RabbitMQ来说,DLX是一个非常有用的特性。它可以处理异常情况下,消息不能够被消费者正确消费(消费者调用了Basic.Nack或者Basic.Reject)而被置入死信队列中
的情况,后续分析程序可以通过消费这个死信队列中的内容来分析当时所遇到的异常情况,进而可以改善和优化系统。DLX配合TTL使用还可以实现延迟队列的功能,详细请看下一节。

延迟队列

简介

延迟队列用来存放延迟消息。延迟消息:指当消息被发送以后,不想让消费者立刻拿到消息,而是等待特定时间后,消费者才能拿到这个消息进行消费。大数据培训

在AMQP协议中,或者RabbitMQ本身没有直接支持延迟队列的功能,但是有两种方案来间接实现:

方案1:采用rabbitmq-delayed-message-exchange 插件实现。(RabbitMQ 3.6.x开始支持)

方案2:通过前面所介绍的DLX和TTL模拟出延迟队列的功能。

在图1-2中,不仅展示的是死信队列的用法,也是延迟队列的用法,对于queue.dlx这个死信队列来说,同样可以看作延迟队列。假设一个应用中需要将每条消息都设置为10秒的延迟,
生产者通过exchange.normal这个交换器将发送的消息存储在queue.normal这个队列中。消费者订阅的并非是queue.normal这个队列,而是queue.dlx这个队列。当消息从queue.normal这个队列中过期之后被存入queue.dlx这个队列中,消费者就恰巧消费到了延迟10秒的这条消息。

在真实应用中,对于延迟队列可以根据延迟时间的长短分为多个等级,一般分为5秒、10秒、30秒、1分钟、5分钟、10分钟、30分钟、1小时这几个维度,当然也可以再细化一下。

以下图(图2-1)为例进行说明。为简化,只设置5秒、10秒、30秒、1分钟这四个等级。根据需求的不同,生产者发送消息的时候通过设置不同的路由键,将消息发送到与交换器绑定的不同的队列中。这里队列也分别配置了DLX和相应的死信队列,当相应的消息过期时,就会转存到相应的死信队列(即延迟队列)中,这样消费者根据业务自身的情况,分别选择不同延迟等级的延迟队列进行消费。

b8fcb275c07f4e33bfa80f82f63cb5d9.png

使用场景

延迟队列的使用场景有很多,比如:

用户下订单场景:用户下单后有30分钟的时间支付,若30分钟内没有支付,则将这个订单取消。
方案:用户下单后将取消订单的消息发送到延迟队列,延迟时间设置为30分钟。取消订单这个消息的订阅者程序在30分钟后收到消息,判断该订单的状态是否为已支付,若还没支付,则将该订单状态设置为:已取消。
定时遥控场景:用户想用手机远程遥控家里的智能设备在指定的时间工作。

方案:假设用户想要的操作是:开启热水器。首先,将开启热水器这个消息发送到延迟队列,延迟时间设置到用户想要的时间到现在时间的差值。开启热水器这个消息的订阅者程序在指定时间收到消息,再将指令推送到智能设备。

需要注意的是,延迟队列的消息是不能取消的,解决方案是:在消费消息的时候判断这个消息对应的业务的当前状态。例如:对于取消订单来说,收到消息时,读取这个消息所对应的数据库信息,如果已经是已付款状态了,就不进行任何操作了,如果是未支付状态,则改为已取消。

本文转载自: 掘金

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

JavaEE之HTML常见标签及个人简历制作 一、前言 二、

发表于 2021-11-18

这是我参与11月更文挑战的第16天,活动详情查看:2021最后一次更文挑战」 @TOC

一、前言

1、 HTML代码都是由各种各样的标签构成的。
2、HTML代码基础结构

1
2
3
4
5
6
7
8
9
10
11
12
html复制代码<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>

</body>
</html>

二、HTML的各种标签

2.1注释标签

1
html复制代码<!-- 注释 -->

在VSCODE中可以使用 ctrl+/的快捷键注释、取消注释。与Java不同的是,HTML的注释可以在开发者工具中看到。当打开一个网页的时候按F12就可以打开开发者工具了。
在这里插入图片描述

2.2标题标签

从h1,到h6,一共6个,数字越小,字体越大。来一起看看效果。
代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
html复制代码<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<h1>最大的一级标签</h1>
<h2>第二大的二级标签</h2>
<h3>第三大</h3>
<h4>第四大</h4>
<h5>老五</h5>
<h6>老弟</h6>
</body>
</html>

效果图:
在这里插入图片描述

2.3段落标签:P

顾名思义一个段落标签就代表一个段落。
假如我这里需要将一段很长的文字复制到HTML代码中。它本来是长这样的:
即是分段的。但是实际上是。。。

写作最好的老师,是写作本身, 老师最好的老师,是教学本身。

刘韧曾在2002年出版的《知识英雄2.0》结尾处反思,“我已经做了10年记者,在这10年中,与被采访对象的关系问题,一直是我需要认真回答的问题。”

20年后的今天,这个问题仍有继续追问的价值。

这里面包括两个问题,即采访对象为什么要接受记者采访?以及记者为什么要进行采访?大多数的人物采访和报道,无法达成预期,便是这两个问题没有想明白,弄清楚。

如何深度思考这两个问题?CSDN特邀《知识英雄2.0》作者刘韧先生,共建“CSDN刘韧写作班”,特招热爱写作的科技报道者,助其精进采访与写作之能力,提升持续自我进化之思维。

刘韧赞成费孝通说的:我并不认为教师的任务是在传授已有的知识,这些学生们自己可以从书本上去学习,而主要是在引导学生敢于向未知的领域进军。作为教师的人就得带个头。至于攻关的结果是否获得了可靠的知识,那是另一个问题。

刘韧想提高自己的写作水平,所以办班,借助学生的力量,探索写作新知。

实际上:粘在一起,并没有分段。
在这里插入图片描述
这时候就需要段落标签P出马了。

p标签使用样例:

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
html复制代码<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<!-- <h1>最大的一级标签</h1>
<h2>第二大的二级标签</h2>
<h3>第三大</h3>
<h4>第四大</h4>
<h5>老五</h5>
<h6>老弟</h6> -->
<p>写作最好的老师,是写作本身,
老师最好的老师,是教学本身。</p>

<p>刘韧曾在2002年出版的《知识英雄2.0》结尾处反思,“我已经做了10年记者,在这10年中,与被采访对象的关系问题,一直是我需要认真回答的问题。”

20年后的今天,这个问题仍有继续追问的价值。</p>

<p>这里面包括两个问题,即采访对象为什么要接受记者采访?以及记者为什么要进行采访?大多数的人物采访和报道,无法达成预期,便是这两个问题没有想明白,弄清楚。

如何深度思考这两个问题?CSDN特邀《知识英雄2.0》作者刘韧先生,共建“CSDN刘韧写作班”,特招热爱写作的科技报道者,助其精进采访与写作之能力,提升持续自我进化之思维。

刘韧赞成费孝通说的:我并不认为教师的任务是在传授已有的知识,这些学生们自己可以从书本上去学习,而主要是在引导学生敢于向未知的领域进军。作为教师的人就得带个头。至于攻关的结果是否获得了可靠的知识,那是另一个问题。

刘韧想提高自己的写作水平,所以办班,借助学生的力量,探索写作新知。</p>

</body>
</html>

效果展示:嗯,这次分段了,很不错。
在这里插入图片描述

2.4换行标签:br

br是break的缩写。代表换一行。需要注意的是br是单标签不需要想段落标签p一样结尾在跟一个

。
举例:

1
2
3
html复制代码  12345
<br>
23456

效果:12345和23456没在一行
在这里插入图片描述

2.5图片标签:img

使用格式:<img src="xxx.png">
其中src代表图片路径,可以是网络路径,也可以是本地的。

2.6:超链接标签:a

提供一个链接的标签,点击后当前页面跳转到目标网页。
使用格式:<a href="http://www.xxx.com"></a>

2.7表格标签

表格标签有以下几类:
table:表示整个表格
tr:表示表格的一行
td: 表示一个单元格
th: 表示表头单元格. 会居中加粗
thead: 表格的头部区域
tbody: 表格得到主体区域.

举一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
html复制代码 <table width="600px">
<tr>
<td>姓名</td>
<td>
<input type="text">
</td>
</tr>
<tr>
<td>性别</td>
<td>
<input type="radio" name="gender" checked="checked" id="male">
<label for="male"><img src="image/男.png" width="20" height="20">男</label>
<input type="radio" name="gender" id="famale" >
<label for="famale"><img src="image/女.png" width="20" height="20">女</label>
</td>
</tr>

效果:
在这里插入图片描述

2.8列表标签

列表标签分为有序列表和无序列表:
有序列表:ul,li
无序列表:ol,li
自定义列表:dl、dt、dd

有序列表、无序列表标签演示:

1
2
3
4
5
6
7
8
9
10
html复制代码  <ul>
<!--无序列表-->
<li>Java</li>
<li>C++</li>
</ul>
<ol>
<!--有序列表-->
<li>数据结构</li>
<li>多线程</li>
</ol>

在这里插入图片描述

三、 综合案例:

3.1展示简历

效果图:
在这里插入图片描述

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
html复制代码<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>简历信息 </title>
</head>
<body>
<h1>张嘉文</h1>
<!--我是注释 嘿嘿嘿-->
<div>
<h2>基本信息</h2>
<img src="张嘉文照片/uzi.png" width="500" height="500">
<p><span>求职意向:Java开发工程师</span></p>
<p><span>联系电话:123-456-7892</span></p>
<p><span>邮箱:xxx@forxmail.com</span></p>
<p><a href="https://gitee.com/"> 我的Gitee</a></p>
<p><a href="https://www.csdn.net/">我的博客</a></p>
</div>

<!--教育背景,用div试试-->
<div>
<h2> 教育背景</h2>
<ol>
<li>2004-2008 xxx 幼儿园</li>
<li>2008-2012 xxx 小学</li>
<li>2013-2016 xxx 初中</li>
<li>2016-2020 xxx 高中</li>
<li>2020-2024 xxx 大学</li>
</ol>

</div>


<div>
<!--专业技能-->
<h2>专业技能</h2>

<li>Java基础语法扎实</li>
<li>常见数据结构都可以独立实现并熟练应用</li>
<li>熟知计算机网络理论</li>
<li>掌握web开发能力</li>

</div>

<div>
<!--我的项目-->
<h2>我的项目</h2>
<ol>
<li>
<h3>留言墙</h3>
<p>开发时间:2008年8月到2009年10月</p>
<p>功能介绍:</p>
<ul>
<li>支持同学留言</li>
<li>支持匿名留言</li>
</ul>
<li> <h3>复盘助手</h3></li>


<p>开发时间:2021年11月到2021年12月</p>
<p>功能介绍:
<ul>
<li>能复盘LPL比赛</li>
<li>能查出谁是演员</li>
</ul>
</p>
</ol>
</div>

<h2>个人评价</h2>
<p>在校期间学习优良</p>
</body>
</html>

3.2填写简历:

效果图:
在这里插入图片描述

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
html复制代码<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>填写简历</title>
</head>
<body>
<h3>请填写简历信息</h3>
<table width="600px">
<tr>
<td>姓名</td>
<td>
<input type="text">
</td>
</tr>
<tr>
<td>性别</td>
<td>
<input type="radio" name="gender" checked="checked" id="male">
<label for="male"><img src="image/男.png" width="20" height="20">男</label>
<input type="radio" name="gender" id="famale" >
<label for="famale"><img src="image/女.png" width="20" height="20">女</label>
</td>
</tr>
<tr>
<td>出生日期</td>
<td>
<select>
<option>--请选择年份</option>
<option>1999</option>
<option>2000</option>
<option>2001</option>
<option>2002</option>
<option>2003</option>
<option>2004</option>
</select>
<select>
<option>--请选择月份</option>
<option>1</option>
<option>2</option>
<option>3</option>
<option>4</option>
<option>5</option>
<option>6</option>
<option>7</option>
<option>8</option>
<option>9</option>
<option>10</option>
<option>11</option>
<option>12</option>
</select>
<select>
<option>--请选择日期</option>
<option>1</option>
<option>2</option>
<option>3</option>
<option>4</option>
<option>5</option>
<option>6</option>
<option>7</option>
<option>8</option>
<option>9</option>
<option>10</option>
<option>11</option>
<option>12</option>
<option>13</option>
<option>14</option>
<option>15</option>
<option>16</option>
<option>17</option>
<option>18</option>
<option>19</option>
<option>20</option>
<option>21</option>
<option>22</option>
<option>23</option>
<option>24</option>
<option>25</option>
<option>26</option>
<option>27</option>
<option>28</option>
<option>29</option>
<option>30</option>
</select>
</td>
</tr>
<tr>
<td>就读学校</td>
<td>
<input type='text'>
</td>
</tr>
<tr>
<td>应聘岗位</td>
<td>
<input type="checkbox" id="frontend"><label for="frontend">前端开发</label>
<input type="checkbox" id="backend"><label for="backend"> 后端开发</label>
<input type="checkbox" id="qa"><label for="qa"> 测试开发</label>
<input type="checkbox" id="op"> <label for="op"> 运维开发</label>
</td>
</tr>
<tr>
<td>掌握技能</td>
<td>
<textarea cols="50" rows="10" ></textarea>
</td>
</tr>
<tr>
<td>项目经历</td>
<td>
<textarea cols="50" rows="10"></textarea>
</td>
</tr>
<tr>
<td></td>
<td>
<input type="checkbox" id="confirm"> <label for="confirm">我已仔细阅读过公司的招聘要求</label>
</td>

</tr>
<tr>
<td></td>
<td>
<a href="#">查看我的状态</a>
</td>
</tr>
<tr>
<td></td>
<td>
<h4>应聘者确认:</h4>
<ul>
<li>以上信息真实有效</li>
<li>能够尽早来公司实习</li>
<li>能够接受公司加班文化</li>
</ul>
</td>
</tr>
</table>
</body>
</html>

本文转载自: 掘金

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

Java的注解有什么用?反射给予注解生命

发表于 2021-11-18

前言

   java的注解,我们都在使用,尤其是框架学习的部分,注解尤为重要,那么 注解究竟有什么魅力?它在程序运行当中又是如何起作用,如何理解注解的存在! 相关资料会告诉我们,注解是解释程序、说明程序的!那么它又是如何解释,说 明程序的?还有注解里的参数,你有没有好奇过它究竟去哪了?下面作以演示说明。

注解

  注解(Annotation),也叫元数据。一种代码级别的说明。它是JDK1.5及以后版本引入的一个特性,与类、接口、枚举是在同一个层次。它可以声明在包、类、字段、方法、局部变量、方法参数等的前面,用来对这些元素进行说明,注释。

反射

  • JAVA反射机制是在运行状态中,获取任意一个类的结构 , 创建对象 , 得到方法,执行方法 , 属性 !
  • 反射:框架设计的灵魂
  • 将类的各个组成部分封装为其他对象,这就是反射机制

反射给予注解生命:

  下面通过自定义注解(模拟在数据库的映射),演示注解的存在价值,注解参数的去向:

  1. 定义注解(数据库注解TableAnnotation,字段注解ColumnAnnotation)
    TableAnnotation
1
2
3
4
5
java复制代码@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface TableAnnotation {
String value();//一个方法的时候,默认用value为名,注解时可不加方法名
}

ColumnAnnotation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ColumnAnnotation {
/**
* 列名
* @return
*/
String columnName();

/**
* 字段类型
* @return
*/
String type();

/**
* 字段长度
* @return
*/
String length();
}
  1. 定义java类 Book来使用注解,作用是声明数据库名以及字段属性等信息
    Book.java
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
java复制代码@TableAnnotation("test_Book")//用value为名,注解时可不加方法名
public class Book{
@ColumnAnnotation(columnName = "id",type = "int",length = "10")
private int id;
@ColumnAnnotation(columnName = "name",type = "varchar",length = "20")
private String name;
@ColumnAnnotation(columnName = "info",type = "varchar",length = "25")
private String info;

public Book() {}

public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public String getInfo() {
return info;
}
public void setInfo(String info) {
this.info = info;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Book book = (Book) o;
return id == book.id &&
Objects.equals(name, book.name) &&
Objects.equals(info, book.info);
}
@Override
public int hashCode() {
return Objects.hash(id, name, info);
}
@Override
public String toString() {
return "Book{" +
"id=" + id +
", name='" + name + '\'' +
", info='" + info + '\'' +
'}';
}
}
  1. 注解的生命,正是利用java的反射机制,来获取注解信息,以达到使用目的! 创建测试类来利用反射,获取Book类的注解生命!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码public class ReflectDemo3 {
public static void main(String[] args) throws ClassNotFoundException {
//加载类,获取Class对象
Class bk = Class.forName("com.kkb.Demo3.Book");
//获取构造方法
TableAnnotation annotation = (TableAnnotation) bk.getAnnotation(TableAnnotation.class);
//获取类注解的注解信息
String value = annotation.value();
System.out.println(value);
System.out.println("===============================");

//获取属性注解的注解信息
Field[] fields = bk.getDeclaredFields();
for (Field f:fields) {
ColumnAnnotation ca = f.getAnnotation(ColumnAnnotation.class);
System.out.println(f.getName()+"属性,对应的字段:"+ca.columnName()+",数据类型是"+ca.type()+",长度是"+ca.length());
}
}
}

运行结果:
在这里插入图片描述

总结

  注解就是利用反射,获取注解的信息以及参数,然后拿到参数值或者注解类型,来进行一些操作,这样注解以及注解的参数等信息就有了它的使用价值!

本文转载自: 掘金

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

SSIS学习使用十一:日志(Logging)的使用配置和自定

发表于 2021-11-18

这是我参与11月更文挑战的第18天,活动详情查看:2021最后一次更文挑战

翻译参考

本文主要参考翻译自 The Stairway to Integration Services 系列文章的 Logging – Level 11 of the Stairway to Integration Services,目的在于对 SSIS 有一个全面清晰的认识,所有内容在原文的基础上进行实操,由于版本差异、个人疑问等多种原因,未采用完全翻译的原则,同时也会对文中内容进行适当修改,希望最终可以更有利于学习和了解 SSIS,

感谢支持!


本篇中,我们将配置SSIS的内置日志记录。演示简单和高级的日志配置,存储和检索日志配置以及生成自定义日志消息。

事件侦听器(Event Listeners)

SSIS中的任务和容器都是可执行文件,包括SSIS包对象。可行性文件引起事件。SSIS事件处理程序是”侦听器”,当任务和容器引发事件时它对事件进行响应。

可以认为 SSIS事件 是在可执行文件之间发送的消息。这个消息根据一定规则传输,比如:消息从低水平作用域流到高水平作用域。

SSIS中的日志(logs)也是侦听器。

配置SSIS日志

将上一篇最后设置的 Sequence.dtsx 的 DisableEventHandlers 属性还原为默认值False

点击顶部的SSIS下拉菜单,点击选中”日志记录”(Logging)。

2021-02-05-09-12-33.png

打开 配置SSIS日志窗口(Configure SSIS Logs window)

可用的日志提供程序的类型是:

  • Windows Event Log —— Windows事件日志
  • Text Files —— 文本文件
  • XML Files —— XML文件
  • SQL Server
  • SQL Server Profiler

2021-02-05-09-18-38.png

选择文本文件提供程序。然后点击 “添加”(Add) 按钮,添加一个文件文件记录 “Precedence.dtsx” 包的日志。

2021-02-05-09-24-08.png

可以在底部看到重要的提示信息。如上图,它指示我们下一步要做什么。

我们需要在左侧的容器树视图中,选择复选框启用日志记录。如下,为 “Precedence” 包启用日志记录。

2021-02-05-09-26-58.png

现在来配置日志自身。首先我们需要分配日志给在容器树视图选择的包。通过选中日志的复选框分配日志。

2021-02-05-09-33-06.png

后面,还可以编辑日志的 “名称”(Name) 和 “描述”(Description) 属性。点击 “配置”(Configuration) 列单元格中的下拉菜单,并点击”<新建连接>”。

一个新的 文件连接管理器 在 连接管理器 中被创建,文件连接管理器编辑器 被打开。它可以用来配置文本文件日志使用的文件。在使用类型中选择”创建文件”(Create file):日志文件不会在SSIS包每次执行时创建,而是如果不存在则被创建;如果日志文件存在,日志数据会追加进去。

2021-02-05-09-54-47.png

点击文件右侧的 “浏览”(Browse),选择日志所在文件夹,然后下方输入文件名。如下所示:

2021-02-05-09-58-53.png

点击”确定”,完成文件连接管理器的配置。

我们已经为 “Precedence.dtsx” 包配置了一个文本文件日志。执行该SSIS包,然后打开 SSISLog.csv 文件查看。

记住,SSIS日志是监听事件的侦听器。默认的日志侦听器,监听 包开始(PackageStart) 和 包结束(PackageEnd) 事件。

2021-02-05-10-07-52.png

添加事件

点击顶部的 SSIS下拉菜单 并点击”日志记录”(Logging),打开配置SSIS日志窗口。

点击”详细信息”标签页。此处列出了所有可执行文件引发事件的事件列表。即侦听器(SSIS事件处理程序和SSIS日志)访问SSIS包中可执行文件事件的集合。

如果在SSIS控制流中添加不同任务,我们将会在该列表和事件处理程序列表中看到额外的事件。

选择 “OnError” 和 “OnInformation” 事件。

2021-02-05-10-16-10.png

执行 “Precedence.dtsx” 包,然后打开 SSISLog.csv 文件查看。可以看到其内容记录增加了按事件冒泡依次发生的错误事件。

高级日志配置

高级配置

打开 “Precedence.dtsx” 包的 配置SSIS日志窗口,打开”详细”标签页,在底部有三个按钮:”高级”(Advanced)、”加载”(Load)和”保存”(Save)。

  • “高级”

点击”高级”按钮,显示可用于配置的SSIS日志字段。如下,之前我们选择的 “OnError” 和 “OnInformation” 事件选择了所有字段。

2021-02-05-10-32-31.png

然后修改如下,”OnError”事件不选择”ExecutionID”、”DataBytes”列,”OnInformation”事件不选择”ExecutionID”、”SourceID”列。最后高级日志配置表格应该类似下图:

2021-02-05-10-40-28.png

  • “保存”

然后点击”保存”(Save)。当保存对话框显示时,在文件名文本框输入 “MyLogConfig”。

2021-02-05-10-43-12.png

点击保存按钮,存储SSIS当前的日志配置到一个XML文件。

  • xml配置文件的作用 —— 加载重用已有的日志配置

下面演示下xml配置文件的作用。清空高级日志配置表格,类似如下。

2021-02-05-10-47-15.png

点击”加载”(Load)按钮,打开对话框显示后选择”MyLogConfig”文件,点击”打开”按钮:

2021-02-05-10-48-53.png

可以看到,高级日志配置表格返回到了保存的选择。你可以使用此功能,鼓励企业中的开发人员从SSIS的内置日志记录中收集类似的日志记录字段。

点击”确定”,关闭配置SSIS日志窗口。

根据需要引发自定义事件

有相当多的日志消息由 SSIS 自动生成。但是也可以手动引发事件并被日志监听到,实现生成自定义日志消息。

打开 “Script Task 3” 的编辑器,点击”编辑脚本”按钮。修改Main函数内容如下:

1
2
3
4
5
6
7
8
9
10
cs复制代码public void Main()
{
// TODO: Add your code here
var sTaskName = Dts.Variables["TaskName"].Value.ToString();
var sMsg = sTaskName + " completed!";
var fireAgain = true;
Dts.Events.FireInformation(101, sTaskName, sMsg, "", 0, ref fireAgain);
MessageBox.Show(sMsg);
Dts.TaskResult = (int)ScriptResults.Success;
}

首先添加一个变量”sMsg”,分配值为sTaskName + " completed!"。然后添加代码手动引发 Information 事件:Dts.Events.FireInformation(101, sTaskName, sMsg, "", 0, ref fireAgain)。

Dts.Events对象可以引发许多不同类型的事件,包括我们当前在日志配置中监听的 Error 和 Information 事件。

“FireInformation”方法有6个参数:informationCode (Integer), subComponent (String), description (String), helpFile (String), helpContext (Integer) 和 fireAgain (Boolean)。InformationCode可用于对消息进行分类。使用subComponent来标识引发事件的任务。description是想要记录的消息。 HelpFile 和 helpContext 用于将消息链接到帮助相关的话题。暂时从未配置过这些话题,不确定它们是如何(或是否)工作的。FireAgain已过时 —— 默认将其设置为True。

关闭脚本编辑器,然后点击”确定”,关闭 Script Task Editor。

在调试器中执行 “Precedence” 包,Script Task 2成功,Script Task 4失败。并确认”Script Task 3“的完成消息。

打开日志文件检查最新的消息记录。

“OnInformation” 事件同样是冒泡传递消息。从 Script Task 3 开始,到 “序列容器1”,再到 Precedence.dtsx SSIS包。

类似于Information事件,我们也可以引发自定义Error事件。

打开”Script Task 4”的编辑器并点击编辑脚本按钮。可以看到使用 “Dts.Events.FireError” 方法引发的自定义Error事件的代码。

总结

在本文中,我们配置了SSIS的内置日志记录,展示了简单和高级的日志配置,存储和获取日志配置,以及使用脚本任务,通过 C# 生成自定义日志消息。

本文转载自: 掘金

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

为什么网络 I/O 会被阻塞? I/O到底是什么? 创建 s

发表于 2021-11-18

你好,我是yes。

最近打算输出 Netty 相关的文章,但要深入学习 Netty 这个底层通信框架,网络相关知识点不可或缺。所以我打算先写一些前置知识点,对齐一下认识,便于之后对 Netty 的理解。

我们应该都知道 socket(套接字),你可以认为我们的通信都要基于这个玩意,而常说的网络通信又分为 TCP 与 UDP 两种,下面我会以 TCP 通信为例来阐述下 socket 的通信流程。

不过在此之前,我先来说说什么叫 I/O。

I/O到底是什么?

I/O 其实就是 input 和 output 的缩写,即输入/输出。

那输入输出啥呢?

比如我们用键盘来敲代码其实就是输入,那显示器显示图案就是输出,这其实就是 I/O。

而我们时常关心的磁盘 I/O 指的是硬盘和内存之间的输入输出。

读取本地文件的时候,要将磁盘的数据拷贝到内存中,修改本地文件的时候,需要把修改后的数据拷贝到磁盘中。

网络 I/O 指的是网卡与内存之间的输入输出。

当网络上的数据到来时,网卡需要将数据拷贝到内存中。当要发送数据给网络上的其他人时,需要将数据从内存拷贝到网卡里。

那为什么都要跟内存交互呢?

我们的指令最终是由 CPU 执行的,究其原因是 CPU 与内存交互的速度远高于 CPU 和这些外部设备直接交互的速度。

因此都是和内存交互,当然假设没有内存,让 CPU 直接和外部设备交互,那也算 I/O。

总结下:I/O 就是指内存与外部设备之间的交互(数据拷贝)。

好了,明确什么是 I/O 之后,让我们来揭一揭 socket 通信内幕~

创建 socket

首先服务端需要先创建一个 socket。在 Linux 中一切都是文件,那么创建的 socket 也是文件,每个文件都有一个整型的文件描述符(fd)来指代这个文件。

int socket(int domain, int type, int protocol);

  • domain:这个参数用于选择通信的协议族,比如选择 IPv4 通信,还是 IPv6 通信等等
  • type:选择套接字类型,可选字节流套接字、数据报套接字等等。
  • protocol:指定使用的协议。

这个 protocol 通常可以设为 0 ,因为由前面两个参数可以推断出所要使用的协议。

比如socket(AF_INET, SOCK_STREAM, 0);,表明使用 IPv4 ,且使用字节流套接字,可以判断使用的协议为 TCP 协议。

这个方法的返回值为 int ,其实就是创建的 socket 的 fd。

bind

现在我们已经创建了一个 socket,但现在还没有地址指向这个 socket。

众所周知,服务器应用需要指明 IP 和端口,这样客户端才好找上门来要服务,所以此时我们需要指定一个地址和端口来与这个 socket 绑定一下。

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

参数里的 sockfd 就是我们创建的 socket 的文件描述符,执行了 bind 参数之后我们的 socket 距离可以被访问又更近了一步。

listen

执行了 socket、bind 之后,此时的 socket 还处于 closed 的状态,也就是不对外监听的,然后我们需要调用 listen 方法,让 socket 进入被动监听状态,这样的 socket 才能够监听到客户端的连接请求。

int listen(int sockfd, int backlog);

传入创建的 socket 的 fd,并且指明一下 backlog 的大小。

这个 backlog 我查阅资料的时候,看到了三种解释:

  1. socket 有一个队列,同时存放已完成的连接和半连接,backlog为这个队列的大小。
  2. socket 有两个队列,分别为已完成的连接队列和半连接队列,backlog为这个两个队列的大小之和。
  3. socket 有两个队列,分别为已完成的连接队列和半连接队列,backlog仅为已完成的连接队列大小。

解释下什么叫半连接

我们都知道 TCP 建立连接需要三次握手,当接收方收到请求方的建连请求后会返回 ack,此时这个连接在接收方就处于半连接状态,当接收方再收到请求方的 ack 时,这个连接就处于已完成状态:

所以上面讨论的就是这两种状态的连接的存放问题。

我查阅资料看到,基于 BSD 派生的系统的实现是使用的一个队列来同时存放这两种状态的连接, backlog 参数即为这个队列的大小。

而 Linux 则使用两个队列分别存储已完成连接和半连接,且 backlog 仅为已完成连接的队列大小

accept

现在我们已经初始化好监听套接字了,此时会有客户端连上来,然后我们需要处理这些已经完成建连的连接。

从上面的分析我们可以得知,三次握手完成后的连接会被加入到已完成连接队列中去。

这时候,我们就需要从已完成连接队列中拿到连接进行处理,这个拿取动作就由 accpet 来完成。

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

这个方法返回的 int 值就是拿到的已完成连接的 socket 的文件描述符,之后操作这个 socket 就可以进行通信了。

如果已完成连接队列没有连接可以取,那么调用 accept 的线程会阻塞等待。

至此服务端的通信流程暂告一段落,我们再看看客户端的操作。

connect

客户端也需要创建一个 socket,也就是调用 socket(),这里就不赘述了,我们直接开始建连操作。

客户端需要与服务端建立连接,在 TCP 协议下开始经典的三次握手操作,再看一下上面画的图:

客户端创建完 socket 并调用 connect 之后,连接就处于 SYN_SEND 状态,当收到服务端的 SYN+ACK 之后,连接就变为 ESTABLISHED 状态,此时就代表三次握手完毕。

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

调用connect需要指定远程的地址和端口进行建连,三次握手完毕之后就可以开始通信了。

客户端这边不需要调用 bind 操作,默认会选择源 IP 和随机端口。

用一幅图来小结一下建连的操作:


可以看到这里的两个阻塞点:

  • connect:需要阻塞等待三次握手的完成。
  • accept:需要等待可用的已完成的连接,如果已完成连接队列为空,则被阻塞。

read、write

连接建立成功之后,就能开始发送和接收消息了,我们来看一下


read 为读数据,从服务端来看就是等待客户端的请求,如果客户端不发请求,那么调用 read 会处于阻塞等待状态,没有数据可以读,这个应该很好理解。

write 为写数据,一般而言服务端接受客户端的请求之后,会进行一些逻辑处理,然后再把结果返回给客户端,这个写入也可能会被阻塞。

这里可能有人就会问 read 读不到数据阻塞等待可以理解,write 为什么还要阻塞,有数据不就直接发了吗?

因为我们用的是 TCP 协议,TCP 协议需要保证数据可靠地、有序地传输,并且给予端与端之间的流量控制。

所以说发送不是直接发出去,它有个发送缓冲区,我们需要把数据先拷贝到 TCP 的发送缓冲区,由 TCP 自行控制发送的时间和逻辑,有可能还有重传什么的。

如果我们发的过快,导致接收方处理不过来,那么接收方就会通过 TCP 协议告知:别发了!忙不过来了。发送缓存区是有大小限制的,由于无法发送,还不断调用 write 那么缓存区就满了,满了就不然你 write 了,所以 write 也会发生阻塞。

综上,read 和 write 都会发生阻塞。

最后

为什么网络 I/O 会被阻塞?

因为建连和通信涉及到的 accept、connect、read、write 这几个方法都可能会发生阻塞。

阻塞会占用当前执行的线程,使之不能进行其他操作,并且频繁阻塞唤醒切换上下文也会导致性能的下降。

由于阻塞的缘故,起初的解决的方案就是建立多个线程,但是随着互联网的发展,用户激增,连接数也随着激增,需要建立的线程数也随着一起增加,到后来就产生了 C10K 问题。

服务端顶不住了呀,咋办?

优化呗!

所以后来就弄了个非阻塞套接字,然后 I/O多路复用、信号驱动I/O、异步I/O。

下篇我们就来好好盘盘,这几种 I/O 模型!

参考:

blog.csdn.net/yangbodong2…


我是yes,欢迎关注我的个人公众号【yes的练级攻略】,从一点点到亿点点,我们下篇见!

本文转载自: 掘金

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

分布式链路追踪之Spring Cloud Sleuth+Zi

发表于 2021-11-18

大家好,我是不才陈某~

这是《Spring Cloud 进阶》第九篇文章,往期文章如下:

  • 五十五张图告诉你微服务的灵魂摆渡者Nacos究竟有多强?
  • openFeign夺命连环9问,这谁受得了?
  • 阿里面试这样问:Nacos、Apollo、Config配置中心如何选型?这10个维度告诉你!
  • 阿里面试败北:5种微服务注册中心如何选型?这几个维度告诉你!
  • 阿里限流神器Sentinel夺命连环 17 问?
  • 对比7种分布式事务方案,还是偏爱阿里开源的Seata,真香!(原理+实战)
  • Spring Cloud Gateway夺命连环10问?
  • Spring Cloud Gateway 整合阿里 Sentinel网关限流实战!

今天这篇文章陈某介绍一下链路追踪相关的知识,以Spring Cloud Sleuth和zipkin这两个组件为主,后续文章介绍另外一种。

文章的目录如下:

为什么需要链路追踪?

大型分布式微服务系统中,一个系统被拆分成N多个模块,这些模块负责不同的功能,组合成一套系统,最终可以提供丰富的功能。在这种分布式架构中,一次请求往往需要涉及到多个服务,如下图:

服务之间的调用错综复杂,对于维护的成本成倍增加,势必存在以下几个问题:

  • 服务之间的依赖与被依赖的关系如何能够清晰的看到?
  • 出现异常时如何能够快速定位到异常服务?
  • 出现性能瓶颈时如何能够迅速定位哪个服务影响的?

为了能够在分布式架构中快速定位问题,分布式链路追踪应运而生。将一次分布式请求还原成调用链路,进行日志记录,性能监控并将一次分布式请求的调用情况集中展示。比如各个服务节点上的耗时、请求具体到达哪台机器上、每个服务节点的请求状态等等。

常见的链路追踪技术有哪些?

市面上有很多链路追踪的项目,其中也不乏一些优秀的,如下:

  • cat:由大众点评开源,基于Java开发的实时应用监控平台,包括实时应用监控,业务监控 。 集成方案是通过代码埋点的方式来实现监控,比如: 拦截器,过滤器等。 对代码的侵入性很大,集成成本较高,风险较大。
  • zipkin:由Twitter公司开源,开放源代码分布式的跟踪系统,用于收集服务的定时数据,以解决微服务架构中的延迟问题,包括:数据的收集、存储、查找和展现。该产品结合spring-cloud-sleuth使用较为简单, 集成很方便, 但是功能较简单。
  • pinpoint:韩国人开源的基于字节码注入的调用链分析,以及应用监控分析工具。特点是支持多种插件,UI功能强大,接入端无代码侵入
  • skywalking:SkyWalking是本土开源的基于字节码注入的调用链分析,以及应用监控分析工具。特点是支持多种插件,UI功能较强,接入端无代码侵入。目前已加入Apache孵化器。
  • Sleuth:SpringCloud 提供的分布式系统中链路追踪解决方案。很可惜的是阿里系并没有链路追踪相关的开源项目,我们可以采用Spring Cloud Sleuth+Zipkin来做链路追踪的解决方案。

Spring Cloud Sleuth是什么?

Spring Cloud Sleuth实现了一种分布式的服务链路跟踪解决方案,通过使用Sleuth可以让我们快速定位某个服务的问题。简单来说,Sleuth相当于调用链监控工具的客户端,集成在各个微服务上,负责产生调用链监控数据。

Spring Cloud Sleuth只负责产生监控数据,通过日志的方式展示出来,并没有提供可视化的UI界面。

学习Sleuth之前必须了解它的几个概念:

  • Span:基本的工作单元,相当于链表中的一个节点,通过一个唯一ID标记它的开始、具体过程和结束。我们可以通过其中存储的开始和结束的时间戳来统计服务调用的耗时。除此之外还可以获取事件的名称、请求信息等。
  • Trace:一系列的Span串联形成的一个树状结构,当请求到达系统的入口时就会创建一个唯一ID(traceId),唯一标识一条链路。这个traceId始终在服务之间传递,直到请求的返回,那么就可以使用这个traceId将整个请求串联起来,形成一条完整的链路。
  • Annotation:一些核心注解用来标注微服务调用之间的事件,重要的几个注解如下:
+ **cs(Client Send)**:客户端发出请求,开始一个请求的生命周期
+ **sr(Server Received)**:服务端接受请求并处理;**sr-cs = 网络延迟 = 服务调用的时间**
+ **ss(Server Send)**:服务端处理完毕准备发送到客户端;**ss - sr = 服务器上的请求处理时间**
+ **cr(Client Reveived)**:客户端接受到服务端的响应,请求结束; **cr - sr = 请求的总时间**

Spring Cloud 如何整合Sleuth?

整合Spring Cloud Sleuth其实没什么的难的,在这之前需要准备以下三个服务:

  • gateway-sleuth9031:作为网关服务
  • sleuth-product9032:商品微服务
  • sleuth-order9033:订单微服务

三个服务的调用关系如下图:

客户端请求网关发起查询订单的请求,网关路由给订单服务,订单服务获取订单详情并且调用商品服务获取商品详情。

添加依赖

在父模块中添加sleuth依赖,如下:

1
2
3
4
xml复制代码<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>

以上只是Spring Cloud Sleuth的依赖,还有Nacos,openFeign的依赖这里就不再详细说了,有不清楚的可以结合陈某前面几篇文章和案例源码补漏一下。

调整日志级别

由于sleuth并没有UI界面,因此需要调整一下日志级别才能在控制台看到更加详细的链路信息。

在三个服务的配置文件中添加以下配置:

1
2
3
4
5
yaml复制代码## 设置openFeign和sleuth的日志级别为debug,方便查看日志信息
logging:
level:
org.springframework.cloud.openfeign: debug
org.springframework.cloud.sleuth: debug

演示接口完善

以下接口只是为了演示造的数据,并没有整合DB。

sleuth-order9033查询订单详情的接口,如下图:

sleuth-product9032的查询商品详情的接口,如下图:

gateway-sleuth9031网关路由配置如下:

测试

启动上述三个服务,浏览器直接访问:http://localhost:9031/order/get/12

观察控制台日志输出,如下图:

日志格式中总共有四个参数,含义分别如下:

  • 第一个:服务名称
  • 第二个:traceId,唯一标识一条链路
  • 第三个:spanId,链路中的基本工作单元id
  • 第四个:表示是否将数据输出到其他服务,true则会把信息输出到其他可视化的服务上观察,这里并未整合zipkin,所以是false

好了,至此整合完成了,不禁心里倒吸一口凉气,直接看日志那不是眼睛要看瞎了……….

案例源码已经上传,公众号【码猿技术专栏】回复关键词 9528获取。

什么是ZipKin?

Zipkin 是 Twitter 的一个开源项目,它基于Google Dapper实现,它致力于收集服务的定时数据,

以解决微服务架构中的延迟问题,包括数据的收集、存储、查找和展现。

ZipKin的基础架构如下图:

Zipkin共分为4个核心的组件,如下:

  • Collector:收集器组件,它主要用于处理从外部系统发送过来的跟踪信息,将这些信息转换为Zipkin内部处理的 Span 格式,以支持后续的存储、分析、展示等功能。
  • Storage:存储组件,它主要对处理收集器接收到的跟踪信息,默认会将这些信息存储在内存中,我们也可以修改此存储策略,通过使用其他存储组件将跟踪信息存储到数据库中
  • RESTful API:API 组件,它主要用来提供外部访问接口。比如给客户端展示跟踪信息,或是外接系统访问以实现监控等。
  • UI:基于API组件实现的上层应用。通过UI组件用户可以方便而有直观地查询和分析跟踪信息

zipkin分为服务端和客户端,服务端主要用来收集跟踪数据并且展示,客户端主要功能是发送给服务端,微服务的应用也就是客户端,这样一旦发生调用,就会触发监听器将sleuth日志数据传输给服务端。

zipkin服务端如何搭建?

首先需要下载服务端的jar包,地址:search.maven.org/artifact/io…

下载完成将会得到一个jar包,如下图:

直接启动这个jar,命令如下:

1
shell复制代码java -jar zipkin-server-2.23.4-exec.jar

出现以下界面表示启动完成:

此时可以访问zipkin的UI界面,地址:http://localhost:9411,界面如下:

以上是通过下载jar的方式搭建服务端,当然也有其他方式安装,比如docker,自己去尝试一下吧,陈某就不再演示了。

zipKin客户端如何搭建?

服务端只是跟踪数据的收集和展示,客户端才是生成和传输数据的一端,下面详细介绍一下如何搭建一个客户端。

还是上述例子的三个微服务,直接添加zipkin的依赖,如下:

1
2
3
4
5
xml复制代码<!--链路追踪 zipkin依赖,其中包含Sleuth的依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>

注意:由于spring-cloud-starter-zipkin中已经包含了Spring Cloud Sleuth依赖,因此只需要引入上述一个依赖即可。

配置文件需要配置一下zipkin服务端的地址,配置如下:

1
2
3
4
5
6
7
8
9
10
11
yaml复制代码spring:
cloud:
sleuth:
sampler:
# 日志数据采样百分比,默认0.1(10%),这里为了测试设置成了100%,生产环境只需要0.1即可
probability: 1.0
zipkin:
#zipkin server的请求地址
base-url: http://127.0.0.1:9411
#让nacos把它当成一个URL,而不要当做服务名
discovery-client-enabled: false

上述配置完成后启动服务即可,此时访问:http://localhost:9031/order/get/12

调用接口之后,再次访问zipkin的UI界面,如下图:

可以看到刚才调用的接口已经被监控到了,点击SHOW进入详情查看,如下图:

可以看到左边展示了一条完整的链路,包括服务名称、耗时,右边展示服务调用的相关信息,包括开始、结束时间、请求url,请求方式…..

除了调用链路的相关信息,还可以清楚看到每个服务的依赖如下图,如下图:

zipKin的数据传输方式如何切换?

zipkin默认的传输方式是HTTP,但是这里存在一个问题,一旦传输过程中客户端和服务端断掉了,那么这条跟踪日志信息将会丢失。

当然zipkin还支持MQ方式的传输,支持消息中间件有如下几种:

  • ActiveMQ
  • RabbitMQ
  • Kafka

使用MQ方式传输不仅能够保证消息丢失的问题,还能提高传输效率,生产中推荐MQ传输方式。

那么问题来了,如何切换呢?

其实方式很简单,下面陈某以RabbitMQ为例介绍一下。

1、服务端连接RabbitMQ

运行服务端并且连接RabbitMQ,命令如下:

1
shell复制代码java -jar zipkin-server-2.23.4-exec.jar --zipkin.collector.rabbitmq.addresses=localhost --zipkin.collector.rabbitmq.username=guest --zipkin.collector.rabbitmq.password=guest

命令分析如下:

  • zipkin.collector.rabbitmq.addresses:MQ地址
  • zipkin.collector.rabbitmq.username:用户名
  • zipkin.collector.rabbitmq.password:密码

2、客户端添加RabbitMQ

既然使用MQ传输,肯定是要添加对应的依赖和配置了,添加RabbitMQ依赖如下:

1
2
3
4
xml复制代码<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

配置MQ的地址、用户名、密码,配置如下:

1
2
3
4
5
yaml复制代码spring:
rabbitmq:
addresses: 127.0.0.1
username: guest
password: guest

3、配置文件中传输方式切换

spring.cloud.zipkin.sender.type这个配置就是用来切换传输方式的,取值为rabbit则表示使用rabbitMQ进行数据传输。

配置如下:

1
2
3
4
5
6
yaml复制代码spring:
cloud:
zipkin:
sender:
## 使用rabbitMQ进行数据传输
type: rabbit

注意:使用MQ传输,则spring.cloud.zipkin.sender.base-url可以去掉。

完整的配置如下图:

4、测试

既然使用MQ传输,那么我们不启动服务端也是能够成功传输的,浏览器访问:http://localhost:9031/order/get/12

此时发现服务并没有报异常,在看RabbitMQ中已经有数据传输过来了,存在zipkin这个队列中,如下图:

可以看到有消息未被消费,点进去可以看到消息内容就是Trace、Span相关信息。

好了,我们启动服务端,命令如下:

1
shell复制代码java -jar zipkin-server-2.23.4-exec.jar --zipkin.collector.rabbitmq.addresses=localhost --zipkin.collector.rabbitmq.username=guest --zipkin.collector.rabbitmq.password=guest

服务端启动后发现zipkin队列中的消息瞬间被消费了,查看zipkin的UI界面发现已经生成了链路信息,如下图:

zipkin如何持久化?

zipkin的信息默认是存储在内存中,服务端一旦重启信息将会丢失,但是zipkin提供了可插拔式的存储。

zipkin支持以下四种存储方式:

  • 内存:服务重启将会失效,不推荐
  • MySQL:数据量越大性能较低
  • Elasticsearch:主流的解决方案,推荐使用
  • Cassandra:技术太牛批,用的人少,自己选择,不过官方推荐

今天陈某就以MySQL为例介绍一下zipkin如何持久化,Elasticsearch放在下一篇,篇幅有点长。

1、创建数据库

zipkin服务端的MySQL建表SQL在源码中的zipkin-storage/mysql-v1/src/main/resources/mysql.sql中,这份SQL文件我会放在案例源码中。

github地址:github.com/openzipkin/…

创建的数据库:zipkin(名称任意),导入建表SQL,新建的数据库表如下图:

2、服务端配置MySQL

服务端配置很简单,运行如下命令:

1
shell复制代码java -jar zipkin-server-2.23.4-exec.jar --STORAGE_TYPE=mysql --MYSQL_HOST=127.0.0.1 --MYSQL_TCP_PORT=3306 --MYSQL_DB=zipkin --MYSQL_USER=root --MYSQL_PASS=Nov2014

上述命令参数分析如下:

  • STORAGE_TYPE:指定存储的方式,默认内存形式
  • MYSQL_HOST:MySQL的ip地址,默认localhost
  • MYSQL_TCP_PORT:MySQL的端口号,默认端口3306
  • MYSQL_DB:MySQL中的数据库名称,默认是zipkin
  • MYSQL_USER:用户名
  • MYSQL_PASS:密码

陈某是如何记得这些参数的?废话,肯定记不住,随时查看下源码不就得了,这些配置都在源码的/zipkin-server/src/main/resources/zipkin-server-shared.yml这个配置文件中,比如上述MySQL的相关配置,如下图:

zipkin服务端的所有配置项都在这里,没事去翻翻看。

github地址:github.com/openzipkin/…

那么采用rabbitMQ传输方式、MySQL持久化方式,完整的命令如下:

1
shell复制代码java -jar zipkin-server-2.23.4-exec.jar --STORAGE_TYPE=mysql --MYSQL_HOST=127.0.0.1 --MYSQL_TCP_PORT=3306 --MYSQL_DB=zipkin --MYSQL_USER=root --MYSQL_PASS=Nov2014 --zipkin.collector.rabbitmq.addresses=localhost --zipkin.collector.rabbitmq.username=guest --zipkin.collector.rabbitmq.password=guest

持久化是服务端做的事,和客户端无关,因此到这就完事了,陈某就不再测试了,自己动手试试吧。

总结

前面介绍了这么多,不知道大家有没有仔细看,陈某总结一下吧:

  • Spring Cloud Sleuth 作为链路追踪的一种组件,只提供了日志采集,日志打印的功能,并没有可视化的UI界面
  • zipkin提供了强大的日志追踪分析、可视化、服务依赖分析等相关功能,结合Spring Cloud Sleuth作为一种主流的解决方案
  • zipkin生产环境建议切换的MQ传输模式,这样做有两个优点
    • 防止数据丢失
    • MQ异步解耦,性能提升很大
  • zipkin默认是内存的形式存储,MySQL虽然也是一种方式,但是随着数据量越大,性能越差,因此生产环境建议采用Elasticsearch,下一篇文章介绍。

案例源码已经上传,公众号【码猿技术专栏】回复关键词9528获取。

最后说一句(求关注,别白嫖我)

陈某每一篇原创文章都是精心输出,尤其是《Spring Cloud 进阶》专栏的文章,知识点太多,要想讲的细,必须要花很多时间准备,从知识点到源码demo。

如果这篇文章对你有所帮助,或者有所启发的话,帮忙点赞、在看、转发、收藏,你的支持就是我坚持下去的最大动力!

本文转载自: 掘金

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

写了这么久的业务连异常都不知道怎么处理吗

发表于 2021-11-18

「这是我参与11月更文挑战的第4天,活动详情查看:2021最后一次更文挑战」

hi ,大家好,我是三天打鱼,两天晒网的小六六

前言

文本已收录至我的GitHub仓库,欢迎Star:github.com/bin39232820…

种一棵树最好的时间是十年前,其次是现在

今天六脉神剑第二章又来问我问题了,还特么给我发了一堆表情,我特么直呼好家伙

aebb0a06-9124-49d5-859b-e4ecc92130cf.jpg

当然我是不可能这么回他的,毕竟是我为数不多的粉丝,所以来关注下小六六吧,我的每位粉丝都能得到我精准的呵护,所以呀,就回了它下面的话

2f3713ef-a42a-414a-8d25-038a82ca61d3.jpg

大家发现没有,这回六哥硬气了,没有秒回这位粉丝了,哈哈 好家伙! 不再是舔狗了,言归正传:下面就给大家分享分享我们之前异常到底要怎么处理,才是真正的最佳实践!

一个案例

是这样的,这个案例是小六六自身经历的一个例子哈,我想大家都用过微服务吧!我先说说背景哈,我有这样的一个场景,就是我支付完成之后,我是不是要去给用户发货,但是给用户发挥之前,我需要调用一下风控的服务,如果这个用户风控判断有风险的话,我就会拦截这个发货行为,因为这个系统是做海外系统的,那我再调用风控系统之前要做这样一个事情,就是把海外的本地币种转化为USD的统一的汇率,这就设计到了汇率的服务了,然后呢有一天假设我做了一些更改,然后需要把我支付这个服务,和我调用汇率接口的服务一起升级,但是因为我不小心,导致忘记发汇率接口,然后就会出现一个问题,因为我汇率服务本身,里面是处理了异常的,就算我没有升级这个服务,你一样也可以调通这个服务,但是我会把我自己的异常转换成code,然后支付调用汇率这个服务,根据code的值成功或者失败去做逻辑,但是呢因为我支付服务,如果调用成功的话,我就把转换后的金额传给风控,如果转换返回失败,我就给一个默认值0,但是这样一写逻辑就碰到一个问题,风控那边会根据金额来做策略,它有一个策略就是拦截金额为0的订单,不发货,所以那一次升级,就是因为这个逻辑导致了有6k多单的发货没有被发货,导致了这个事故,像上面这个问题,其实我们觉得我们不应该说帮人家去做决策,如果失败的话,我们不应该说给一个默认值,而是抛异常出去才对,这才是正确的做法,就是很多时候,我们自己并不知道是给一个业务code的错误,还是抛一个Exception,像很多其他的不那么严谨的业务,可能并不说考虑的那么清楚,但是我们支付就必须一点点都得考虑的很严谨了,像这个事故,我们还有很多不足的地方,延迟发货的告警没有告,调用汇率接口失败了,也没有告警,而是把原生的错误转换了等等。所以小六六这边才觉得,很多的时候,我们自己确实是不知道如何的处理一些业务的异常,应该怎么样给其他服务返回,才能让调用你的服务的人,觉得你这个服务的设计上好的,等等,这就是我想跟大家聊的这篇文章。

不过我们还是先来了解下Java的异常体系吧!

什么是异常

异常是程序中的一些错误,但并不是所有的错误都是异常,并且错误有时候是可以避免的。

比如说,你的代码少了一个分号,那么运行出来结果是提示是错误java.lang.Error;如果你用System.out.println(11/0),那么你是因为你用0做了除数,会抛出java.lang.ArithmeticException的异常。

异常发生的原因有很多,通常包含以下几大类:

  • 用户输入了非法数据。
  • 要打开的文件不存在。
  • 网络通信时连接中断,或者JVM内存溢出。

要理解Java异常处理是如何工作的,你需要掌握以下三种类型的异常:

  • 检查性异常: 最具代表的检查性异常是用户错误或问题引起的异常,这是程序员无法预见的。例如要打开一个不存在文件时,一个异常就发生了,这些异常在编译时不能被简单地忽略。
  • 运行时异常: 运行时异常是可能被程序员避免的异常。与检查性异常相反,运行时异常可以在编译时被忽略。
  • 错误: 错误不是异常,而是脱离程序员控制的问题。错误在代码中通常被忽略。例如,当栈溢出时,一个错误就发生了,它们在编译也检查不到的。

Java异常的体系结构

Java把异常当作对象来处理,并定义一个基类java.lang.Throwable作为所有异常的超类。

在Java API中已经定义了许多异常类,这些异常类分为两大类,错误Error和异常Exception。

Java异常层次结构图如下图所示:

image.png

在Java中,所有异常类的父类是Throwable类,Error类是error类型异常的父类,Exception类是exception类型异常的父类,RuntimeException类是所有运行时异常的父类,RuntimeException以外的并且继承Exception的类是非运行时异常。

  • Error:Error类对象由 Java 虚拟机生成并抛出,大多数错误与代码编写者所执行的操作无关。例如,Java虚拟机运行错误(Virtual MachineError),当JVM不再有继续执行操作所需的内存资源时,将出现 OutOfMemoryError。这些异常发生时,Java虚拟机(JVM)一般会选择线程终止;还有发生在虚拟机试图执行应用时,如类定义错误(NoClassDefFoundError)、链接错误(LinkageError)。这些错误是不可查的,因为它们在应用程序的控制和处理能力之 外,而且绝大多数是程序运行时不允许出现的状况。对于设计合理的应用程序来说,即使确实发生了错误,本质上也不应该试图去处理它所引起的异常状况。在Java中,错误通常是使用Error的子类描述。
  • Exception:在Exception分支中有一个重要的子类RuntimeException(运行时异常),该类型的异常自动为你所编写的程序定义ArrayIndexOutOfBoundsException(数组下标越界)、NullPointerException(空指针异常)、ArithmeticException(算术异常)、MissingResourceException(丢失资源)、ClassNotFoundException(找不到类)等异常,这些异常是不检查异常,程序中可以选择捕获处理,也可以不处理。这些异常一般是由程序逻辑错误引起的,程序应该从逻辑角度尽可能避免这类异常的发生;而RuntimeException之外的异常我们统称为非运行时异常,类型上属于Exception类及其子类,从程序语法角度讲是必须进行处理的异常,如果不处理,程序就不能编译通过。如IOException、SQLException等以及用户自定义的Exception异常,一般情况下不自定义检查异常。

Java 异常的处理机制

Java的异常处理本质上是抛出异常和捕获异常。

  • 抛出异常:要理解抛出异常,首先要明白什么是异常情形(exception condition),它是指阻止当前方法或作用域继续执行的问题。其次把异常情形和普通问题相区分,普通问题是指在当前环境下能得到足够的信息,总能处理这个错误。对于异常情形,已经无法继续下去了,因为在当前环境下无法获得必要的信息来解决问题,你所能做的就是从当前环境中跳出,并把问题提交给上一级环境,这就是抛出异常时所发生的事情。抛出异常后,会有几件事随之发生。首先,是像创建普通的java对象一样将使用new在堆上创建一个异常对象;然后,当前的执行路径(已经无法继续下去了)被终止,并且从当前环境中弹出对异常对象的引用。此时,异常处理机制接管程序,并开始寻找一个恰当的地方继续执行程序,这个恰当的地方就是异常处理程序或者异常处理器,它的任务是将程序从错误状态中恢复,以使程序要么换一种方式运行,要么继续运行下去。

举个简单的例子,假使我们创建了一个学生对象Student的一个引用stu,在调用的时候可能还没有初始化。所以在使用这个对象引用调用其他方法之前,要先对它进行检查,可以创建一个代表错误信息的对象,并且将它从当前环境中抛出,这样就把错误信息传播到更大的环境中。

1
2
3
scss复制代码if(stu == null){
throw new NullPointerException();
}
  • 捕获异常:在方法抛出异常之后,运行时系统将转为寻找合适的异常处理器(exception handler)。潜在的异常处理器是异常发生时依次存留在调用栈中的方法的集合。当异常处理器所能处理的异常类型与方法抛出的异常类型相符时,即为合适的异常处理器。运行时系统从发生异常的方法开始,依次回查调用栈中的方法,直至找到含有合适异常处理器的方法并执行。当运行时系统遍历调用栈而未找到合适的异常处理器,则运行时系统终止。同时,意味着Java程序的终止。

Java异常处理涉及到五个关键字,分别是:try、catch、finally、throw、throws。下面将骤一介绍,通过认识这五个关键字,掌握基本异常处理知识。

• try – 用于监听。将要被监听的代码(可能抛出异常的代码)放在try语句块之内,当try语句块内发生异常时,异常就被抛出。

• catch – 用于捕获异常。catch用来捕获try语句块中发生的异常。

• finally – finally语句块总是会被执行。它主要用于回收在try块里打开的物力资源(如数据库连接、网络连接和磁盘文件)。只有finally块,执行完成之后,才会回来执行try或者catch块中的return或者throw语句,如果finally中使用了return或者throw等终止方法的语句,则就不会跳回执行,直接停止。

• throw – 用于抛出异常。

• throws – 用在方法签名中,用于声明该方法可能抛出的异常。

项目中到底要怎么去处理异常呢

小六六这边分2种情况来说说,一种就是我们一般的后台管理系统,一种是类似于支付系统的C端项目,再我的感觉中,它们对异常处理的细粒度是不一样的。

一般的后台管理系统的方式

一个异常枚举类,所有异常码定义在这里:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
arduino复制代码public enum BizExceptionEnum {
APPLICATION_ERROR(1000, "网络繁忙,请稍后再试"),
INVALID_USER(1001, "用户名或密码错误"),
INVALID_REQ_PARAM(1002, "参数错误"),
EXAM_NOT_FOUND(1003, "未查到考试信息"),
;
BizExceptionEnum(Integer errorCode, String errorMsg) {
this.errorCode = errorCode;
this.errorMsg = errorMsg;
}
private final Integer errorCode;
private final String errorMsg;

// get......
}

一个业务异常类:

1
2
3
4
5
6
7
8
9
10
scala复制代码public class BizException extends RuntimeException {
private final BizExceptionEnum bizExceptionEnum;
public BizException(BizExceptionEnum bizExceptionEnum) {
super(bizExceptionEnum.getErrorMsg());
this.bizExceptionEnum = bizExceptionEnum;
}
public BizExceptionEnum getBizExceptionEnum() {
return bizExceptionEnum;
}
}

一个全局的异常处理类:

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
java复制代码@RestControllerAdvice
public class GlobalHandler {
private final Logger logger = LoggerFactory.getLogger(GlobalHandler.class);
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result exceptionHandler(MethodArgumentNotValidException e) {
Result result = new Result(BizExceptionEnum.INVALID_REQ_PARAM.getErrorCode(),
BizExceptionEnum.INVALID_REQ_PARAM.getErrorMsg());
logger.error("req params error", e);
return result;
}
@ExceptionHandler(BizException.class)
public Result exceptionHandler(BizException e) {
BizExceptionEnum exceptionEnum = e.getBizExceptionEnum();
Result result = new Result(exceptionEnum.getErrorCode(), exceptionEnum.getErrorMsg());
logger.error("business error", e);
return result;
}
@ExceptionHandler(value = Exception.class)
public Result exceptionHandler(Exception e) {
Result result = new Result(BizExceptionEnum.APPLICATION_ERROR.getErrorCode(),
BizExceptionEnum.APPLICATION_ERROR.getErrorMsg());
logger.error("application error", e);
return result;
}

}

其中Result类的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
kotlin复制代码public class Result<T> {
private Boolean success;
private Integer errorCode;
private String errorMsg;
private T data;
public Result(T data) {
this(true, null, null, data);
}
public Result(Integer errorCode, String errorMsg) {
this(false, errorCode, errorMsg, null);
}
public Result(Boolean success, Integer errorCode, String errorMsg, T data) {
this.success = success;
this.errorCode = errorCode;
this.errorMsg = errorMsg;
this.data = data;
}
// get set......
}

示例Controller类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
less复制代码@RestController
@RequestMapping("/json/exam")
public class ExamController {
@Autowired
private IExamService examService;
@PostMapping("/getExamList")
public Result<List<GetExamListResVo>> getExamList(@Validated @RequestBody GetExamListReqVo reqVo,
@AuthenticationPrincipal UserDetails userDetails)
throws IOException {
List<GetExamListResVo> resVos = examService.getExamList(reqVo, userDetails);
Result<List<GetExamListResVo>> result = new Result(resVos);
return result;
}
}

其中Result类的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
kotlin复制代码public class Result<T> {
private Boolean success;
private Integer errorCode;
private String errorMsg;
private T data;
public Result(T data) {
this(true, null, null, data);
}
public Result(Integer errorCode, String errorMsg) {
this(false, errorCode, errorMsg, null);
}
public Result(Boolean success, Integer errorCode, String errorMsg, T data) {
this.success = success;
this.errorCode = errorCode;
this.errorMsg = errorMsg;
this.data = data;
}
// get set......
}

示例Controller类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
less复制代码@RestController
@RequestMapping("/json/exam")
public class ExamController {
@Autowired
private IExamService examService;
@PostMapping("/getExamList")
public Result<List<GetExamListResVo>> getExamList(@Validated @RequestBody GetExamListReqVo reqVo,
@AuthenticationPrincipal UserDetails userDetails)
throws IOException {
List<GetExamListResVo> resVos = examService.getExamList(reqVo, userDetails);
Result<List<GetExamListResVo>> result = new Result(resVos);
return result;
}
}

示例Service类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码@Service
public class IExamServiceImpl implements IExamService {
@Autowire
private ManualMicrowebsiteMapper microwebsiteMapper;
@Override
public List<GetExamListResVo> getExamList(GetExamListReqVo reqVo, UserDetails userDetails) throws IOException {
List<MicrowebsiteExam> examEntities = microwebsiteMapper.select(reqVo.getExamType(), userDetails.getUsername());
// 按照业务的定义要求,此处考试列表必须不为空,一旦为空,则说明后台配置有误或其它未知原因,这种情况视为一种业务异常
if (examEntities.isEmpty()) {
// 未查到考试信息,抛出相应的业务异常
throw new BizException(BizExceptionEnum.EXAM_NOT_FOUND);
}
// 此处代码还有其它各类异常抛出......
List<GetExamListResVo> resVos = examEntities.stream().map(examEntity -> {
GetExamListResVo resVo = new GetExamListResVo();
BeanUtils.copyProperties(examEntity, resVo);
return resVo;
}).collect(toList());
return resVos;
}
}

C端项目的例子

  • 其实,C端项目大体和上面说一致的,但是我们一般都是微服务进行开发,那么我们应该一开始就给每个服务的业务异常码返回一个范围,这样就能从请求的源头就能知道错误的点在哪个系统,这是第一个点吧
  • 第二个,其实对于每个微服务,和上面的异常处理上一样的,但是我想说的是对于上面处理的Service,我们应该对里面的业务异常更加细腻的去处理,因为我们只是抛出了一些我们能预判到的一些业务异常,但是一些比如JSON转换异常等等异常,我们要到最外层去处理,但是最外层也只是把这个异常转换成大异常了,这样就是说对于C端项目来说,这样异常的力度,应该不是不够的,我们应该再细分一下,就是尽可能的把一些可能的异常转换成我们业务异常,这样的话,我们代码的健壮性就会好很多,而且很多的异常展示给用户的文案也是可以统一转换的。我们来看下面一个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
java复制代码// 1根据传入条件判断该派安盈用户存在,状态是normal
ThirdAcct thirdAcct = null;
try {
thirdAcct = thirdAcctRelationService.getNormalThirdAcct(chId, userType, thirdUserId, payMethod, chAccountId);
} catch (DataAccessException e) {
throw new UIDataAccessException(e.getCode(), e.getMessage(), "fail.thirdAcct.queryfail",e);
}

//1.1不存在说明已经删除过了
if (ObjectUtil.isNull(thirdAcct)) {
return Boolean.TRUE;
}


//1.2存在,证明等待删除
// 2.获取账号的渠道用于调取派安盈接口
ChAccount chAccount = null;
try {
chAccount = chAccountRelationService.getChAccount(appId, chId, payMethod, chAccountId);
} catch (DataAccessException e) {
//di装bi
throw new UIDataAccessException(e.getCode(), e.getMessage(), "fail.chAccount.miss",e);
}
if (ObjectUtil.isNull(chAccount)) {
throw new UIDataAccessException("fail.chAccount.miss", ",获取渠道账号信息失败", "fail.chAccount.miss");
}

ChannelRelationService channelService = this.getChannelRelationService(chId);
if (channelService == null) {
//没有相关渠道
throw new UIDataAccessException("fail.channel.unfound", "找不到相关渠道的service,chId:"+chId, "fail.channel.unfo

//解绑
try {
channelService.unbind(chAccount,thirdAcct);
} catch (DataAccessException e) {
throw new UIDataAccessException(e.getCode(), e.getMessage(), “fail.thirdChannel.bindFail”,e);
}

//4.解绑,注意这些多个联合组件的,要根据条件来解绑的,
try {
thirdAcctRelationService.unbindThirdAcct(thirdAcct);
} catch (DataAccessException e) {
throw new UIDataAccessException(e.getCode(), e.getMessage(), “fail.dbthirdacct.bindFail”,e);

}


我们可以看到,一个业务可能拆分多个子业务,那么每个子业务都有可能抛不同的异常,你要再不同的子业务中把它们的业务转换成ui的异常,只要你的系统够完善,那么意外的异常就会非常少,这样下去你的系统会越来越稳定的。


结束
--


好了,今天小六六的分享就到这了,可能很多小伙伴看了会觉得没啥东西,那是因为你没有体验过一个C端产品的严谨性,如果仅仅是一个后台管理,确实是不必要这样的,但是对于面向用户的产品,我觉得异常处理的好坏,决定了你这个产品的系统质量!好了,我是小六六,三天打鱼,两天晒网!



**本文转载自:** [掘金](https://juejin.cn/post/7031745150648844301)

*[开发者博客 – 和开发相关的 这里全都有](https://dev.newban.cn/)*
1…295296297…956

开发者博客

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