JavaEdge

JavaEdge 查看完整档案

南京编辑南京邮电大学  |  物联网工程 编辑某不致命公司  |  软件开发工程师 编辑 github.com/Wasabi1234 编辑
编辑

0.全干货技术公众号:JavaEdge
1.经历:19届双一流本科,曾在百度、携程、华为等大厂搬金砖
2.涉猎领域:Java生态各种中间件原理、框架源码、微服务、中台等架构设计及落地实战,只生产硬核干货!
3.开源社区荣誉:阿里云栖社区博客专家、腾讯云+社区2019年度最佳作者、慕课网认证作者、CSDN百万流量万粉博客专家,简书优秀创作者兼《程序员》专题管理员
4.著作:在牛客网著有《Java源码面试解析指南》,目前已有上千人在学习,已助众多读者成功拿到满意offer~

个人动态

JavaEdge 回答了问题 · 2020-10-18

Oauth2的授权码模式为什么非要先获取code?

为了安全和回调

关注 3 回答 2

JavaEdge 发布了文章 · 2020-09-19

Spring Bean基础

全是干货的技术号:

本文已收录在github,欢迎 star/fork:

https://github.com/Wasabi1234...

Spring管理的这些bean藉由配置元数据创建,例如被@Bean注解。那么在 Spring 内部又是如何存储这些信息的呢?

1 BeanDefinition

1.1 域

在容器内,这些bean定义被表示为BeanDefinition对象,它包含但不限于如下元数据:

这些元数据会转换为构成每个bean定义内的一组属性。

1.1.1 包限定类名

被定义bean的实际实现类

1.1.2 bean行为

这些状态指示bean在容器中的行为(作用域、生命周期回调等)。如下即为作用域:

  • 默认的作用域也就是singleton

1.1.3 需要的其它bean引用

这些引用也就是常见的协作或依赖对象。

  • 例如对于如下类:

除了包含有关如何创建特定bean信息的bean定义外,ApplicationContext实现还允许注册在容器外部(用户自定义的)创建的现有对象。

这是通过getBeanFactory()方法访问ApplicationContextBeanFactory完成的,该方法返回其DefaultListableBeanFactory实现。

DefaultListableBeanFactory通过registerSingleton(..)registerBeanDefinition(..)方法支持此注册。当然了,我们开发的应用程序一般只使用通过常规的bean定义内的元数据定义的bean。

DefaultListableBeanFactory支持通过如下两种方式进行注册:

  • registerSingleton(String beanName, Object singletonObject)

bean实例就是传递给registerSingleton方法的singletonObject对象

  • registerBeanDefinition(String beanName, BeanDefinition beanDefinition)

容器根据BeanDefinition实例化bean

当然了,一般的应用程序还是仅通过元数据定义的bean来定义bean。

Bean元数据和显式编码提供的单例实例需尽早地注册,方便容器在自动装配和其他自省(指在运行时来判断一个对象的类型的能力)过程能正确推理它们。虽然在某种程度上支持覆盖现有的元数据或单例实例,但在运行时(与对工厂的实时访问并发)对新bean的注册并不被正式支持,并且可能导致并发访问异常,比如bean容器中的状态不一致。

2 如何给 bean 命名?

每个bean都有一或多个标识符,这些标识符在其所在容器中必须唯一。一个bean通常只有一个标识符。但若它就是需要有一个以上的,那么多余标识符被视为别名。

在bean定义中,可组合使用id、name 属性指定bean的标识符。

  • 最多指定一个名称的id属性。一般来说,这些名字由字母数字组成(如myBean,fooService),但也可能包含特殊字符。
  • 如果还想为bean引入其他别名,可在name属性指定任意数量的其他名称。用逗号,、分号;或空格分隔。

在Spring 3.1前,id属性定义为xsd:ID类型,该类型限制了可能的字符。从3.1开始,它被定义为xsd:string类型。注意,Bean的id唯一性仍由容器强制执行,而不再是XML解析器。

开发者无需提供bean的nameid。如果未明确提供,容器将为该bean生成一个唯一name。但如果想通过使用ref元素或服务定位器模式查找来按名称引用该bean,则必须提供一个name。不提供名称的原因和内部beans和自动装配有关。

可以为bean提供多个名称。这些名称视作同一bean的别名,例如允许应用中的每个组件通过使用特定于组件本身的bean名称来引用公共依赖。

2.1 Bean命名规范

与对实例字段名称的命名规范相同。即小写字母开头,后跟驼峰式大小写。

示例:userServiceroleController

扫描类路径下的组件,Spring就会按照该习惯为未命名的组件生成bean名称:将类名初始字符转换为小写。其实这个规范即是JDK 里的Introspector#decapitalize方法,Spring正使用了它:

decapitalize

java.beans.Introspector.decapitalize


public static String decapitalize(String name) {

if (name == null || name.length() == 0) {

return name;

}

// 如果有多个字符且第一和第二个字符均为大写字母

// 则会保留原始大小写

if (name.length() > 1 && Character.isUpperCase(name.charAt(1)) &&

Character.isUpperCase(name.charAt(0))){

return name;

}

// 使用简单的类名,并将其初始字符转换为小写

char chars[] = name.toCharArray();

chars[0] = Character.toLowerCase(chars[0]);

return new String(chars);

}

2.2 如何为单个bean指定多个别名?

有时希望为单个Bean提供多个名称,尤其是在多系统环境。

XML配置

可使用<alias/>标签:


<alias name="srcName" alias="extName"/>

定义别名后,可将同一容器中名为srcName的bean称为extName

环境示例:

  • 子系统A的配置元数据可通过名称subA-ds引用数据源
  • 子系统B可通过名称subB-ds引用数据源
  • 使用这俩子系统的主系统通过名称main-ds引用数据源。

要使所有三个名称都引用相同的对象,可将以下别名定义添加到配置元数据:


<alias name="subA-ds" alias="subB-ds"/>

<alias name="subA-ds" alias="main-ds" />

现在,每个组件和主应用程序都可以通过唯一名称引用数据源,并且可保证不与任何其它定义冲突(等于高效创建了名称空间),而且引用的是同一bean。

Java代码配置

使用@Bean注解的name属性接收一个String数组。示例如下:


@Configuration

public class AppConfig {

@Bean({"dataSource", "subA-ds", "subB-ds"})

public DataSource dataSource() {

// ...

}

}

3 如何实例化 bean?

BeanDefinition可看做是创建对象的配方。容器在被询问时,会查看被命名过的bean的BeanDefinition,并使用该BeanDefinition中的配置元数据创建(或直接从缓存池获取)对应的对象实例。

比如在XML方式下,在<bean/>标签的class属性指定要实例化的对象的类型。这个class属性,其实就是BeanDefinition实例的Class属性,因此该属性一般强制必须指定。

可通过如下方式使用Class属性来实例化 bean:

3.1 构造器

在容器自身通过反射调用其构造器直接创建bean时,指定要构造的bean类,类似new运算符。该方式下,类基本上都能被Spring兼容。即bean类无需实现任何特定接口或以特定方式编码。 指定bean类即可。注意,根据所用的IoC类型,有时需要一个默认的无参构造器。

3.2 静态工厂方法

指定包含将要创建对象的静态工厂方法的实际类,容器将在类上调用静态工厂方法以创建bean。

定义使用静态工厂方法创建的bean时,可使用class属性来指定包含静态工厂方法的类,并使用factory-method属性指定工厂方法本身的名称。开发者应该能够调用此方法并返回一个存活对象,该对象随后将被视为通过构造器创建的。

这种BeanDefinition的一种用法是在老代码中调用static工厂。

看个例子,如下BeanDefinition指定将通过调用工厂方法来创建bean。该定义不指定返回对象的类型,而仅指定包含工厂方法的类。该示例中的initInstance()方法须是静态方法。


<bean id="serverService"

class="examples.ServerService"

factory-method="initInstance"/>

可与上面的BeanDefinition协同的类:


public class ServerService {

private static ServerService serverService = new ServerService();

private ServerService() {}

public static ServerService createInstance() {

return serverService;

}

}

3.3 实例工厂方法

使用该方式实例化会从容器中调用现有bean的非静态方法来创建新bean。要使用此机制,需将class属性置空,并在factory-bean属性中,在当前(或父/祖先)容器中指定包含要创建该对象的实例方法的bean的名称。factory-method设置工厂方法本身的名称。

示例如下,来看看如何配置这样的bean:

  • 相应的类:

  • 一个工厂类也可以容纳一个以上的工厂方法,如下:

这种方式还表明,即使是工厂bean也可以通过依赖注入进行管理和配置。

“factory bean”是指在Spring容器中配置并通过实例或静态工厂方法创建对象的bean。相比之下,FactoryBean是指特定于Spring的FactoryBean实现类。

4 如何确定Bean的运行时类型?

bean元数据定义中的指定类只是初始类引用,可能结合使用的如下方式之一:

  • 声明的工厂方法
  • FactoryBean类,该情况可能导致bean的运行时类型不同
  • 实例级工厂方法(通过指定的factory-bean名称解析),该情况下直接就不设置了

因此,看起来确定bean运行时类型绝非易事,该如何准确获取呢?

BeanFactory.getType

推荐调用 BeanFactory.getType确定bean的运行时类型。

该方法可确定给定名称bean的类型。 更确切地,返回针对相同bean名称的BeanFactory.getBean调用将返回的对象的类型。

且该方法的实现考虑了前面穷举的所有情况,并针对于FactoryBean ,返回FactoryBean所创建的对象类型,和FactoryBean.getObjectType()返回一致。

查看原文

赞 1 收藏 1 评论 0

JavaEdge 发布了文章 · 2020-09-04

Redis事件处理机制详解

全是干货的技术号:
本文已收录在github,欢迎 star/fork:
https://github.com/Wasabi1234...

Redis 服务器的事件主要处理两方面:

  • 处理文件事件:在多个客户端中实现多路复用,接受它们发来的命令请求,并将命令的执行结果返回给客户端
  • 时间事件:实现服务器常规操作

1 文件事件

Redis server通过在多个客户端间多路复用, 实现了高效的命令请求处理: 多个客户端通过socket连接到 Redis server, 但只有在socket可无阻塞读/写时, server才会和这些客户端交互。

Redis 将这类因为对socket进行多路复用而产生的事件称为文件事件, 文件事件可分类如下:

1.1 读事件

读事件标志着客户端命令请求的发送状态。

当一个新的client连接到服务器时, server会给该client绑定读事件, 直到client断开连接后, 该读事件才会被移除。

读事件在整个网络连接的生命期内, 都会在等待和就绪两种状态之间切换:

  • 当client只是连接到server,但并未向server发送命令时,该客户端的读事件就处于等待状态
  • 当client给server发送命令请求,并且请求已到达时(相应的套接字可以无阻塞地执行读操作),该client的读事件处于就绪状态。

示例

如图展示三个已连接到server、但并未发命令的client

此时客户端的状态:

Client读事件状态命令发送状态
A等待未发送
B等待未发送
C等待未发送

后来,A向服务器发送命令请求, 并且命令请求已到达时, A的读事件状态变为就绪:

此时客户端的状态:

Client读事件状态命令发送状态
A就绪已发送且已到达
B等待未发送
C等待未发送

事件处理器被执行时,就绪的文件事件会被识别到,相应的命令请求就会被发送到命令执行器,并对命令进行求值。

1.2 写事件

写事件标志着client对命令结果的接收状态。

和client自始至终都关联着读事件不同, server只会在有命令结果要传回给client时, 才会为client关联写事件, 并且在命令结果传送完毕之后, client和写事件的关联就会被移除。

一个写事件会在两种状态之间切换:

  • 当server有命令结果需返回给client,但client还未能执行无阻塞写,那么写事件处等待状态
  • 当server有命令结果需返回给client,且client可无阻塞写,那么写事件处就绪状态

当client向server发命令请求, 且请求被接受并执行后, server就需将保存在缓存内的命令执行结果返回给client, 这时server就会为client关联写事件。

示例

server正等待client A 变得可写, 从而将命令结果返回给A:

此时客户端的状态:

Client读事件状态写事件状态
A等待等待
B等待
C等待

当A的socket可无阻塞写时, 写事件就绪, server将保存在缓存内的命令执行结果返回给client:

此时client状态:

Client读事件状态写事件状态
A等待已就绪
B等待
C等待

当命令执行结果被传回client后, client和写事件的关联会被解除(只剩读事件),返回命令执行结果的动作执行完毕,回到最初:

1.3 同时关联读/写事件

我们说过,读事件只有在client断开和server的连接时,才会被移除。即当client关联写事件时,实际上它在同时关联读/写事件。

因为在同一次文件事件处理器的调用中, 单个客户端只能执行其中一种事件(要么读,要么写,不能又读又写), 当出现读事件和写事件同时就绪时,事件处理器优先处理读事件

即当server有命令结果要返回client, 而client又有新命令请求进入时, server先处理新命令请求。

2 时间事件

时间事件记录着那些要在指定时间点运行的事件,多个时间事件以无序链表结构保存在服务器状态中。

无序链表并不影响时间事件处理器的性能。
在Redis3.0版本,正常模式下的 Redis 只带有 serverCron 一个时间事件, 而在 benchmark 模式下, Redis 也只使用两个时间事件。
在这种情况下, 程序几乎是将无序链表退化成一个指针来使用, 所以使用无序链表来保存时间事件, 并不影响事件处理器性能。
  • 时间事件的数据结构

根据 timeProc 函数返回值,将时间事件分类如下:

  • 返回 AE_NOMORE


那么这个事件为单次执行事件。该事件会在指定时间被处理一次,之后该事件就会被删除

  • 返回一个非 AE_NOMORE 的整数值,则为循环执行事件。该事件会在指定时间被处理,之后它会按照timeProc的返回值,更新事件的 when 属性,让这个事件在之后某时间点再运行,以这种方式一直更新运行。

伪代码表示的两种事件处理:

def handle_time_event(server, time_event):
    # 执行事件处理器,并获取返回值
    retval = time_event.timeProc()
    
    if retval == AE_NOMORE:
        # 如果返回 AE_NOMORE ,那么将事件从链表中删除,不再执行
        server.time_event_linked_list.delete(time_event)
    else:
        # 否则,更新事件的 when 属性
        # 让它在当前时间之后的 retval 毫秒之后再次运行
        time_event.when = unix_ts_in_ms() + retval

当时间事件处理器被执行时, 它遍历链表中所有的时间事件, 检查它们的when 属性,并执行已到达事件:

def process_time_event(server):

    # 遍历时间事件链表
    for time_event in server.time_event_linked_list:
        # 检查事件是否已经到达
        if time_event.when <= unix_ts_in_ms():
            # 处理已到达事件
            handle_time_event(server, time_event)

时间事件实例

服务器需要定期对自身的资源和状态进行检查、整理, 保证服务器维持在一个健康稳定状态, 这类操作被统称为常规操作(cron job)。

在 Redis 中, 常规操作由 redis.c/serverCron 实现, 包括如下操作:

  • 更新服务器的各类统计信息,比如时间、内存占用、数据库占用情况等
  • 清理数据库中的过期键值对
  • 对不合理的数据库进行大小调整
  • 关闭和清理连接失效的客户端
  • 尝试进行 AOF 或 RDB 持久化操作
  • 如果服务器是主节点的话,对附属节点进行定期同步
  • 如果处于集群模式的话,对集群进行定期同步和连接测试

Redis 将 serverCron(后文简称为sC) 作为时间事件运行, 确保它能够定期自动运行一次,又因 sC 需要在 Redis 服务器运行期一直定期运行, 所以它是一个循环时间事件:sC 会一直定期执行,直至服务器关闭。

Redis 2.6 的 sC 每秒运行 10 次,即平均每 100 ms运行一次。
Redis 2.8 用户可以通过修改 hz 选项设置 sC 的每秒执行次数。

3 两种事件的调度

简单地说, Redis 里面的两种事件呈协作关系, 它们之间包含如下属性:

  • 一种事件会等待另一种事件执行完后,才开始执行,事件之间不会出现抢占
  • 事件处理器先处理文件事件(即处理命令请求),再执行时间事件(调用 sC)
  • 文件事件的等待时间(类 poll 函数的最大阻塞时间),由距离到达时间最短的时间事件决定

这表明, 实际处理时间事件的时间, 通常会比事件所预定的时间要晚, 延迟时间取决于时间事件执行前, 执行完成文件事件所耗时间。

示例

常规案例

虽然时间事件 Time Event Y 可设置其when属性计划在 t1 时间执行, 但因为文件事件 File Event X 正在运行, 所以 Time Event Y 的执行被延迟。

sC 案例

而且对于 sC 这类循环执行的时间事件来说,如果事件处理器的返回值是 t ,那么 Redis 只保证:

  • 如果两次执行时间事件处理器之间的时间间隔≥t ,则该时间事件至少会被处理一次
  • 而非,每隔 t 时间,就一定要执行一次事件

这对于不使用抢占调度的 Redis 事件处理器而言,也不可能做到

比如,虽然 sC 设定的间隔为 10 ms,但它并非是如下那样每隔 10 ms就运行一次:

实际的 sC 运行方式更可能如下:

根据情况,如果处理文件事件耗费了非常多的时间,sC 被推迟到一两秒之后才能执行,也有可能。
整个事件处理器程序可以用以下伪代码描述:

def process_event():
    # 获取执行时间最接近现在的一个时间事件
    te = get_nearest_time_event(server.time_event_linked_list)
    
    # 检查该事件的执行时间和现在时间之差
    # 如果值 <= 0 ,说明至少有一个时间事件已到达
    # 如果值 > 0 ,说明目前没有任何时间事件到达
    nearest_te_remaind_ms = te.when - now_in_ms()
    
    if nearest_te_remaind_ms <= 0:
        # 若有时间事件已达,则调用不阻塞的文件事件等待函数
        poll(timeout=None)
    else:
        # 若时间事件还没到达,则阻塞的最大时间不超过 te 的到达时间
        poll(timeout=nearest_te_remaind_ms)
        
    # 优先处理已就绪的文件事件
    process_file_events()
    
    # 再处理已到达的时间事件
    process_time_event()

可以看出:

  • 到达时间最近的时间事件,决定了 poll 的最大阻塞时长
  • 文件事件优先于时间事件处理

将这个事件处理函数置于一个循环中,加上初始化和清理函数,这就构成了 Redis 服务器的主
函数调用:

def redis_main():
    # 初始化服务器
    init_server()
    
    # 一直处理事件,直到服务器关闭为止
    while server_is_not_shutdown():
        process_event()
    
    # 清理服务器
    clean_server()

参考

  • 《Redis 设计与实现》
查看原文

赞 0 收藏 0 评论 0

JavaEdge 发布了文章 · 2020-09-04

Redis二进制安全的原理

全是干货的技术号:
本文已收录在github,欢迎 star/fork:
https://github.com/Wasabi1234...

二进制安全

二进制安全是一种主要用于字符串操作函数相关的计算机编程术语。一个二进制安全函数,其本质是将操作输入作为原始的、无任何特殊字符意义的数据流。其在操作上应包含一个字符所能有的256种可能的值(假设为8比特字符)

  • 那什么是特殊字符?

标记字符,如转义码,0结尾的字符串(如C语言中的字符串),不是二进制安全的。

  • 场景

在处理未知格式的数据,例如随意的文件、加密数据及类似情况时,二进制安全功能是必须的。函数必须知道数据长度,以便函数操作整体数据。

Redis二进制安全原理

struct sdshdr{
        int len;//buf数组中已经使用的字节的数量,也就是SDS字符串长度
        int  free;//buf数组中未使用的字节的数量
        char buf[];//字节数组,字符串就保存在这里面
};

redis通过定义上述结构体的方式,扩展了C语言底层字符串的缺点,字符串长度的获取时间复杂度从原来的O(N)变成了O(1),另一方面也可以通过free的动态改变来减少内存的分配。需要强调一点的是buf数组不是存储的字符,而是二进制数组,因为C语言字符串中间是不能出现空字符的,而二进制数据中间很有可能会有空字符,所以C语言是二进制不安全的,而redis又是二进制安全。为了存储多种类型的数据,redis就直接把所有数据当作二进制来存储,这样就可以存储媒体文件和字符串,所以SDS虽然叫简单动态字符串,但是它可不只是用来保存字符串。SDS在Redis中是实现字符串对象的工具。当你对该字符串取值时是通过len属性判断实际内容的长度,然后取的值。拼接字符串时是追加到free空间中的。

简单总结: 二进制安全的意思就是,只关心二进制化的字符串,不关心具体格式,只会严格的按照二进制的数据存取,不会妄图以某种特殊格式解析数据。

Redis的简单动态字符串SDS对比C语言的字符串char*,有以下特性:

  • 可以在O(1)的时间复杂度得到字符串的长度
  • 可以高效的执行append追加字符串操作

SDS通过判断当前字符串空余的长度与需要追加的字符串长度,如果空余长度大于等于需要追加的字符串长度,那么直接追加即可,这样就减少了重新分配内存操作;否则,先用sdsMakeRoomFor函数对SDS进行扩展,按照一定的机制来决定扩展的内存大小,然后再执行追加操作,扩展后多余的空间不释放,方便下次再次追加字符串,这样做的代价就是浪费了一些内存,但是在Redis字符串追加操作很频繁的情况下,这种机制能很高效的完成追加字符串的操作。

查看原文

赞 0 收藏 0 评论 0

JavaEdge 发布了文章 · 2020-08-28

MySQL存储引擎与适用场景详解

1 Isam

在读取数据方面速度很快,而且不占用大量的内存和存储资源
但不支持事务、外键、索引。
MySQL≥5.1版本中不再支持。

2 Berkeley

支持COMMIT和ROLLBACK等事务特性。

MySQL在 ≥ 5.1版本中不再支持。

3 CSV

使用该引擎的MySQL数据库表会在MySQL安装目录data文件夹中的和该表所在数据库名相同的目录中生成一个.CSV文件(所以,它可以将CSV类型的文件当做表进行处理),这种文件是一种普通文本文件,每个数据行占用一个文本行。

但是不支持索引,即使用该种类型的表没有主键列;
也不允许表中的字段为null。csv的编码转换需要格外注意。

适用场景

支持从数据库中拷入/拷出CSV文件。如果从电子表格软件输出一个CSV文件,将其存放在MySQL服务器的数据目录中,服务器就能够马上读取相关的CSV文件。同样,如果写数据库到一个CSV表,外部程序也可以立刻读取它。在实现某种类型的日志记录时,CSV表作为一种数据交换格式,特别有用。

4 MEMORY(亦称HEAP)

在内存中创建临时表来存储数据。

出发点是速度 采用的逻辑存储介质是内存。

每个基于该引擎的表实际对应一个磁盘文件,文件名和表名相同,类型为.frm。
磁盘文件只存储表结构,数据存储在内存,所以使用该种引擎的表拥有极高插入、更新和查询效率。

默认使用哈希(Hash)索引,速度比使用B+Tree快,也可使用B+树索引。

由于这种存储引擎所存储的数据保存在内存中,无法持久化!所以其保存的数据具有不稳定性,比如如果mysqld进程发生异常会造成这些数据的消失,所以该存储引擎下的表的生命周期很短,一般只使用一次。

适用场景

如果需要该数据库中一个用于查询的临时表。

5 BLACKHOLE - 黑洞引擎

支持事务,而且支持mvcc的行级锁,写入这种引擎表中的任何数据都会消失,主要用于做日志记录或同步归档的中继存储,该存储引擎除非有特别目的,否则不适合使用。

适用场景1

使用BLACKHOLE存储引擎的表不存储任何数据,但如果mysql启用了二进制日志,SQL语句被写入日志(并被复制到从服务器)。这样使用BLACKHOLE存储引擎的mysqld可以作为主从复制中的中继重复器或在其上面添加过滤器机制。例如,假设你的应用需要从服务器侧的过滤规则,但传输所有二进制日志数据到从服务器会导致较大的网络流量。在这种情况下,在主服务器主机上建立一个伪从服务器进程。

image

场景2:

如果配置一主多从的话,多个从服务器会在主服务器上分别开启自己相对应的线程,执行binlogdump命令而且多个此类进程并不是共享的。为了避免因多个从服务器同时请求同样的事件而导致主机资源耗尽,可以单独建立一个伪的从服务器或者叫分发服务器。

image

ARCHIVE

区别于InnoDB和MyISAM,ARCHIVE提供压缩功能,拥有高效地插入。
但不支持索引,所以查询性能较差。
支持insert、replace和select操作,不支持update和delete。

适用场景

数据归档

压缩比非常高,存储空间大概是innodb的10-15分之一,所以存储历史数据非常适合,由于不支持索引也不能缓存索引和数据,不适合作为并发访问表。

日志表

因为高压缩和快速插入的特点。
但前提是不经常对该表进行查询。

PERFORMANCE_SCHEMA:

该引擎主要用于收集数据库服务器性能参数。这种引擎提供以下功能:提供进程等待的详细信息,包括锁、互斥变量、文件信息;保存历史的事件汇总信息,为提供MySQL服务器性能做出详细的判断;对于新增和删除监控事件点都非常容易,并可以随意改变mysql服务器的监控周期,例如(CYCLE、MICROSECOND)。 MySQL用户是不能创建存储引擎为PERFORMANCE_SCHEMA的表。

场景: DBA能够较明细得了解性能降低可能是由于哪些瓶颈。

Merge

Merge允许将一组使用MyISAM存储引擎的并且表结构相同(即每张表的字段顺序、字段名称、字段类型、索引定义的顺序及其定义的方式必须相同)的数据表合并为一个表,方便了数据的查询。

场景:MySQL中没有物化视图,视图的效率极低,故数据仓库中数据量较大的每天、每周或者每个月都创建一个单一的表的历史数据的集合可以通过Merge存储引擎合并为一张表。

Federated

该存储引擎可以不同的Mysql服务器联合起来,逻辑上组成一个完整的数据库。
这种存储引擎非常适合数据库分布式应用。
Federated存储引擎可以使你在本地数据库中访问远程数据库中的数据,针对federated存储引擎表的查询会被发送到远程数据库的表上执行,本地是不存储任何数据的。

场景: dblink。

image

缺点:

1.对本地虚拟表的结构修改,并不会修改远程表的结构

2.truncate 命令,会清除远程表数据

  1. drop命令只会删除虚拟表,并不会删除远程表

4.不支持 alter table 命令

  1. select count(), select from limit M, N 等语句执行效率非常低,数据量较大时存在很严重的问题,但是按主键或索引列查询,则很快,如以下查询就非常慢(假设 id 为主索引)

select id from db.tablea where id >100 limit 10 ;

而以下查询就很快:

select id from db.tablea where id >100 and id<150

  1. 如果虚拟虚拟表中字段未建立索引,而实体表中为此字段建立了索引,此种情况下,性能也相当差。但是当给虚拟表建立索引后,性能恢复正常。
  2. 类似 where name like "str%" limit 1 的查询,即使在 name 列上创建了索引,也会导致查询过慢,是因为federated引擎会将所有满足条件的记录读取到本,再进行 limit 处理。

Cluster/NDB

该存储引擎用于多台数据机器联合提供服务以提高整体性能和安全性。适合数据量大、安全和性能要求高的场景。

CAP理论。CAP理论(Brewer’s CAP Theorem) ,是说Consistency(一致性), Availability(可用性), Partition tolerance(分布) 三部分在系统实现只可同时满足二点,没法三者兼顾。如果对"一致性"要求高,且必需要做到"分区",那么就要牺牲可用性;而对大型网站,可用性与分区容忍性优先级要高于数据一致性,一般会尽量朝着 A、P 的方向设计,然后通过其它手段保证对于一致性的商务需求。

MyISAM

MySQL5.5版本之前默认数据库引擎,由早期的ISAM所改良,提供ISAM所没有的索引和字段管理等大量功能。
适用于查询密集型,插入密集型。性能极佳,但却有一个缺点:不支持事务处理(transaction)。
因此,几年发展后,MySQL引入InnoDB,以强化参照完整性与并发违规处理机制,取代了MyISAM。

每个MyISAM表,由存储在硬盘上的3个文件组成,每个文件都以表名称为文件主名,并搭配不同扩展名区分文件类型:

  • .frm--存储资料表定义,此文件非MyISAM引擎的一部分
  • .MYD--存放真正的资料
  • .MYI--存储索引信息。

MyISAM使用表锁机制优化并发读写操作,但需要经常运行OPTIMIZE TABLE命令恢复被更新机制所浪费的空间,否则碎片也会随之增加,最终影响数据访问性能。

MyISAM强调快速读取操作,主要用于高并发select,这也是MySQL深受Web开发喜爱原因:Web场景下大量操作都是读数据,所以大多数虚拟主机提供商和Internet平台提供商(Internet Presence Provider,IPP)只允许MyISAM格式。

MyISAM类型的表支持三种不同的存储结构:静态型、动态型、压缩型。

  • 静态表(默认的存储格式) 表中的字段都是非变长字段,这样每个记录都是固定长度的,这样存储

    • 优点:非常迅速,易缓存,出现故障容易恢复
    • 缺点:占用的空间通常比动态表多。静态表在数据存储时会根据列定义的宽度定义补足空格,但是在访问的时候并不会得到这些空格,这些空格在返回给应用之前已经去掉。同时需要注意:在某些情况下可能需要返回字段后的空格,而使用这种格式时后面到空格会被自动处理掉。
  • 动态表 包含变长字段,记录非固定长度的

    • 优点:占用空间较少
    • 缺点:频繁更新删除记录会产生碎片,需要定期执行OPTIMIZE TABLEmyisamchk -r改善性能,并且出现故障的时候恢复相对比较困难
  • 压缩表 由myisamchk工具创建,占据非常小空间,因为每条记录都是被单独压缩,所以只有非常小的访问开支

InnoDB

  • MySQL5.5后的默认存储引擎

适用于更新密集型。

  • 系统崩溃修复能力

InnoDB可借由事务记录日志(Transaction Log)恢复程序崩溃(crash),或非预期结束所造成的资料错误;
而MyISAM遇到错误,必须完整扫描后才能重建索引,或修正未写入硬盘的错误。InnoDB的修复时间,大都固定,但MyISAM的修复时间,与数据量成正比。相对比较,随数据量增加,InnoDB有更佳稳定性。

  • 缓存

MyISAM必须依靠操作系统来管理读与写的缓存,而InnoDB则是有自己的读写缓存管理机制。InnoDB不会将被修改的数据页立即交给操作系统(page cache),因此在某些情况下,InnoDB的数据访问会比MyISAM更有效率。

  • 提供ACID事务、多版本并发MVCC控制的行锁。
  • 支持自增长列

自增长列的值不能为空,如果在使用的时候为空,则自动从现有值开始增值,如果有但是比现在的还大,则直接保存这个值。

  • 支持外键(foreign key)

外键所在的表称为子表而所依赖的表称为父表。

当操作完全兼容ACID时,虽然InnoDB会自动合并多个连接,但每次有事务产生时,仍至少须写入硬盘一次,因此对于某些硬盘或磁盘阵列,会造成每秒200次的事务处理上限。若希望达到更高的性能且保持事务的完整性,就必使用磁盘缓存与电池备援。当然InnoDB也提供数种对性能冲击较低的模式,但相对的也会降低事务的完整性。
而MyISAM则无此问题,但这并非因为它比较先进,这只是因为它不支持事务。

Infobright

mysql的列存储引擎,适用于数据分析和数据仓库设计。

优点:

1.查询性能高 --比普通Mysql 数据库引擎(MyISAM、InnoDB) 快5-60倍.

2.存储数据量大 --能存储的数据量特别大.

3.高压缩比 --与普通数据库存放的数据文件相比, 可以达到55:1

4.不需要建立索引 --省去了大量建立索引的时间.(对于我们非常有优势)

缺点:

1.不能高并发.最多10个并发

2.Infobright分两个版本:社区版(ICE,免费)、企业版(IEE,收费),社区版在添加数据时,只支持loaddata , 而不支持.insert,update ,delete . 企业版,则全部支持.

TokuDB

支持数据压缩,支持高速写入的一个引擎,但是不适合update多的场景。

XtraDB

XtraDB为派生自InnoDB的强化版,由Percona开发,从MariaDB的10.0.9版起取代InnoDB成为默认的数据库引擎。

常用的MyISAM与InnoDB引擎选型

MyISAM与InnoDB

InnoDB和MyISAM是许多人在使用MySQL时最常用的两个表类型,这两个表类型各有优劣,视具体应用而定。

  • MyISAM类型不支持事务处理等高级处理,而InnoDB类型支持
  • MyISAM类型的表强调的是性能,其执行数度比InnoDB类型更快,但是不提供事务支持,而InnoDB提供事务支持以及外部键等高级数据库功能。

所以从宏观来讲,事务数据库关注细节,而数据仓库关注高层次的聚集,所以,InnoDB更适合作为线上的事务处理,而MyISAM更适合作为ROLAP型数据仓库。

InnoDB引擎适合线上事物型数据库

1.InnoDB引擎表是基于B+树的索引组织表(IOT);

2.每个表都需要有一个聚集索引(clustered index);

3.所有的行记录都存储在B+树的叶子节点(leaf pages of the tree);

4.基于聚集索引的增、删、改、查的效率相对是最高的;

5.如果我们定义了主键(PRIMARY KEY),那么InnoDB会选择器作为聚集索引;

6.如果没有显式定义主键,则InnoDB会选择第一个不包含有NULL值的唯一索引作为主键索引;

7.如果也没有这样的唯一索引,则InnoDB会选择内置6字节长的ROWID作为隐含的聚集索引(ROWID随着行记录的写入而主键递增,这个ROWID不像ORACLE的ROWID那样可引用,是隐含的)。

MYISAM引擎适用于ROLAP数据仓库:

1.读取效率:数据仓库的高并发上承载的大部分是读, MYISAM强调的是性能,每次查询具有原子性,其执行数度比InnoDB类型更快。

2. 存储空间:MyISAM: MyISAM的索引和数据是分开的,并且索引是有压缩的,内存使用率就对应提高了不少。InnoDB:需要更多的内存和存储,它会在主内存中建立其专用的缓冲池用于高速缓冲数据和索引。

3. MyISAM可移植性备份及恢复:MyISAM:数据是以文件的形式存储,所以在跨平台的数据转移中会很方便。在备份和恢复时可单独针对某个表进行操作。InnoDB:免费的方案可以是拷贝数据文件、备份 binlog,或者用 mysqldump,在数据量达到几十G的时候就相对痛苦了。移植过程中MyISAM不受字典数据的影响。

4.从接触的应用逻辑来说,select count(*) 和order by 是最频繁的,大概能占了整个sql总语句的60%以上的操作,而这种操作Innodb其实也是会锁表的,很多人以为Innodb是行级锁,那个只是where对它主键是有效,非主键的都会锁全表的。但MYISAM对于count操作只需要在元数据中读取,不用扫表。

5.如果和MyISAM比insert写操作的话,Innodb还达不到MyISAM的写性能,如果是针对基于索引的update操作,虽然MyISAM可能会逊色Innodb,但是那么高并发的写,从库能否追的上也是一个问题,且不建议数据仓库中频繁update数据。

6.如果是用MyISAM的话,merge引擎可以大大加快数据仓库开发速度,非常适合大项目总量约几亿的rows某一类型(如日志,调查统计)的业务表。

7.全文索引:MyISAM:支持 FULLTEXT类型的全文索引。InnoDB:不支持FULLTEXT类型的全文索引,但是innodb可以使用sphinx插件支持全文索引,并且效果更好。

8.表主键:MyISAM:允许没有任何索引和主键的表存在,索引都是保存行的地址。InnoDB:如果没有设定主键或者非空唯一索引,就会自动生成一个6字节的主键(用户不可见),数据是主索引的一部分,附加索引保存的是主索引的值。

9.对于AUTO_INCREMENT类型的字段,InnoDB中必须包含只有该字段的索引,但是在MyISAM表中,可以和其他字段一起建立联合索引。

10. MyISAM不支持外键,需通过其他方式弥补。

根据引擎特性的优化

如何对InnoDB引擎的表做最优的优化:

1.使用自增列(INT/BIGINT类型)做主键,这时候写入顺序是自增的,和B+数叶子节点分裂顺序一致,这时候存取效率是最高的

2.该表不指定自增列做主键,同时也没有可以被选为主键的唯一索引(上面的条件),这时候InnoDB会选择内置的ROWID作为主键,写入顺序和ROWID增长顺序一致。

参考

查看原文

赞 1 收藏 1 评论 0

JavaEdge 关注了用户 · 2020-05-27

ChiuCheng @chiucheng

Talk is cheap, show me the code!

关注 173

JavaEdge 发布了文章 · 2020-02-29

Jprofile解析dump文件使用详解

1 Jprofile简介

在这里插入图片描述

  • 下载对应的系统版本即可

性能查看工具JProfiler,可用于查看java执行效率,查看线程状态,查看内存占用与内存对象,还可以分析dump日志.

2 功能简介

  • 选择attach to a locally running jvm

  • 选择需要查看运行的jvm,双击或者点击start

  • 等待进度完成,弹出模式选择

  • Instrumentation模式记录所有的信息。包括方法执行次数等Sampling模式则只支持部分功能,不纪录方法调用次数等,并且更为安全

由于纪录信息很多,java运行会变的比正常执行慢很多,sampling模式则不会

- 常规使用选择sampling模式即可,当需要调查方法执行次数才需要选择Instrumentation模式,模式切换需要重启jprofiler
  • 点击OK

  • 选择Live Momory可以查看内存中的对象和大小

  • 选择cpu views点击下图框中的按钮来纪录cpu的执行时间

  • 这时候可以在外部对需要录的jvm操作进行记录了,得出的结果可以轻松看出方法执行调用过程与消耗时间比例:
  • 根据cpu截图的信息,可以找到效率低的地方进行处理,如果是Instrumentation模式则在时间位置会显示调用次数

在Thread界面则可以实时查看线程运行状态,黄色的是wait 红色是block 绿色的是runnable蓝色是网络和I/O请求状态

选择ThreadDumps,可以录制瞬时线程的调用堆栈信息,如下图所示:

3 dump 文件分析

3.1 dump 生成

JProfiler 在线

当JProfiler连接到JVM之后选择Heap Walker,选择Take snapshot图标,然后等待即可


如果内存很大,jprofiler万一参数设置的不正确打不开就需要要重新生成,内存小的时候无所谓

使用JProfiler生成文件

当JProfiler连接到JVM之后选择菜单上的Profiling->save HPROF snapshot 弹出下拉框保存即可,这时候生成的文件就可以一直保存在文件上

jmap

jmap -dump:format=b,file=文件名 pid

windows下不用[],路径要加引号

jmap -dump:format=b,file="D:\a.dump" 8632

命令中文件名就是要保存的dump文件路径, pid就是当前jvm进程的id

JVM启动参数

在发生outofmemory的时候自动生成dump文件:

-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=D:\heapdump

Pah后面是一个存在的可访问的路径,将改参数放入jvm启动参数可以在发生内存outofmemory的时候自动生成dump文件,但是正式环境使用的时候不要加这个参数,不然在内存快满的时候总是会生成dump而导致jvm卡半天,需要调试的时候才需要加这个参数

注意:通过WAS生成的PHD文件dump不能分析出出问题的模板,因为PHD文件不包含对象的值内容,无法根据PHD文件找到出问题的模板,所以PHD文件没有太大的参考价值

3.2 dump文件分析

dump文件生成后,将dump压缩传输到本地,不管当前dump的后缀名是什么,直接改成*.hprof,就可以直接用jprofiler打开了

打开的过程时间可能会很长,主要是要对dump进行预处理,计算什么的,注意 这个过程不能点skip,否则就不太好定位大文件

  • 直接打开.hprof文件

  • 注意如下过程,中途可以喝一杯☕️,不要作死手滑点击了 skip!







这样界面的时候下面可以开始进行操作了!

4 模块功能点详解

也可以使用工具栏中的“转到开始”按钮访问第一个数据集

4.1 内存视图 Memory Views

JProfiler的内存视图部分可以提供动态的内存使用状况更新视图和显示关于内存分配状况信息的视图。所有的视图都有几个聚集层并且能够显示现有存在的对象和作为垃圾回收的对象。

  • 所有对象 All Objects

显示类或在状况统计和尺码信息堆上所有对象的包。你可以标记当前值并显示差异值。

  • 记录对象 Record Objects

显示类或所有已记录对象的包。你可以标记出当前值并且显示差异值。

  • 分配访问树 Allocation Call Tree

显示一棵请求树或者方法、类、包或对已选择类有带注释的分配信息的J2EE组件。

  • 分配热点 Allocation Hot Spots

显示一个列表,包括方法、类、包或分配已选类的J2EE组件。你可以标注当前值并且显示差异值。对于每个热点都可以显示它的跟踪记录树。

  • 类追踪器 Class Tracker

类跟踪视图可以包含任意数量的图表,显示选定的类和包的实例与时间。

4.2 堆遍历 Heap Walker

使用背景

在视图中找到增长快速的对象类型,在memory视图中找到Concurrenthashmap---点右键----选择“Show Selectiion In Heap Walker”,切换到HeapWarker 视图;切换前会弹出选项页面,注意一定要选择“Select recorded objects”,这样Heap Walker会在刚刚的那段记录中进行分析;否则,会分析tomcat的所有内存对象,这样既耗时又不准确;

在JProfiler的堆遍历器(Heap Walker)中,你可以对堆的状况进行快照并且可以通过选择步骤下寻找感兴趣的对象。堆遍历器有五个视图:

  • 类 Classes

显示所有类和它们的实例,可以右击具体的类"Used Selected Instance"实现进一步跟踪。

  • 分配 Allocations

为所有记录对象显示分配树和分配热点。

  • 索引 References

为单个对象和“显示到垃圾回收根目录的路径”提供索引图的显示功能。还能提供合并输入视图和输出视图的功能。

  • 时间 Time

显示一个对已记录对象的解决时间的柱状图。

  • 检查 Inspections

显示了一个数量的操作,将分析当前对象集在某种条件下的子集,实质是一个筛选的过程。

在HeapWalker中,找到泄漏的对象

HeapWarker 会分析内存中的所有对象,包括对象的引用、创建、大小和数量.
通过切换到References页签,可以看到这个类的具体对象实例。 为了在这些内存对象中,找到泄漏的对象(应该被回收),可以在该对象上点击右键,选择“Use Selected Instances”缩小对象范围

通过引用分析该对象

References 可以看到该对象的的引用关系,选项显示引用的类型

  • incoming

显示这个对象被谁引用

  • outcoming

显示这个对象引用的其他对象

选择“Show In Graph”将引用关系使用图形方式展现;

  • 选中该对象,点击Show Paths To GC Root,会找到引用的根节点

通过创建分析该对象

如果还不能定位内存泄露的地方,我们可以尝试使用Allocations页签,该页签显示对象是如何创建出来的;
我们可以从创建方法开始检查,检查所有用到该对象的地方,直到找到泄漏位置;

图表 Graph

你需要在references视图和biggest视图手动添加对象到图表,它可以显示对象的传入和传出引用,能方便的找到垃圾收集器根源。

tips:在工具栏点击"Go To Start"可以使堆内存重新计数,也就是回到初始状态。

  

CPU 视图 CPU Views

  JProfiler 提供不同的方法来记录访问树以优化性能和细节。线程或者线程组以及线程状况可以被所有的视图选择。所有的视图都可以聚集到方法、类、包或J2EE组件等不同层上。CPU视图部分包括:

  

访问树 Call Tree
显示一个积累的自顶向下的树,树中包含所有在JVM中已记录的访问队列。JDBC,JMS和JNDI服务请求都被注释在请求树中。请求树可以根据Servlet和JSP对URL的不同需要进行拆分。
热点 Hot Spots
显示消耗时间最多的方法的列表。对每个热点都能够显示回溯树。该热点可以按照方法请求,JDBC,JMS和JNDI服务请求以及按照URL请求来进行计算。
访问图 Call Graph
显示一个从已选方法、类、包或J2EE组件开始的访问队列的图。
方法统计 Method Statistis
显示一段时间内记录的方法的调用时间细节。

线程视图 Thread Views

  JProfiler通过对线程历史的监控判断其运行状态,并监控是否有线程阻塞产生,还能将一个线程所管理的方法以树状形式呈现。对线程剖析,JProfiler提供以下视图:

  

线程历史 Thread History
显示一个与线程活动和线程状态在一起的活动时间表。
线程监控 Thread Monitor
显示一个列表,包括所有的活动线程以及它们目前的活动状况。
线程转储 Thread Dumps
显示所有线程的堆栈跟踪。

监控器视图 Monitor Views

  JProfiler提供了不同的监控器视图,如下所示:

  

当前锁定图表 Current Locking Graph
显示JVM中的当前锁定情况。
当前监视器 Current Monitors
显示当前正在等待或阻塞中的线程操作。
锁定历史图表 Locking History Graph
显示记录在JVM中的锁定历史。
监控器历史 Monitor History
显示等待或者阻塞的历史。
监控器使用统计 Monitor Usage Statistics
计算统计监控器监控的数据。

VM遥感勘测技术视图 VM Telemetry Views

  观察JVM的内部状态,JProfiler提供了不同的遥感勘测视图,如下所示:

  

内存 Memory
显示堆栈的使用状况和堆栈尺寸大小活动时间表。
记录的对象 Recorded Objects
显示一张关于活动对象与数组的图表的活动时间表。
记录的生产量 Recorded Throughput
显示一段时间累计的JVM生产和释放的活动时间表。
垃圾回收活动 GC Activity
显示一张关于垃圾回收活动的活动时间表。
类 Classes
显示一个与已装载类的图表的活动时间表。
线程 Threads
显示一个与动态线程图表的活动时间表。
CPU负载 CPU Load
显示一段时间中CPU的负载图表。

参考

使用JProfiler进行内存分析

本文由博客一文多发平台 OpenWrite 发布!
查看原文

赞 0 收藏 0 评论 0

JavaEdge 发布了文章 · 2020-02-16

MySQL8.0关系数据库基础教程(三)-select语句详解

1 查询指定字段

  • 在 employee 表找出所有员工的姓名、性别和电子邮箱。


  • SELECT 表示查询,随后列出需要返回的字段,字段间逗号分隔
  • FROM 表示要从哪个表中进行查询
  • 分号为语句结束符

这种查询表中指定字段的操作在关系运算中被称为投影(Projection)
使用 SELECT 子句进行表示。投影是针对表进行的垂直选择,保留需要的字段用于生成新的表

投影操作中包含一个特殊的操作,就是查询表中所有的字段。

2 查询全部字段

  • ‘*’ 表示全部字段


数据库在解析该语句时,会使用表中的字段名进行扩展:

SELECT emp_id, emp_name, sex, dept_id, manager,
       hire_date, job_id, salary, bonus, email
  FROM employee;

虽然星号可以便于快速编写查询语句,但是在实际项目中不推荐使用:

  • 程序可能并不需要所有的字段,避免返回过多的无用数据
  • 当表结构发生变化时,星号返回的信息也会发生改变

除了查询表的字段之外,SELECT 语句还支持扩展的投影操作,包括基于字段的算术运算、函数和表达式等。

3 多字段查询

返回员工的姓名、一年的工资(12 个月的月薪)以及电子邮箱的大写形式:

结果中,返回字段的名称不是很好理解;能不能给它指定一个更明确的标题呢?

4 别名(Alias)

为了提高查询结果的可读性,可以使用别名为表或者字段指定一个临时的名称。SQL 中使用关键字 AS 指定别名。

别名中的关键字 AS 可以省略。

为 employee 表指定了一个表别名 e,然后为查询的结果字段指定了 3 个更明确的列别名(使用双引号)。在查询中为表指定别名之后,引用表中的字段时可以加上别名限定,例如 e.emp_name,表示要查看哪个表中的字段。

在 SQL 语句中使用别名不会修改数据库中存储的表名或者列名,别名只在当前语句中生效。

5 注释

分为单行注释和多行注释

  • 单行注释以两个连字符(--)开始,直到这一行结束
  • SQL 使用 C 语言风格的多行注释(//)

# 也可以用于表示单行注释。

6 无表查询

  • 计算一个表达式的值:


用于快速查找信息。这种语法并不属于 SQL 标准,而是数据库产品自己的扩展。

7 总结

SQL 不仅仅能够查询表中的数据,还可以返回算术运算、函数和表达式的结果。在许多数据库中,不包含 FROM 子句的无表查询可以用于快速获取信息。另外,别名和注释都可以让我们编写的 SQL 语句更易阅读和理解。

本文由博客一文多发平台 OpenWrite 发布!
查看原文

赞 0 收藏 0 评论 0

JavaEdge 发布了文章 · 2020-02-15

MySQL8.0数据库基础教程(二)-理解"关系"

1 SQL 的哲学

形如 Linux 哲学一切都是文件,在 SQL 领域也有这样一条至理名言

一切都是关系

2 关系数据库

所谓关系数据库(Relational database)是创建在关系模型基础上的数据库,借助于集合代数等数学概念和方法来处理数据库中的数据。

现实世界中的各种实体以及实体之间的各种联系均用关系模型表示。现如今虽然对此模型有一些批评意见,但它还是数据存储的传统标准。标准数据查询语言SQL就是一种基于关系数据库的语言,这种语言执行对关系数据库中数据的检索和操作。

关系模型由关系数据结构、关系操作集合、关系完整性约束三部分组成。

2.1 数据结构

  • 表(关系Relation)

以列(值组Tuple)和行(属性Attribute)的形式组织起来的数据的集合。一个数据库包括一个或多个表(关系Relation)。例如,可能有一个有关作者信息的名为authors的表(关系Relation)。每行(属性Attribute)都包含特定类型的信息,如作者的姓氏。每列(值组Tuple)都包含有关特定作者的所有信息:姓、名、住址等等。在关系型数据库当中一个表(关系Relation)就是一个关系,一个关系数据库可以包含多个表(关系Relation)

也称为记录(Record),代表了关系中的单个实体。

也称为字段(Field),表示实体的某个属性。表中的每个列都有一个对应的数据类型,常见的数据类型包括字符类型、数字类型、日期时间类型等。

2.2 操作集合

关系模块中常用的操作包括:

  • 增加(Create)
  • 查询(Retrieve)
  • 更新(Update)
  • 删除(Delete)

其中,使用最多、也最复杂的操作就是数据查询,具体来说包括

  • 选择(Selection)
  • 投影(Projection)
  • 并集(Union)
  • 交集(Intersection)
  • 差集(exception)
  • 笛卡儿积(Cartesian product)
  • ...

2.3 完整性约束

完整性约束包括

2.3.1 实体完整性(Entity integrity)

实体完整性(是关系模型中数据库完整性三项规则的其中之一。实体完整性这项规则要求每个数据表都必须有主键,而作为主键的所有栏位,其属性必须是独一及非空值。

在关系数据库中,唯一标识每一行数据的字段称为主键(Primary Key),主键字段不能为空。每个表有且只能有一个主键。

2.3.2 参照完整性

又称引用完整性,是数据的属性,用以表明引用的有效。参照的完整性不允许关系中有不存在的实体引用。参照完整性与实体完整性二者,皆是关系模型必须满足的完整性约束条件,其目的在于保证数据的一致性。

外键的参照完整性。

  • 外键(Foreign Key)代表了两个表之间的关联关系

比如员工属于某个部门;因此员工表中存在部门编号字段,引用了部门表中的部门编号字段。对于外键引用,被引用的数据必须存在,员工不可能属于一个不存在的部门;删除某个部门之前,也需要对部门中的员工进行相应的处理。

2.3.3 用户定义完整性

基于业务需要自定义的约束。

  • 非空约束(NOT NULL)

确保了相应的字段不会出现空值,例如员工一定要有姓名

  • 唯一约束(UNIQUE)

用于确保字段中的值不会重复,每个员工的电子邮箱必须唯一

  • 检查约束(CHECK)

可以定义更多的业务规则,例如,薪水必须大于 0 ,字符必须大写等

  • 默认值(DEFAULT)

用于向字段中插入默认的数据。

MySQL 中只有 InnoDB 存储引擎支持外键约束;MySQL 8.0.16 增加了对检查约束的支持。因此我们强大的 MySQL 支持以上所有约束。

从 MySQL 5.5 开始默认使用 InnoDB 存储引擎,支持事务处理(ACID)、行级锁定、故障恢复、多版本并发控制(MVCC)以及外键约束等

3 SQL:面向集合编程

3.1 语法特性

SQL 是一种声明性的编程语言,语法接近于自然语言(英语)。通过几个简单的英文单词,例如 SELECT、INSERT、UPDATE、CREATE、DROP 等,完成大部分的数据库操作。

  • 简单的查询示例


可以看出,SQL 简单直观。

  • 以上查询中的 SELECT、FROM 等称为关键字(也称为子句),一般大写
  • 表名、列名等内容一般小写
  • 分号(;)表示语句的结束

SQL 语句不区分大小写,但是遵循一定的规则可以让代码更容易阅读。

SQL 是一种声明式的语言,声明式语言的主要思想是告诉计算机想要什么结果(what),但不指定具体怎么做。这类语言还包括 HTML、正则表达式以及函数式编程等。

3.2 面向集合

对于 SQL 语句而言,它所操作的对象是一个集合(表),操作的结果也是一个集合(表)。例如以下查询:

SELECT emp_id, emp_name, salary
  FROM employee;

其中 employee 是一个表,它是该语句查询的对象;同时,查询的结果也是一个表。所以,我们可以继续扩展该查询:

SELECT emp_id, emp_name, salary
  FROM (
       SELECT emp_id, emp_name, salary
         FROM employee
       ) dt;

我们将括号中的查询结果(取名为 dt)作为输入值,传递给了外面的查询;最终整个语句的结果仍然是一个表。

SQL 中的查询可以完成各种数据操作,例如过滤转换、分组汇总、排序显示等;但是它们本质上都是针对表的操作,结果也是表。

不仅仅是查询语句,SQL 中的插入、更新和删除都以集合为操作对象。我们再看一个插入数据的示例:

CREATE TABLE t(id INTEGER);

-- 适用于 MySQL、SQL Server 以及 PostgreSQL
INSERT INTO t(id)
VALUES (1), (2), (3);

我们首先使用 CREATE TABLE 语句创建了一个表,然后使用 INSERT INTO 语句插入数据。在执行插入操作之前,会在内存中创建一个包含 3 条数据的临时集合(表),然后将该集合插入目标表中。由于我们通常一次插入一条数据,以为是按照数据行进行插入;实际上,一条数据也是一个集合,只不过它只有一个元素而已。

UNION ALL 是 SQL 中的并集运算,用于将两个集合组成一个更大的集合。此外,SQL 还支持交集运算(INTERSECT)、差集运算(EXCEPT)以及笛卡儿积(Cartesian product)

4 数据库案例

包含 3 个表:员工表(employee)、部门表(department)和职位表(job)。

  • 结构图,也称为实体-关系图(Entity-Relational Diagram)

5 总结

关系模型中定义了一个简单的数据结构,即关系(表),用于存储数据。SQL 是关系数据库的通用标准语言,通过声明的方式执行数据定义、数据操作、访问控制等。
记住,对于 SQL,一切都是关系(表)。

参考

本文由博客一文多发平台 OpenWrite 发布!
查看原文

赞 0 收藏 0 评论 0

JavaEdge 发布了文章 · 2020-02-04

Java框架-MyBatis三剑客之MyBatis Generator(mybatis-generator MBG插件)详解

生成器设计思路: 连接数据库 -> 获取表结构 -> 生成文件

1 下载与安装

  • 贴至pom 文件

2 新建配置文件

  • 项目实例
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
        PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
        "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">

<generatorConfiguration>
    <classPathEntry location="/Volumes/doc/jar/mysql-connector-java-8.0.18.jar" />

    <context id="DB2Tables" targetRuntime="MyBatis3">
        
        <plugin type="org.mybatis.generator.plugins.UnmergeableXmlMappersPlugin" />

        <commentGenerator>
            <property name="suppressAllComments" value="true"/>
        </commentGenerator>

        <jdbcConnection driverClass="com.mysql.cj.jdbc.Driver"
                        connectionURL="jdbc:mysql://127.0.0.1:3306/mall?characterEncoding=utf-8"
                        userId="root"
                        password="root">
        </jdbcConnection>

        <javaTypeResolver >
            <property name="forceBigDecimals" value="false" />
        </javaTypeResolver>

        <javaModelGenerator targetPackage="com.javaedge.mall.pojo" targetProject="src/main/java">
            <property name="enableSubPackages" value="true" />
<!--            <property name="trimStrings" value="true" />-->
        </javaModelGenerator>

        <sqlMapGenerator targetPackage="mappers"  targetProject="src/main/resources">
            <property name="enableSubPackages" value="true" />
        </sqlMapGenerator>

        <javaClientGenerator type="XMLMAPPER" targetPackage="com.javaedge.mall.dao"  targetProject="src/main/java">
            <property name="enableSubPackages" value="true" />
        </javaClientGenerator>

        <table tableName="mall_order" domainObjectName="Order" enableCountByExample="false" enableDeleteByExample="false" enableSelectByExample="false" enableUpdateByExample="false"/>
        <table tableName="mall_order_item" domainObjectName="OrderItem" enableCountByExample="false" enableDeleteByExample="false" enableSelectByExample="false" enableUpdateByExample="false"/>

    </context>
</generatorConfiguration>

3 生成文件

  • 默认不覆盖已有文件,重复生成文件后果


  • 不过,可以设置可覆盖


但是注意,对于 xml 文件的内容是追加生成的,不会覆盖!怎么解决呢?

  • 选用该插件
  • 修改配置文件

命令行生成

mvn mybatis-generator:generate

插件运行

  • Maven 插件按钮

  • gradle 插件按钮


生成成功

生成新文件后的项目结构
可多次执行,类会覆盖,但是设计 mapper 的 xml 文件会重复生成

附 :配置文件详解

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
  PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
"http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">

<!-- 配置生成器 -->
<generatorConfiguration>
<!-- 可以用于加载配置项或者配置文件,在整个配置文件中就可以使用${propertyKey}的方式来引用配置项
    resource:配置资源加载地址,使用resource,MBG从classpath开始找,比如com/myproject/generatorConfig.properties        
    url:配置资源加载地质,使用URL的方式,比如file:///C:/myfolder/generatorConfig.properties.
    注意,两个属性只能选址一个;

    另外,如果使用了mybatis-generator-maven-plugin,那么在pom.xml中定义的properties都可以直接在generatorConfig.xml中使用
<properties resource="" url="" />
 -->

 <!-- 在MBG工作的时候,需要额外加载的依赖包
     location属性指明加载jar/zip包的全路径
     --!>
     <!-- windows下路径, D:\downloads\xxx.jar -->
<classPathEntry location="/Volumes/doc/jar/mysql-connector-java-5.1.6.jar" />

<!-- 
    context:生成一组对象的环境 
    id:必选,上下文id,用于在生成错误时提示
    defaultModelType:指定生成对象的样式
        1,conditional:类似hierarchical;
        2,flat:所有内容(主键,blob)等全部生成在一个对象中;
        3,hierarchical:主键生成一个XXKey对象(key class),Blob等单独生成一个对象,其他简单属性在一个对象中(record class)
    targetRuntime:
        1,MyBatis3:默认的值,生成基于MyBatis3.x以上版本的内容,包括XXXBySample;
        2,MyBatis3Simple:类似MyBatis3,只是不生成XXXBySample;
    introspectedColumnImpl:类全限定名,用于扩展MBG
-->
<context id="mysql" defaultModelType="hierarchical" targetRuntime="MyBatis3Simple" >

    <!-- 自动识别数据库关键字,默认false,如果设置为true,根据SqlReservedWords中定义的关键字列表;
        一般保留默认值,遇到数据库关键字(Java关键字),使用columnOverride覆盖
     -->
    <property name="autoDelimitKeywords" value="false"/>
    <!-- 生成的Java文件的编码 -->
    <property name="javaFileEncoding" value="UTF-8"/>
    <!-- 格式化java代码 -->
    <property name="javaFormatter" value="org.mybatis.generator.api.dom.DefaultJavaFormatter"/>
    <!-- 格式化XML代码 -->
    <property name="xmlFormatter" value="org.mybatis.generator.api.dom.DefaultXmlFormatter"/>

    <!-- beginningDelimiter和endingDelimiter:指明数据库的用于标记数据库对象名的符号,比如ORACLE就是双引号,MYSQL默认是`反引号; -->
    <property name="beginningDelimiter" value="`"/>
    <property name="endingDelimiter" value="`"/>

    <!-- 必须要有的,使用这个配置链接数据库
        @TODO:是否可以扩展
     -->
    <jdbcConnection driverClass="com.mysql.jdbc.Driver" connectionURL="jdbc:mysql:///pss" userId="root" password="admin">
        <!-- 这里面可以设置property属性,每一个property属性都设置到配置的Driver上 -->
    </jdbcConnection>

    <!-- java类型处理器 
        用于处理DB中的类型到Java中的类型,默认使用JavaTypeResolverDefaultImpl;
        注意一点,默认会先尝试使用Integer,Long,Short等来对应DECIMAL和 NUMERIC数据类型; 
    -->
    <javaTypeResolver type="org.mybatis.generator.internal.types.JavaTypeResolverDefaultImpl">
        <!-- 
            true:使用BigDecimal对应DECIMAL和 NUMERIC数据类型
            false:默认,
                scale>0;length>18:使用BigDecimal;
                scale=0;length[10,18]:使用Long;
                scale=0;length[5,9]:使用Integer;
                scale=0;length<5:使用Short;
         -->
        <property name="forceBigDecimals" value="false"/>
    </javaTypeResolver>

    <!-- java模型创建器,是必须要的元素
        负责:1,key类(见context的defaultModelType);2,java类;3,查询类
        targetPackage:生成的类要放的包,真实的包受enableSubPackages属性控制;
        targetProject:目标项目,指定一个存在的目录下,生成的内容会放到指定目录中,如果目录不存在,MBG不会自动建目录
     -->
    <javaModelGenerator targetPackage="com._520it.mybatis.domain" targetProject="src/main/java">
        <!--  for MyBatis3/MyBatis3Simple
            自动为每一个生成的类创建一个构造方法,构造方法包含了所有的field;而不是使用setter;
         -->
        <property name="constructorBased" value="false"/>

        <!-- 在targetPackage的基础上,根据数据库的schema再生成一层package,最终生成的类放在这个package下,默认为false -->
        <property name="enableSubPackages" value="true"/>

        <!-- for MyBatis3 / MyBatis3Simple
            是否创建一个不可变的类,如果为true,
            那么MBG会创建一个没有setter方法的类,取而代之的是类似constructorBased的类
         -->
        <property name="immutable" value="false"/>

        <!-- 设置一个根对象,
            如果设置了这个根对象,那么生成的keyClass或者recordClass会继承这个类;在Table的rootClass属性中可以覆盖该选项
            注意:如果在key class或者record class中有root class相同的属性,MBG就不会重新生成这些属性了,包括:
                1,属性名相同,类型相同,有相同的getter/setter方法;
         -->
        <property name="rootClass" value="com._520it.mybatis.domain.BaseDomain"/>

        <!-- 设置是否在getter方法中,对String类型字段调用trim()方法 -->
        <property name="trimStrings" value="true"/>
    </javaModelGenerator>

    <!-- 生成SQL map的XML文件生成器,
        注意,在Mybatis3之后,我们可以使用mapper.xml文件+Mapper接口(或者不用mapper接口),
            或者只使用Mapper接口+Annotation,所以,如果 javaClientGenerator配置中配置了需要生成XML的话,这个元素就必须配置
        targetPackage/targetProject:同javaModelGenerator
     -->
    <sqlMapGenerator targetPackage="com._520it.mybatis.mapper" targetProject="src/main/resources">
        <!-- 在targetPackage的基础上,根据数据库的schema再生成一层package,最终生成的类放在这个package下,默认为false -->
        <property name="enableSubPackages" value="true"/>
    </sqlMapGenerator>

    <!-- 对于mybatis来说,即生成Mapper接口,注意,如果没有配置该元素,那么默认不会生成Mapper接口 
        targetPackage/targetProject:同javaModelGenerator
        type:选择怎么生成mapper接口(在MyBatis3/MyBatis3Simple下):
            1,ANNOTATEDMAPPER:会生成使用Mapper接口+Annotation的方式创建(SQL生成在annotation中),不会生成对应的XML;
            2,MIXEDMAPPER:使用混合配置,会生成Mapper接口,并适当添加合适的Annotation,但是XML会生成在XML中;
            3,XMLMAPPER:会生成Mapper接口,接口完全依赖XML;
        注意,如果context是MyBatis3Simple:只支持ANNOTATEDMAPPER和XMLMAPPER
    -->
    <javaClientGenerator targetPackage="com._520it.mybatis.mapper" type="ANNOTATEDMAPPER" targetProject="src/main/java">
        <!-- 在targetPackage的基础上,根据数据库的schema再生成一层package,最终生成的类放在这个package下,默认为false -->
        <property name="enableSubPackages" value="true"/>

        <!-- 可以为所有生成的接口添加一个父接口,但是MBG只负责生成,不负责检查
        <property name="rootInterface" value=""/>
         -->
    </javaClientGenerator>

    <!-- 选择一个table来生成相关文件,可以有一个或多个table,必须要有table元素
        选择的table会生成一下文件:
        1,SQL map文件
        2,生成一个主键类;
        3,除了BLOB和主键的其他字段的类;
        4,包含BLOB的类;
        5,一个用户生成动态查询的条件类(selectByExample, deleteByExample),可选;
        6,Mapper接口(可选)

        tableName(必要):要生成对象的表名;
        注意:大小写敏感问题。正常情况下,MBG会自动的去识别数据库标识符的大小写敏感度,在一般情况下,MBG会
            根据设置的schema,catalog或tablename去查询数据表,按照下面的流程:
            1,如果schema,catalog或tablename中有空格,那么设置的是什么格式,就精确的使用指定的大小写格式去查询;
            2,否则,如果数据库的标识符使用大写的,那么MBG自动把表名变成大写再查找;
            3,否则,如果数据库的标识符使用小写的,那么MBG自动把表名变成小写再查找;
            4,否则,使用指定的大小写格式查询;
        另外的,如果在创建表的时候,使用的""把数据库对象规定大小写,就算数据库标识符是使用的大写,在这种情况下也会使用给定的大小写来创建表名;
        这个时候,请设置delimitIdentifiers="true"即可保留大小写格式;

        可选:
        1,schema:数据库的schema;
        2,catalog:数据库的catalog;
        3,alias:为数据表设置的别名,如果设置了alias,那么生成的所有的SELECT SQL语句中,列名会变成:alias_actualColumnName
        4,domainObjectName:生成的domain类的名字,如果不设置,直接使用表名作为domain类的名字;可以设置为somepck.domainName,那么会自动把domainName类再放到somepck包里面;
        5,enableInsert(默认true):指定是否生成insert语句;
        6,enableSelectByPrimaryKey(默认true):指定是否生成按照主键查询对象的语句(就是getById或get);
        7,enableSelectByExample(默认true):MyBatis3Simple为false,指定是否生成动态查询语句;
        8,enableUpdateByPrimaryKey(默认true):指定是否生成按照主键修改对象的语句(即update);
        9,enableDeleteByPrimaryKey(默认true):指定是否生成按照主键删除对象的语句(即delete);
        10,enableDeleteByExample(默认true):MyBatis3Simple为false,指定是否生成动态删除语句;
        11,enableCountByExample(默认true):MyBatis3Simple为false,指定是否生成动态查询总条数语句(用于分页的总条数查询);
        12,enableUpdateByExample(默认true):MyBatis3Simple为false,指定是否生成动态修改语句(只修改对象中不为空的属性);
        13,modelType:参考context元素的defaultModelType,相当于覆盖;
        14,delimitIdentifiers:参考tableName的解释,注意,默认的delimitIdentifiers是双引号,如果类似MYSQL这样的数据库,使用的是`(反引号,那么还需要设置context的beginningDelimiter和endingDelimiter属性)
        15,delimitAllColumns:设置是否所有生成的SQL中的列名都使用标识符引起来。默认为false,delimitIdentifiers参考context的属性

        注意,table里面很多参数都是对javaModelGenerator,context等元素的默认属性的一个复写;
     -->
    <table tableName="userinfo" >

        <!-- 参考 javaModelGenerator 的 constructorBased属性-->
        <property name="constructorBased" value="false"/>

        <!-- 默认为false,如果设置为true,在生成的SQL中,table名字不会加上catalog或schema; -->
        <property name="ignoreQualifiersAtRuntime" value="false"/>

        <!-- 参考 javaModelGenerator 的 immutable 属性 -->
        <property name="immutable" value="false"/>

        <!-- 指定是否只生成domain类,如果设置为true,只生成domain类,如果还配置了sqlMapGenerator,那么在mapper XML文件中,只生成resultMap元素 -->
        <property name="modelOnly" value="false"/>

        <!-- 参考 javaModelGenerator 的 rootClass 属性 
        <property name="rootClass" value=""/>
         -->

        <!-- 参考javaClientGenerator 的  rootInterface 属性
        <property name="rootInterface" value=""/>
        -->

        <!-- 如果设置了runtimeCatalog,那么在生成的SQL中,使用该指定的catalog,而不是table元素上的catalog 
        <property name="runtimeCatalog" value=""/>
        -->

        <!-- 如果设置了runtimeSchema,那么在生成的SQL中,使用该指定的schema,而不是table元素上的schema 
        <property name="runtimeSchema" value=""/>
        -->

        <!-- 如果设置了runtimeTableName,那么在生成的SQL中,使用该指定的tablename,而不是table元素上的tablename 
        <property name="runtimeTableName" value=""/>
        -->

        <!-- 注意,该属性只针对MyBatis3Simple有用;
            如果选择的runtime是MyBatis3Simple,那么会生成一个SelectAll方法,如果指定了selectAllOrderByClause,那么会在该SQL中添加指定的这个order条件;
         -->
        <property name="selectAllOrderByClause" value="age desc,username asc"/>

        <!-- 如果设置为true,生成的model类会直接使用column本身的名字,而不会再使用驼峰命名方法,比如BORN_DATE,生成的属性名字就是BORN_DATE,而不会是bornDate -->
        <property name="useActualColumnNames" value="false"/>

        <!-- generatedKey用于生成生成主键的方法,
            如果设置了该元素,MBG会在生成的<insert>元素中生成一条正确的<selectKey>元素,该元素可选
            column:主键的列名;
            sqlStatement:要生成的selectKey语句,有以下可选项:
                Cloudscape:相当于selectKey的SQL为: VALUES IDENTITY_VAL_LOCAL()
                DB2       :相当于selectKey的SQL为: VALUES IDENTITY_VAL_LOCAL()
                DB2_MF    :相当于selectKey的SQL为:SELECT IDENTITY_VAL_LOCAL() FROM SYSIBM.SYSDUMMY1
                Derby      :相当于selectKey的SQL为:VALUES IDENTITY_VAL_LOCAL()
                HSQLDB      :相当于selectKey的SQL为:CALL IDENTITY()
                Informix  :相当于selectKey的SQL为:select dbinfo('sqlca.sqlerrd1') from systables where tabid=1
                MySql      :相当于selectKey的SQL为:SELECT LAST_INSERT_ID()
                SqlServer :相当于selectKey的SQL为:SELECT SCOPE_IDENTITY()
                SYBASE      :相当于selectKey的SQL为:SELECT @@IDENTITY
                JDBC      :相当于在生成的insert元素上添加useGeneratedKeys="true"和keyProperty属性
        <generatedKey column="" sqlStatement=""/>
         -->

        <!-- 
            该元素会在根据表中列名计算对象属性名之前先重命名列名,非常适合用于表中的列都有公用的前缀字符串的时候,
            比如列名为:CUST_ID,CUST_NAME,CUST_EMAIL,CUST_ADDRESS等;
            那么就可以设置searchString为"^CUST_",并使用空白替换,那么生成的Customer对象中的属性名称就不是
            custId,custName等,而是先被替换为ID,NAME,EMAIL,然后变成属性:id,name,email;

            注意,MBG是使用java.util.regex.Matcher.replaceAll来替换searchString和replaceString的,
            如果使用了columnOverride元素,该属性无效;

        <columnRenamingRule searchString="" replaceString=""/>
         -->

         <!-- 用来修改表中某个列的属性,MBG会使用修改后的列来生成domain的属性;
             column:要重新设置的列名;
             注意,一个table元素中可以有多个columnOverride元素哈~
          -->
         <columnOverride column="username">
             <!-- 使用property属性来指定列要生成的属性名称 -->
             <property name="property" value="userName"/>

             <!-- javaType用于指定生成的domain的属性类型,使用类型的全限定名
             <property name="javaType" value=""/>
              -->

             <!-- jdbcType用于指定该列的JDBC类型 
             <property name="jdbcType" value=""/>
              -->

             <!-- typeHandler 用于指定该列使用到的TypeHandler,如果要指定,配置类型处理器的全限定名
                 注意,mybatis中,不会生成到mybatis-config.xml中的typeHandler
                 只会生成类似:where id = #{id,jdbcType=BIGINT,typeHandler=com._520it.mybatis.MyTypeHandler}的参数描述
             <property name="jdbcType" value=""/>
             -->

             <!-- 参考table元素的delimitAllColumns配置,默认为false
             <property name="delimitedColumnName" value=""/>
              -->
         </columnOverride>

         <!-- ignoreColumn设置一个MGB忽略的列,如果设置了改列,那么在生成的domain中,生成的SQL中,都不会有该列出现 
             column:指定要忽略的列的名字;
             delimitedColumnName:参考table元素的delimitAllColumns配置,默认为false

             注意,一个table元素中可以有多个ignoreColumn元素
         <ignoreColumn column="deptId" delimitedColumnName=""/>
         -->
    </table>

</context>

</generatorConfiguration>
本文由博客一文多发平台 OpenWrite 发布!
查看原文

赞 0 收藏 0 评论 0

认证与成就

  • 获得 67 次点赞
  • 获得 2 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 2 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

注册于 2018-07-11
个人主页被 4k 人浏览