Volantis

A Wonderful Theme for Hexo

1.准备

1
yum install build-essential openssl openssl-devel unixODBC unixODBC-devel make gcc gcc-c++ kernel-devel m4 ncurses-devel tk tc xz

2.下载

1
2
3
wget www.rabbitmq.com/releases/erlang/erlang-18.3-1.el7.centos.x86_64.rpm 
wget http://repo.iotti.biz/CentOS/7/x86_64/socat-1.7.3.2-5.el7.lux.x86_64.rpm
wget www.rabbitmq.com/releases/rabbitmq-server/v3.6.5/rabbitmq-server-3.6.5-1.noarch.rpm

3.配置

1
2
3
4
5
6
# 修改hostname
vi /etc/hostname
# 修改hosts(配置集群时可以配置多个节点)
vi /etc/hosts
# 关闭防火墙
systemctl stop firewalld.service

4.软件安装

1
2
3
4
5
6
# 安装erlang
rpm -ivh erlang-18.3-1.el7.centos.x86_64.rpm
# 安装socat
rpm -ivh socat-1.7.3.2-5.el7.lux.x86_64.rpm
# 安装rabbitmq
rpm -ivh rabbitmq-server-3.6.5-1.noarch.rpm

5.启用guest用户

1
2
3
# 修改配置文件rabbit.app
vi /usr/lib/rabbitmq/lib/rabbitmq_server-3.6.5/ebin/rabbit.app
# 比如修改密码、配置等等,例如:loopback_users中的<<”guest”>>, 只保留guest

6.启动服务

1
2
3
4
# 启动
rabbitmq-server start &
# 停止
rabbitmqctl stop_app

7.管理插件:

1
2
3
4
# 查看所有插件
rabbitmq-plugins list
# 启动rabbitmq_management
rabbitmq-plugins enable rabbitmq_management

5.访问地址

1
2
3
# 访问rabbitmq管理后台
http://{你的linux服务器ip}:15672
# 使用用户名guest,密码guest登录

ThreadLocal提供线程本地变量。它与普通变量的区别在于,在不同的线程访问ThreadLocal(通过getset方法)拥有自己的独立副本。ThreadLocal实例一般用private static修饰。

1.ThreadLocal用法

1.1 ThreadLocal常用方法

1
2
3
4
5
6
7
8
// 返回当前线程中thread-local变量副本的值,如果当前线程的变量没有值,则该变量会被方法initialValue初始化
public T get();

// 设置当前线程的thread-local变量副本为指定的值
public void set(T value);

// 移除当前线程的thread-local变量副本的值
public void remove();

1.2 ThreadLocal使用案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class ThreadLocalTest {

// (1) 创建线程变量
private static ThreadLocal<String> threadLocal = new ThreadLocal<>();

public static void main(String[] args) {
// (2) 设置线程变量
threadLocal.set("hello world");
// (3) 启动子线程
Thread thread = new Thread(() -> {
// (4) 子线程输出变量的值
System.out.println("thread: " + threadLocal.get());
});
thread.start();

// (5) 主线程输出线程变量的值
System.out.println("main: " + threadLocal.get());
}

}

以上实例代码运行输出内容为:

1
2
main: hello world
thread: null

代码(2)处设置了主线程的ThreadLocal副本值为hello world,故代码(5)输出为main: hello world;
代码(4)处输出子线程中ThreadLocal副本值,因为在子线程中未进行设置,故输出thread: null。

2.源码分析

2.1 set方法

  • 获取当前Thread实例t
  • 根据t获取ThreadLocalMap实例map
    • 如果map不为空,则以this(ThreadLocal对象)为key,value(泛型T)为value存入map
    • 如果map为空,则以this为firstKey,value为firstValue创建ThreadLocalMap,并赋值给t.threadLocals
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}

ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}

void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}

2.2 get方法

  • 获取当前Thread实例t
  • 根据t获取ThreadLocalMap实例map
    • 如果map不为空,则以this(ThreadLocal对象)为key获取ThreadLocalMap.Entry实例e,如果e不为空,则返回e.value
    • 如果map为空,返回setInitialValue方法的值(方法setInitialValue返回的是方法initialValue的返回值)
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
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
// (1) 获取对应的ThreadLocalMap.Entry实例
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}

private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
// (1)处获取的Entry实例为空,会重新创建一个Entry
map.set(this, value);
else
createMap(t, value);
return value;
}

protected T initialValue() {
return null;
}

2.3 ThreadLocalMap

ThreadLocalMap是只适用于管理线程本地变量的定制化的哈希表。

  • ThreadLocalMap常用方法
1
2
3
4
5
6
7
8
// 获取key关联的Entry
private Entry getEntry(ThreadLocal<?> key);

// 设置value关联key
private void set(ThreadLocal<?> key, Object value);

// 移除key对应的Entry
private void remove(ThreadLocal<?> key);
  • Entry
1
2
3
4
5
6
7
8
9
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;

Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}

参考链接:

当涉及校验用户输入时,如果由开发人员编码校验数据,那么验证数据的代码和业务逻辑代码会耦合在一起。Spring Framework 4.0支持Bean Validation 1.0(JSR-303)和Bean Validation 1.1(JSR-349),也将其改写成了Spring的Validator接口。使用Spring Validation可以方便的进行数据校验。

1.引入依赖

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>

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
package io.github.lmy.springbootinaction.request;

import io.github.lmy.springbootinaction.entity.User;
import lombok.Data;
import org.hibernate.validator.constraints.Range;
import org.springframework.beans.BeanUtils;

import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;

@Data
public class UserRequest {

@NotBlank(message = "用户名不能为空")
private String name;

@Range(min = 0, max = 150, message = "年龄格式不正确")
@NotNull
private Integer age;

@Email(message = "邮箱格式不正确")
@NotBlank(message = "邮箱不能为空")
private String email;

@Pattern(regexp = "^1(3|4|5|7|8)\\d{9}$", message = "手机号码格式不正确")
@NotBlank(message = "手机号不能为空")
private String phone;

@NotBlank(message = "密码不能为空")
private String password;

}

3.在Controller层校验数据

对需要校验的参数添加注解@Valid或@Validated,如果需要获取校验的信息,可以在被校验参数后面添加一个BindingResult参数,Spring会将校验的信息填充到该校验参数后面的BindingResult中。

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
package io.github.lmy.springbootinaction.controller;

import io.github.lmy.springbootinaction.request.LoginRequest;
import io.github.lmy.springbootinaction.request.UserRequest;
import io.github.lmy.springbootinaction.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.validation.Valid;

@Controller
@RequestMapping("/user")
@Slf4j
public class UserController {

@Autowired
private UserService userService;

@PostMapping("/register")
@ResponseBody
public boolean register(@Valid @RequestBody UserRequest userRequest, BindingResult result) {
if (result.hasErrors()) {
for (FieldError fieldError : result.getFieldErrors()) {
log.info("field: {}, message: {}", fieldError.getField(), fieldError.getDefaultMessage());
}
return false;
}
boolean success = userService.register(userRequest);
return success;
}

@PostMapping("/login")
public String login(@Validated @RequestBody LoginRequest loginRequest) {
userService.login(loginRequest);
return "index";
}

}

4.校验失败统一处理

如果校验参数后没有BindingResult参数,且校验参数校验失败,则会抛出MethodArgumentNotValidException 异常。可以使用 @ExceptionHandler 注解统一处理参数校验失败。

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
package io.github.lmy.springbootinaction.global;

import org.springframework.http.HttpStatus;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.util.Map;
import java.util.stream.Collectors;

@RestControllerAdvice
public class GlobalExceptionHandler {

@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(MethodArgumentNotValidException.class)
public Map<String, String> handleValidationExceptions(MethodArgumentNotValidException ex) {
Map<String, String> result = ex.getBindingResult()
.getFieldErrors()
.stream()
.collect(Collectors.toMap(FieldError::getField, FieldError::getDefaultMessage));
return result;
}

}

参考链接:

java7引入try-with-resource语法,允许在try代码块声明一个或多个资源,可以确保代码块执行完毕资源被关闭。

1.使用try-catch-finally

在java7之前,我们需要在finally代码块来关闭资源,确保程序即使在try发生异常,资源也能正确的被关闭。

以下的例子,从硬盘中读取文件file.txt的内容,输出至控制台。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Test
public void testTryCatchFinally() {
BufferedReader br = null;
try {
br = new BufferedReader(new FileReader(new File("D://Resource/file.txt")));
String line;
while((line = br.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (br != null) {
br.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}

2.使用try-with-resource替代

从java7开始,你可以使用try-with-resource来替代try-catch-finally,简化代码。资源对象需要在try代码块中被声明和初始化,这样资源就会被自动的关闭。

try-with-resource语法使用的时候需要注意以下几点:

  • try()代码块中可以声明一个或多个resource(resource是指那些实现了java.lang.AutoCloseable或java.io.Closeable的对象),多个资源用分号分割
  • try-with-resource语法会自动处理资源的关闭,资源关闭的顺序跟声明的顺序相反
1
2
3
4
5
6
7
8
9
10
11
@Test
public void testTryWithResource() {
try(BufferedReader br = new BufferedReader(new FileReader(new File("D://Resource/file.txt")))) {
String line;
while((line = br.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}
}

3.从字节码查看try-with-resource

使用jd-gui查看方法testTryWithResource生成的字节码,可以看到try-with-resource主要做了一下2点处理

  • finally代码块中执行了资源的关闭
  • 如果在try代码块和finally代码块中都发生了异常,则会抛出try代码块捕获的异常,并抑制了finally代码块中异常
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
@Test
public void testTryWithResource() {
try {
BufferedReader br = new BufferedReader(new FileReader(new File("D://Resource/file.txt")));Throwable localThrowable3 = null;
try {
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
} catch (Throwable localThrowable1) {
localThrowable3 = localThrowable1;throw localThrowable1;
} finally {
if (br != null) {
if (localThrowable3 != null) {
try {
br.close();
} catch (Throwable localThrowable2) {
localThrowable3.addSuppressed(localThrowable2);
}
} else {
br.close();
}
}
}
}
catch (IOException e) {
e.printStackTrace();
}
}

参考链接:

1.配置YUM源

  • 1.1 下载YUM源rpm安装包

在MySQL官网中下载YUM源rpm安装包:http://dev.mysql.com/downloads/repo/yum/

  • 1.2 安装mysql源
1
yum localinstall mysql80-community-release-el7-3.noarch.rpm
  • 1.3 检查mysql源是否安装成功
1
yum repolist enabled | grep "mysql.*-community.*"

t6ByXn.png

看到上图所示表示安装成功。

  • 1.4 修改默认安装的mysql版本

当前文件的mysql版本为5.6,若要改为5.7,则将5.7的enabled改为1,将5.6的enabled改为0即可。

t6Bcmq.png

2.安装MySQL

1
yum install mysql-community-server

3.启动MySQL

1
2
3
4
# 启动MySQL
systemctl start mysqld
# 查看MySQL启动状态
systemctl status mysqld

t6Bg00.png

4.修改root用户密码

  • 4.1 直接输入mysql就可以进入MySQL了
  • 4.2 修改root用户密码
1
set password = password('root');

5.开启用户远程访问

1
GRANT ALL PRIVILEGES ON *.*TO 'root'@'%' IDENTIFIED BY 'root' WITH GRANT OPTION;

6.开放3306端口

开启端口后,可以在外部使用mysql连接工具连接,具体操作如下:

1
2
3
4
5
6
7
8
# 查看防火墙状态
firewall-cmd --state
# 在running状态下,向firewall添加需要开放的端口
firewall-cmd --permanent --zone=public --add-port=3306/tcp
# 加载配置,使得修改有效
firewall-cmd --reload
# 查看开启的端口,出现3306/tcp则开启正确
firewall-cmd --permanent --zone=public --list-ports

参考链接:

1.检查JDK环境

通过java -version命令检查系统是否安装JDK

t6raRg.png

1
java -version

2.下载Tomcat及安装

  • 2.1 去Apache Tomcat官网下载Tomcat9

  • 2.2 使用WinSCP上传安装包至/usr/local/目录下

  • 2.3 安装tomcat

1
2
3
4
5
6
# 解压压缩包
tar -zxvf apache-tomcat-9.0.19.tar.gz
# 删除压缩包
rm apache-tomcat-9.0.19.tar.gz
# 重命名
mv apache-tomcat-9.0.19 tomcat-9.0.19

3.启动Tomcat

  • 3.1 启动tomcat
1
/usr/local/tomcat-9.0.19/bin/startup.sh

t6rdzQ.png

  • 3.2 查看tomcat进程
1
ps -ef|grep java

t6rUJS.png

  • 3.3 查看tomcat欢迎页
1
curl http://localhost:8080

4.配置端口

在linux上开启的tomcat使用浏览器访问不了。
主要原因在于防火墙的存在,导致的端口无法访问。
CentOS7使用firewall而不是iptables。所以解决这类问题可以通过添加firewall的端口,使其对我们需要用的端口开放。

  • 4.1 查看防火墙状态
1
firewall-cmd --state
  • 4.2 在running 状态下,向firewall 添加需要开放的端口
1
2
# 永久的添加该端口。去掉--permanent则表示临时。
firewall-cmd --permanent --zone=public --add-port=8080/tcp
  • 4.3 加载配置,使得修改有效
1
firewall-cmd --reload
  • 4.4 查看开启的端口,出现8080/tcp则开启正确
1
firewall-cmd --permanent --zone=public --list-ports
  • 4.5 端口添加完后可以外部浏览器访问

t6r0Mj.png

参考链接:

1.下载JDK

Oracle官网下载JDK8,根据CentOS的版本下载对应的32位或64位安装包。

  • 查看CentOS是32位还是64位可以用以下命令(如果有x86_64就是64位的,没有就是32位的;后面是X686或X86_64则内核是64位的,i686或i386则内核是32位的)
1
uname -a # 用于打印当前系统相关信息(内核版本号、硬件架构、主机名称和操作系统类型等)
  • 在centos系统中执行uname -a指令,系统为64位,下载文件jdk-8u211-linux-x64.tar.gz

t6snwq.png

2.传输文件及解压

  • 2.1 在/usr/local/下建立java文件夹,使用WinSCP上传jdk至java文件夹里面

  • 2.2 解压文件

1
tar -zxvf jdk-8u211-linux-x64.tar.gz
  • 2.3 删除压缩包
1
rm jdk-8u211-linux-x64.tar.gz

3.配置环境变量

  • 3.1 修改/etc/profile文件
1
vi /etc/profile
  • 3.2 在文件末尾追加以下内容
1
2
3
4
5
6
7
# 配置JAVA_HOME环境变量
export JAVA_HOME=/usr/local/java/jdk1.8.0_211
# 在PATH路径上追加$JAVA_HOME/bin目录
export PATH=$JAVA_HOME/bin:$PATH
# 配置CLASSPATH,tools.jar用于编译,dt.jar是Swing需要用到的
# 如果CLASSPATH中不包括当前路径.,JRE就不会在当前路径下搜索Java类
export CLASSPATH=.:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar
  • 3.3 使文件修改生效
1
2
# source命令也称为“点命令”,也就是一个点符号(.),是bash的内部命令。 source命令通常用于重新执行刚修改的初始化文件,使之立即生效,而不必注销并重新登录。
source /etc/profile

4.查看安装jdk是否成功

1
java -version

参考链接:

INSERT

  • 标准插入语法
1
INSERT [INTO] tbl_name [(col_name,...)] {VALUES|VALUE} ({expr|DEFAULT},...),(...),...
  • 可以使用子查询,但是不能插入多条记录
1
INSERT [INTO] tbl_name SET col_name={expr|DEFAULT},...
  • 可以将查询结果插入到指定数据表
1
INSERT [INTO] tbl_name [(col_name,...)] SELECT ...

UPDATE

1
UPDATE [LOW_PRIORITY] [IGNORE] table_reference SET col_name1={expr1|DEFAULT} [,col_name2={expr2|DEFAULT}] ... [WHERE where_condition]

DELETE

1
DELETE FROM tbl_name [WHERE where_condition]

SELECT

1
2
3
4
5
6
7
8
9
SELECT select_expr [,select_expr ...]
[
FROM table_references
[WHERE where_condition]
[GROUP BY {col_name|position} [ASC|DESC], ...]
[HAVING where_condition]
[ORDER BY {col_name|expr|position} [ASC|DESC], ...]
[LIMIT {[offset,] row_count}|row_count OFFSET offset]
]
  • 查询表达式(select_expr)

每一个表达式表示想要的一列,必须有至少一个。

多个列之间以英文逗号分隔。

星号(*)表示所有列。tbl_name. *可以表示命名表的所有列。

查询表达式可以用[AS] alias_name为其赋予别名。

别名可用于GROUP BY, ORDER BY或HAVING字句。

  • WHERE条件表达式

对记录进行过滤,如果没有指定WHERE子句,则显示所有记录。

在WHERE表达式中,可以使用MySQL支持的函数或运算符。

约束

  • 1.约束保证数据的完整性和一致性。
  • 2.约束分为表级约束和列级约束。
  • 3.约束类型包括:
    NOT NULL(非空约束)
    PRIMARY KEY(主键约束)
    UNIQUE KEY(唯一约束)
    DEFAULT(默认约束)
    FOREIGN KEY(外键约束)

    外键约束

保证数据一致性,完整性。
实现一对一或一对多关系。

  • 外键约束的要求

1.父表和子表必须使用相同的存储引擎,而且禁止使用临时表。
2.数据表的存储引擎只能为InnoDB。
3.外键列和参照列必须具有相似的数据类型。其中数字的长度或是否有符号位必须相同;而字符的长度则可以不同。
4.外键列和参照列必须创建索引。如果外键列不存在索引的话,MySQL将自动创建索引。

  • 编辑数据表的默认存储引擎

MySQL配置文件

1
default-storage-engine=INNODB
  • 外键约束的参照操作
参考操作 说明
CASCADE 从父表删除或更新且自动删除或更新子表中匹配的行
SET NULL 从父表删除或更新行,并设置子表中的外键列为NULL。如果使用该选项,必须保证子表列没有指定NOT NULL
RESTRICT 拒绝对父表的删除或更新操作
NO ACTION 标准SQL的关键字,在MySQL中与RESTRICT相同
  • 表级约束与列级约束

对一个数据列建立的约束,称为列级约束。
对多个数据列建立的约束,称为表级约束。
列级约束既可以在列定义时声明,也可以在列定义后声明。
表级约束只能在列定义后声明。

  • 修改数据表

添加单列

1
ALTER TABLE tbl_name ADD [COLUMN] col_name column_definition [FIRST | AFTER col_name]

添加多列

1
ALTER TABLE tbl_name ADD [COLUMN] (col_name column_definition,...)

删除列

1
ALTER TABLE tbl_name DROP [COLUMN] col_name

对某张表执行多个动作,如删除多列

1
ALTER TABLE tbl_name DROP [COLUMN] col_name, DROP [COLUMN] col_name

添加主键约束

1
ALTER TABLE tbl_name ADD [CONSTRAINT[symbol]] PRIMARY KEY [index_type] (index_col_name,...)

添加唯一约束

1
ALTER TABLE tbl_name ADD [CONSTRAINT[symbol]] UNIQUE [INDEX|KEY] [index_name] [index_type] (index_col_name,...)

添加外键约束

1
ALTER TABLE tbl_name ADD [CONSTRAINT[symbol]] FOREIGN KEY [index_name] (index_col_name) REFERENCES tbl_name (col_namme)

添加或删除默认约束

1
ALTER TABLE tbl_name ALTER [COLUMN] col_name {SET DEFAULT literal|DROP DEFAULT}

删除主键约束

1
ALTER TABLE tbl_name DROP PRIMARY KEY

删除唯一约束

1
ALTER TABLE tbl_name DROP {INDEX|KEY} index_name

删除外键约束

1
ALTER TABLE tbl_name DROP FOREIGN KEY fk_symbol

修改列定义

1
ALTER TABLE tbl_name MODIFY [COLUMN] col_name column_definition [FIRST|AFTER col_name]

修改列名称

1
ALTER TABLE tbl_name CHANGE [COLUMN] old_col_name new_col_name column_definition [FIRST|AFTER col_name]

修改表名

1
2
ALTER TABLE tbl_name RENAME [TO|AS] new_tbl_name
RENAME TABLE tbl_name TO new_tbl_name [,tbl_name2 TO new_tbl_name2]...

1. MySQL操作数据表

1.1 创建数据表

1
2
3
4
CREATE TABLE [IF NOT EXISTS] table_name (
column_name data_type,
...
)

1.2 查看数据表列表

1
SHOW TABLES [FROM db_name] [LIKE 'pattern' | WHERE expr]

1.3 查看数据表结构

1
SHOW COLUMNS FROM tbl_name;

1.4 插入记录

1
INSERT [INTO] tbl_name [(col_name,...)] VALUES(val,...);

1.5 记录查找

1
SELECT expr,... FROM tbl_name;

2. MySQL约束

2.1 空值与非空

NULL, 字段值可以为空
NOT NULL,字段值禁止为空

2.2 AUTO_INCREMENT

自动编号,且必须与主键组合使用
默认情况下,起始值为1,每次增量为1

2.3 主键约束(PRIMARY KEY)

每张数据表只能存在一个主键
主键保证记录的唯一性
主键自动为NOT NULL

2.4 唯一约束(UNIQUE KEY)

唯一约束可以保证记录的唯一性
唯一约束的字段可以为空值(NULL)
每张数据表可以存在多个唯一约束

2.5 默认值(DEFAULT)

当插入记录时,如果没有明确为字段赋值,则自动赋予默认值。