2024年3月

今天我们将继续进行爬虫实战,除了常规的网页数据抓取外,我们还将引入一个全新的下载功能。具体而言,我们的主要任务是爬取小说内容,并实现将其下载到本地的操作,以便后续能够进行离线阅读。

为了确保即使在功能逐渐增多的情况下也不至于使初学者感到困惑,我特意为你绘制了一张功能架构图,具体如下所示:

image

让我们开始深入解析今天的主角:小说网

小说解析

书单获取

在小说网的推荐列表中,我们可以选择解析其中的某一个推荐内容,而无需完全还原整个网站页面的显示效果,从而更加高效地获取我们需要的信息。

以下是一个示例代码,帮助你更好地理解:

headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 Edg/122.0.0.0'}
req = Request("https://www.readnovel.com/",headers=headers)
# 发出请求,获取html
# 获取的html内容是字节,将其转化为字符串
html = urlopen(req)
html_text = bytes.decode(html.read())
soup = bf(html_text,'html.parser')

for li in soup.select('#new-book-list li'):
    a_tag = li.select_one('a[data-eid="qd_F24"]')
    p_tag = li.select_one('p')
    book = {
        'href': a_tag['href'],
        'title': a_tag.get('title'),
        'content': p_tag.get_text()
    }
    print(book)

书籍简介

在通常情况下,我们会先查看书单,然后对书籍的大致内容进行了解,因此直接解析相关内容即可。以下是一个示例代码:

headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 Edg/122.0.0.0'}
req = Request(f"https://www.readnovel.com{link}#Catalog",headers=headers)
# 发出请求,获取html
# 获取的html内容是字节,将其转化为字符串
html = urlopen(req)
html_text = bytes.decode(html.read())
soup = bf(html_text,'html.parser')
og_title = soup.find('meta', property='og:title')['content']
og_description = soup.find('meta', property='og:description')['content']
og_novel_author = soup.find('meta', property='og:novel:author')['content']
og_novel_update_time = soup.find('meta', property='og:novel:update_time')['content']
og_novel_status = soup.find('meta', property='og:novel:status')['content']
og_novel_latest_chapter_name = soup.find('meta', property='og:novel:latest_chapter_name')['content']
# 查找内容为"免费试读"的a标签
div_tag = soup.find('div', id='j-catalogWrap')
list_items = div_tag.find_all('li', attrs={'data-rid': True})
for li in list_items:
    link_text = li.find('a').text
    if '第' in link_text:
        link_url = li.find('a')['href']
        link_obj = {'link_text':link_text,
                'link_url':link_url}
        free_trial_link.append(link_obj)
print(f"书名:{og_title}")
print(f"简介:{og_description}")
print(f"作者:{og_novel_author}")
print(f"最近更新:{og_novel_update_time}")
print(f"当前状态:{og_novel_status}")
print(f"最近章节:{og_novel_latest_chapter_name}")

在解析过程中,我们发现除了获取书籍的大致内容外,还顺便解析了相关的书籍目录。将这些目录保存下来会方便我们以后进行试读操作,因为一旦对某本书感兴趣,我们接下来很可能会阅读一下。如果确实对书籍感兴趣,可能还会将其加入书单。为了避免在阅读时再次解析,我们在这里直接保存了这些目录信息。

免费试读

在这一步,我们的主要任务是解析章节的名称以及章节内容,并将它们打印出来,为后续封装成方法以进行下载或阅读做准备。这样做可以更好地组织和管理数据,提高代码的复用性和可维护性。下面是一个示例代码,展示了如何实现这一功能:

headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 Edg/122.0.0.0'}
req = Request(f"https://www.readnovel.com{link}",headers=headers)
# 发出请求,获取html
# 获取的html内容是字节,将其转化为字符串
html = urlopen(req)
html_text = bytes.decode(html.read())
soup = bf(html_text, 'html.parser')
name = soup.find('h1',class_='j_chapterName')
chapter = {
    'name':name.get_text()
}
print(name.get_text())
ywskythunderfont = soup.find('div', class_='ywskythunderfont')
if ywskythunderfont:
    p_tags = ywskythunderfont.find_all('p')
    chapter['text'] = p_tags[0].get_text()
    print(chapter)

小说下载

当我们完成内容解析后,已经成功获取了小说的章节内容,接下来只需执行下载操作即可。对于下载操作的具体步骤,如果有遗忘的情况,我来帮忙大家进行回顾一下。

file_name = 'a.txt'
with open(file_name, 'w', encoding='utf-8') as file:
    file.write('尝试下载')
print(f'文件 {file_name} 下载完成!')

包装一下

按照老规矩,以下是源代码示例。即使你懒得编写代码,也可以直接复制粘贴运行一下,然后自行琢磨其中的细节。这样能够更好地理解代码的运行逻辑和实现方式。

# 导入urllib库的urlopen函数
from urllib.request import urlopen,Request
# 导入BeautifulSoup
from bs4 import BeautifulSoup as bf
from random import choice,sample
from colorama import init
from termcolor import colored
from readchar import  readkey
FGS = ['green', 'yellow', 'blue', 'cyan', 'magenta', 'red']
book_list = []
free_trial_link = []
headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 Edg/122.0.0.0'}

def get_hot_book():
    print(colored('开始搜索书单!',choice(FGS)))
    book_list.clear()
    req = Request("https://www.readnovel.com/",headers=headers)
    # 发出请求,获取html
    # 获取的html内容是字节,将其转化为字符串
    html = urlopen(req)
    html_text = bytes.decode(html.read())
    soup = bf(html_text,'html.parser')

    for li in soup.select('#new-book-list li'):
        a_tag = li.select_one('a[data-eid="qd_F24"]')
        p_tag = li.select_one('p')
        book = {
            'href': a_tag['href'],
            'title': a_tag.get('title'),
            'content': p_tag.get_text()
        }
        book_list.append(book)

def get_book_detail(link):
    global free_trial_link
    free_trial_link.clear()
    req = Request(f"https://www.readnovel.com{link}#Catalog",headers=headers)
    # 发出请求,获取html
    # 获取的html内容是字节,将其转化为字符串
    html = urlopen(req)
    html_text = bytes.decode(html.read())
    soup = bf(html_text,'html.parser')
    og_title = soup.find('meta', property='og:title')['content']
    og_description = soup.find('meta', property='og:description')['content']
    og_novel_author = soup.find('meta', property='og:novel:author')['content']
    og_novel_update_time = soup.find('meta', property='og:novel:update_time')['content']
    og_novel_status = soup.find('meta', property='og:novel:status')['content']
    og_novel_latest_chapter_name = soup.find('meta', property='og:novel:latest_chapter_name')['content']
    # 查找内容为"免费试读"的a标签
    div_tag = soup.find('div', id='j-catalogWrap')
    list_items = div_tag.find_all('li', attrs={'data-rid': True})
    for li in list_items:
        link_text = li.find('a').text
        if '第' in link_text:
            link_url = li.find('a')['href']
            link_obj = {'link_text':link_text,
                    'link_url':link_url}
            free_trial_link.append(link_obj)
    print(colored(f"书名:{og_title}",choice(FGS)))
    print(colored(f"简介:{og_description}",choice(FGS)))
    print(colored(f"作者:{og_novel_author}",choice(FGS)))
    print(colored(f"最近更新:{og_novel_update_time}",choice(FGS)))
    print(colored(f"当前状态:{og_novel_status}",choice(FGS)))
    print(colored(f"最近章节:{og_novel_latest_chapter_name}",choice(FGS)))

def free_trial(link):
    req = Request(f"https://www.readnovel.com{link}",headers=headers)
    # 发出请求,获取html
    # 获取的html内容是字节,将其转化为字符串
    html = urlopen(req)
    html_text = bytes.decode(html.read())
    soup = bf(html_text, 'html.parser')
    name = soup.find('h1',class_='j_chapterName')
    chapter = {
        'name':name.get_text()
    }
    print(colored(name.get_text(),choice(FGS)))
    ywskythunderfont = soup.find('div', class_='ywskythunderfont')
    if ywskythunderfont:
        p_tags = ywskythunderfont.find_all('p')
        chapter['text'] = p_tags[0].get_text()
    return chapter

def download_chapter(chapter):
    file_name = chapter['name'] + '.txt'
    with open(file_name, 'w', encoding='utf-8') as file:
        file.write(chapter['text'].replace('\u3000\u3000', '\n'))
    print(colored(f'文件 {file_name} 下载完成!',choice(FGS)))

def print_book():
    for i in range(0, len(book_list), 3):
        names = [f'{i + j}:{book_list[i + j]["title"]}' for j in range(3) if i + j < len(book_list)]
        print(colored('\t\t'.join(names),choice(FGS)))

def read_book(page):
    if not free_trial_link:
        print(colored('未选择书单,无法阅读!',choice(FGS)))
    
    print(colored(free_trial(free_trial_link[page]['link_url'])['text'],choice(FGS)))

get_hot_book()

init() ## 命令行输出彩色文字
print(colored('已搜索完毕!',choice(FGS)))
print(colored('m:返回首页',choice(FGS)))
print(colored('d:免费试读',choice(FGS)))
print(colored('x:全部下载',choice(FGS)))
print(colored('n:下一章节',choice(FGS)))
print(colored('b:上一章节',choice(FGS)))
print(colored('q:退出阅读',choice(FGS)))
my_key = ['q','m','d','x','n','b']
current = 0
while True:
    while True:
        move = readkey()
        if move in my_key:
            break
    if move == 'q': ## 键盘‘Q’是退出
        break 
    if move == 'd':  
        read_book(current)
    if move == 'x':  ## 这里只是演示为主,不循环下载所有数据了
        download_chapter(free_trial(free_trial_link[0]['link_url']))
    if move == 'b':  
        current = current - 1
        if current < 0 :
            current = 0
        read_book(current)
    if move == 'n':  
        current = current + 1
        if current > len(free_trial_link) :
            current = len(free_trial_link) - 1
        read_book(current)
    if move == 'm':
        print_book()
        current = 0
        num = int(input('请输入书单编号:=====>'))
        if num <= len(book_list):
            get_book_detail(book_list[num]['href'])

总结

今天在爬虫实战中,除了正常爬取网页数据外,我们还添加了一个下载功能,主要任务是爬取小说并将其下载到本地,以便离线阅读。为了避免迷糊,我为大家绘制了功能架构图。我们首先解析了小说网,包括获取书单、书籍简介和免费试读章节。然后针对每个功能编写了相应的代码,如根据书单获取书籍信息、获取书籍详细信息、免费试读章节解析和小说下载。最后,将这些功能封装成方法,方便调用和操作。通过这次实战,我们深入了解了爬虫的应用,为后续的项目提供了基础支持。

相关文章

数据库系列:MySQL慢查询分析和性能优化
数据库系列:MySQL索引优化总结(综合版)
数据库系列:高并发下的数据字段变更
数据库系列:覆盖索引和规避回表
数据库系列:数据库高可用及无损扩容
数据库系列:使用高区分度索引列提升性能
数据库系列:前缀索引和索引长度的取舍
数据库系列:MySQL引擎MyISAM和InnoDB的比较
数据库系列:InnoDB下实现高并发控制
数据库系列:事务的4种隔离级别
数据库系列:RR和RC下,快照读的区别
数据库系列:MySQL InnoDB锁机制介绍
数据库系列:MySQL不同操作分别用什么锁?
数据库系列:业内主流MySQL数据中间件梳理
数据库系列:大厂使用数据库中间件解决什么问题?

1 介绍

在笔者的这篇文章《
构建高性能索引(策略篇)
》中,我们详细讨论了如何设计高质量索引,里面多个地方提及可能导致索引失效的场景。
这边咱们重新梳理下,以枚举的方式来梳理出所有可能出现索引失效的点,避免RD同学们踩坑。

2 数据准备及验证

2.1 数据准备

1、创建两个表:员工表和部门表

/*部门表,存在则进行删除 */
drop table if EXISTS dep;
create table dep(
    id int unsigned primary key auto_increment,
    depno mediumint unsigned not null default 0,
    depname varchar(20) not null default "",
    memo varchar(200) not null default ""
);

/*员工表,存在则进行删除*/
drop table if EXISTS emp;
create table emp(
    id int unsigned primary key auto_increment,
    empno mediumint unsigned not null default 0,
    empname varchar(20) not null default "",
    job varchar(9) not null default "",
    mgr mediumint unsigned not null default 0,
    hiredate datetime not null,
    sal decimal(7,2) not null,
    comn decimal(7,2) not null,
    depno mediumint unsigned not null default 0
);

2、创建两个函数:生成随机字符串和随机编号

/* 产生随机字符串的函数*/
DELIMITER $
drop FUNCTION if EXISTS rand_string;
CREATE FUNCTION rand_string(n INT) RETURNS VARCHAR(255)
BEGIN
    DECLARE chars_str VARCHAR(100) DEFAULT 'abcdefghijklmlopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
    DECLARE return_str VARCHAR(255) DEFAULT '';
    DECLARE i INT DEFAULT 0;
    WHILE i < n DO
    SET return_str = CONCAT(return_str,SUBSTRING(chars_str,FLOOR(1+RAND()*52),1));
    SET i = i+1;
    END WHILE;
    RETURN return_str;
END $
DELIMITER;


/*产生随机部门编号的函数*/
DELIMITER $
drop FUNCTION if EXISTS rand_num;
CREATE FUNCTION rand_num() RETURNS INT(5)
BEGIN
    DECLARE i INT DEFAULT 0;
    SET i = FLOOR(RAND()*100+RAND()*20);
    RETURN i;
END $
DELIMITER;

3、编写存储过程,模拟500W的员工数据

/*建立存储过程:往emp表中插入数据*/
DELIMITER $
drop PROCEDURE if EXISTS insert_emp;
CREATE PROCEDURE insert_emp(IN START INT(10),IN max_num INT(10))
BEGIN
    DECLARE i INT DEFAULT 0;
    /*set autocommit =0 把autocommit设置成0,把默认提交关闭*/
    SET autocommit = 0;
    REPEAT
    SET i = i + 1;
    INSERT INTO emp(empno,empname,job,mgr,hiredate,sal,comn,depno) VALUES ((START+i),rand_string(6),'SALEMAN',0001,now(),2000,400,rand_num());
    UNTIL i = max_num
    END REPEAT;
    COMMIT;
END $
DELIMITER;
/*插入500W条数据*/
call insert_emp(0,5000000);

4、编写存储过程,模拟120条部门数据

/*建立存储过程:往dep表中插入数据*/
DELIMITER $
drop PROCEDURE if EXISTS insert_dept;
CREATE PROCEDURE insert_dept(IN START INT(10),IN max_num INT(10))
BEGIN
    DECLARE i INT DEFAULT 0;
    SET autocommit = 0;
    REPEAT
    SET i = i+1;
    INSERT  INTO dep( depno,depname,memo) VALUES((START+i),rand_string(10),rand_string(8));
    UNTIL i = max_num
    END REPEAT;
    COMMIT;
END $
DELIMITER;
/*插入120条数据*/
call insert_dept(0,120);

5、建立关键字段的索引,这边是跑完数据之后再建索引,会导致建索引耗时长,但是跑数据就会快一些。

# 这边建立一个复合索引,包含 depno(部门编号)、empname(员工姓名)、job(工作岗位)。
create index idx_depno_empname_job on emp(depno,empname,job);

2.2 验证过程

在 MySQL 中建设合理高效的索引是提升检索性能的最有效方式,因为索引可以快速地定位表中的某条记录,达到提高数据库查询的速度的目的。
大多数情况下都(默认)采用B+树来构建索引,我们下面也默认使用InnoDB引擎来举例。

2.2.1 违反最左匹配原则

如下图,b+树的数据项是复合的数据结构,比如(empname,depno,job)这种(即构建一个联合索引)时,b+树是按照从左到右的顺序来建立搜索树的。
示例:
1、当以('brand',106,'SALEMAN')这样的数据来检索的时候,
b+树会优先比较empname来确定下一步的所搜方向,如果empname相同再依次比较depno和job,最后得到检索的数据。

2、但如果是(106,'SALEMAN')这样,没有empname的数据来的时候,b+树就不知道下一步该查哪个节点,
因为empname就是第一个比较因子,必须要先根据empname来搜索才能知道下一步去哪里查询。

3、再比如当('brand','SALEMAN')这样的数据来检索时,b+树可以用empname来指定搜索方向,但下一个字段depno的缺失,
所以只能把名字等于 'brand' 的数据都扫描出来,然后再匹配职位是SALEMAN的数据了。

这个重要特征就是索引的最左匹配原则,按照这个原则执行索引效率特别高。
我们试试在b+树上分析和举例:
下图中是3个字段(depno,empname,job)的联合索引,数据以depno asc,empname asc,job asc这种排序方式存储在节点中的,
排序原则:
1、索引以depno字段升序
2、depno相同时,以empname字段升序,
3、empname相同的时候,以job字段升序

image

第一位置
检索depno=7的记录
由于页中的记录是以depno asc,empname asc,job asc这种排序方式存储的,所以depno字段是有序的,可以通过二分法快速检索到,步骤如下:
1、将Disk1加载到内存中
2、在内存中对Disk1中的记录采用二分法找,可以确定depno=7的记录位于{7,Brand,1}和{7,dyny,1}关联的范围内,这两个所指向的页分别是 Disk2 和 Disk4。
3、加载页Disk2,在Disk2中采用二分法快速找到第一条depno=7的记录,然后通过链表向下一条及下一页开始扫描,直到在Disk4中找到第一个不满足depno=7的记录为止。
image

第一+第二位置
检索depno=7 and empname like 'B%'的记录
步骤跟上面是一致的,可以确定depno=1 and empname like 'B%'的记录位于{7,Band,1}和{7,Bec,1}关联的范围内,查找过程和depno=7查找步骤类似。
image

第二位置
检索empname like 'C%'的记录
这种情况通过Disk1页中的记录,无法判断empname like 'C%' 的记录在哪些页中的,只能逐个加载索引树的页,对所有记录进行遍历,然后进行过滤,此时索引无效。

# 验证脚本:未使用到了索引,全表扫描
mysql> explain select empno,empname,job from emp  where empname like 'C%';
+----+-------------+-------+------------+------+---------------+------+---------+------+---------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key  | key_len | ref  | rows    | filtered | Extra       |
+----+-------------+-------+------------+------+---------------+------+---------+------+---------+----------+-------------+
|  1 | SIMPLE      | emp   | NULL       | ALL  | NULL          | NULL | NULL    | NULL | 4982087 |    11.11 | Using where |
+----+-------------+-------+------------+------+---------------+------+---------+------+---------+----------+-------------+
1 row in set, 1 warning (0.01 sec)

第三位置
检索job=8的记录
这种情况和查询 empname like 'C%' 也一样,也只能扫描所有叶子节点,索引也无效。

# 验证脚本:未使用到了索引,全表扫描
mysql> explain select empno,empname,job from emp  where job=8;
+----+-------------+-------+------------+------+---------------+------+---------+------+---------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key  | key_len | ref  | rows    | filtered | Extra       |
+----+-------------+-------+------------+------+---------------+------+---------+------+---------+----------+-------------+
|  1 | SIMPLE      | emp   | NULL       | ALL  | NULL          | NULL | NULL    | NULL | 4982087 |    10.00 | Using where |
+----+-------------+-------+------------+------+---------------+------+---------+------+---------+----------+-------------+
1 row in set, 2 warnings (0.00 sec)

第二+第三位置
empname和job一起查
这种原理跟前面两个一致,无法使用索引,只能对所有数据进行扫描。

第一+第三位置
按照(depno,job)字段顺序检索
这种仅使用到索引中的depno字段了,通过depnon确定范围之后,加载所有depno下的数据,再对job条件进行过滤。如果的depno查出来的数据基数巨大,也会慢。
比如我们的测试数据中 depno=16 的数据有50W左右,也是比较多的。

# 验证脚本:未使用到了索引,但仅覆盖了depno,所以扫描行数也有 37626 行
mysql> explain select empno,empname,job from emp  where depno=7 and job=8;
+----+-------------+-------+------------+------+-----------------------+-----------------------+---------+-------+-------+----------+-----------------------+
| id | select_type | table | partitions | type | possible_keys         | key                   | key_len | ref   | rows  | filtered | Extra                 |
+----+-------------+-------+------------+------+-----------------------+-----------------------+---------+-------+-------+----------+-----------------------+
|  1 | SIMPLE      | emp   | NULL       | ref  | idx_depno_empname_job | idx_depno_empname_job | 3       | const | 37626 |    10.00 | Using index condition |
+----+-------------+-------+------------+------+-----------------------+-----------------------+---------+-------+-------+----------+-----------------------+
1 row in set, 2 warnings (0.01 sec)

停止匹配的条件
检索depno=1 and empname>'' and job=1的记录
据上面的图,这种检索方式只能先确定depno=1 and empname>''所在页的范围,然后对这个范围的所有页进行遍历,job字段在这个查询的过程中,是无法确定数据在哪些页的,此时我们说job是不走索引的,只有depno、empname能够有效的确定索引页的范围。

2.2.2 索引列使用函数

当我们不恰当的使用索引所对应的字段的时候,可能会导致索引失效,比如查询的过程没有保证独立的列,
这个独立的列是指索引对应的列不能作用在函数中。如下:

mysql> select * from emp  where id = 4990000;
+---------+---------+---------+---------+-----+---------------------+------+------+-------+
| id      | empno   | empname | job     | mgr | hiredate            | sal  | comn | depno |
+---------+---------+---------+---------+-----+---------------------+------+------+-------+
| 4990000 | 4990000 | PWmulY  | SALEMAN |   1 | 2021-01-23 16:46:24 | 2000 | 400  |   102 |
+---------+---------+---------+---------+-----+---------------------+------+------+-------+
1 row in set  (0.002 sec)

mysql> select * from emp  where ABS(id) = 4990001;
+---------+---------+---------+---------+-----+---------------------+------+------+-------+
| id      | empno   | empname | job     | mgr | hiredate            | sal  | comn | depno |
+---------+---------+---------+---------+-----+---------------------+------+------+-------+
| 4990001 | 4990001 | fXtdiH  | SALEMAN |   1 | 2021-01-23 16:46:24 | 2000 | 400  |   107 |
+---------+---------+---------+---------+-----+---------------------+------+------+-------+
1 row in set  (2.007 sec)

耗时分别是 0.002、2.007,使用explain分析后发现作用在函数的时候没有走索引,变成全表扫描:

mysql> explain select * from emp  where id = 4990000;
+----+-------------+-------+-------+--------------------+---------+---------+-------+------+-------+
| id | select_type | table | type  | possible_keys      | key     | key_len | ref   | rows | Extra |
+----+-------------+-------+-------+--------------------+---------+---------+-------+------+-------+
|  1 | SIMPLE      | emp   | const | PRIMARY,idx_emp_id | PRIMARY | 4       | const |    1 | NULL  |
+----+-------------+-------+-------+--------------------+---------+---------+-------+------+-------+
1 row in set

mysql> explain select * from emp  where ABS(id) = 4990001;
+----+-------------+-------+------+---------------+------+---------+------+---------+-------------+
| id | select_type | table | type | possible_keys | key  | key_len | ref  | rows    | Extra       |
+----+-------------+-------+------+---------------+------+---------+------+---------+-------------+
|  1 | SIMPLE      | emp   | ALL  | NULL          | NULL | NULL    | NULL | 4952492 | Using where |
+----+-------------+-------+------+---------------+------+---------+------+---------+-------------+
1 row in set

2.2.3 计算表达式导致索引无效

索引对应的列也不能作用于计算表达式中:

mysql> select * from emp  where id = 4990000;
+---------+---------+---------+---------+-----+---------------------+------+------+-------+
| id      | empno   | empname | job     | mgr | hiredate            | sal  | comn | depno |
+---------+---------+---------+---------+-----+---------------------+------+------+-------+
| 4990000 | 4990000 | PWmulY  | SALEMAN |   1 | 2021-01-23 16:46:24 | 2000 | 400  |   102 |
+---------+---------+---------+---------+-----+---------------------+------+------+-------+
1 row in set  (0.002 sec)

mysql> select * from emp  where id+1 = 4990001;
+---------+---------+---------+---------+-----+---------------------+------+------+-------+
| id      | empno   | empname | job     | mgr | hiredate            | sal  | comn | depno |
+---------+---------+---------+---------+-----+---------------------+------+------+-------+
| 4990000 | 4990000 | PWmulY  | SALEMAN |   1 | 2021-01-23 16:46:24 | 2000 | 400  |   102 |
+---------+---------+---------+---------+-----+---------------------+------+------+-------+
1 row in set  (1.762 sec)

耗时分别是 0.002、1.762,使用explain分析后发现作用在表达式的时候没有走索引,变成全表扫描:

mysql> explain select * from emp  where id = 4990000;
+----+-------------+-------+-------+--------------------+---------+---------+-------+------+-------+
| id | select_type | table | type  | possible_keys      | key     | key_len | ref   | rows | Extra |
+----+-------------+-------+-------+--------------------+---------+---------+-------+------+-------+
|  1 | SIMPLE      | emp   | const | PRIMARY,idx_emp_id | PRIMARY | 4       | const |    1 | NULL  |
+----+-------------+-------+-------+--------------------+---------+---------+-------+------+-------+
1 row in set

# 下面这种是不行的
mysql> explain select * from emp  where id+1 = 4990001;
+----+-------------+-------+------+---------------+------+---------+------+---------+-------------+
| id | select_type | table | type | possible_keys | key  | key_len | ref  | rows    | Extra       |
+----+-------------+-------+------+---------------+------+---------+------+---------+-------------+
|  1 | SIMPLE      | emp   | ALL  | NULL          | NULL | NULL    | NULL | 4952492 | Using where |
+----+-------------+-------+------+---------------+------+---------+------+---------+-------------+
1 row in set

# 下面这种是可以的
mysql> explain select * from emp  where id = 4990001-1;
+----+-------------+-------+------------+-------+---------------+---------+---------+-------+------+----------+-------+
| id | select_type | table | partitions | type  | possible_keys | key     | key_len | ref   | rows | filtered | Extra |
+----+-------------+-------+------------+-------+---------------+---------+---------+-------+------+----------+-------+
|  1 | SIMPLE      | emp   | NULL       | const | PRIMARY       | PRIMARY | 4       | const |    1 |   100.00 | NULL  |
+----+-------------+-------+------------+-------+---------------+---------+---------+-------+------+----------+-------+
1 row in set, 1 warning (0.00 sec)

2.2.4 类型转换(自动或手动)导致索引失效

mysql> select * from emp where empname ='LsHfFJA';
+---------+---------+---------+---------+-----+---------------------+------+------+-------+
| id      | empno   | empname | job     | mgr | hiredate            | sal  | comn | depno |
+---------+---------+---------+---------+-----+---------------------+------+------+-------+
| 4582071 | 4582071 | LsHfFJA | SALEMAN |   1 | 2021-01-23 16:46:03 | 2000 | 400  |   106 |
+---------+---------+---------+---------+-----+---------------------+------+------+-------+
1 row in set

从这个数据中我们可以看出 empname为字符串类型的,depno为数值类型的,这两个上面都有独立的索引,我们来看两个语句:

mysql> select * from emp where empname =1;
Empty set, 65535 warnings (2.57 sec)

mysql> explain select * from emp where empname =1;
+----+-------------+-------+------------+------+---------------+------+---------+------+---------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key  | key_len | ref  | rows    | filtered | Extra       |
+----+-------------+-------+------------+------+---------------+------+---------+------+---------+----------+-------------+
|  1 | SIMPLE      | emp   | NULL       | ALL  | NULL          | NULL | NULL    | NULL | 4982087 |    10.00 | Using where |
+----+-------------+-------+------------+------+---------------+------+---------+------+---------+----------+-------------+
1 row in set, 2 warnings (0.00 sec)

mysql> select count(*) from emp where depno ='106';
+----------+
| count(*) |
+----------+
|   500195 |
+----------+
1 row in set  (0.000 sec)

mysql> select count(*) from emp where depno =106;
+----------+
| count(*) |
+----------+
|   500195 |
+----------+
1 row in set  (0.001 sec)

1、第一个查询,即便是在empname上建了索引,耗时依旧达到2s多。那是因为empname是字符串类型,字符串和数字比较的时候,会将字符串强制转换为数字,然后进行比较,所以整个查询变成了全表扫描,一个个抽出每条数据,将empname转换为数字和1进行比较。从第二个explain语句中也印证了这个算法。

2、 第三个和第四个查询,depno是int类型的,两个查询效率一致,都是正常利用索引快速检索。这是因为数值类型的字段,查询匹配的值无论是字符串还是数值都会走索引。

2.2.5 模糊查询(Like)左边包含%号

下面看两个查询,都采用了模糊查询,但是使用%开头会造成无法从页面确定扫描的位置,导致索引无效,继而全表扫描。

mysql> select * from emp where empname like 'LsHfFJA%';
+---------+---------+---------+---------+-----+---------------------+------+------+-------+
| id      | empno   | empname | job     | mgr | hiredate            | sal  | comn | depno |
+---------+---------+---------+---------+-----+---------------------+------+------+-------+
| 4582071 | 4582071 | LsHfFJA | SALEMAN |   1 | 2021-01-23 16:46:03 | 2000 | 400  |   106 |
+---------+---------+---------+---------+-----+---------------------+------+------+-------+
1 row in set  (0.000 sec)

mysql> select * from emp where empname like '%LsHfFJA%';
+---------+---------+---------+---------+-----+---------------------+------+------+-------+
| id      | empno   | empname | job     | mgr | hiredate            | sal  | comn | depno |
+---------+---------+---------+---------+-----+---------------------+------+------+-------+
| 4582071 | 4582071 | LsHfFJA | SALEMAN |   1 | 2021-01-23 16:46:03 | 2000 | 400  |   106 |
+---------+---------+---------+---------+-----+---------------------+------+------+-------+
1 row in set  (2.034 sec)

上面第一个查询可以利用到name字段上面的索引,下面的查询是无法确定需要查找的值所在的范围的,只能全表扫描,无法利用索引,所以速度比较慢,这个过程上面有说过。

2.2.6 条件使用or关键字(OR 前后存在非索引的列)

在 WHERE 子句中,OR 前后的条件列不属于索引列,那么索引会失效。
以下面的语句为例子,使用And就可以使用到索引,Or就会全表扫描。
原理其实很好理解,使用And的时候,我们在搜索树上先找到第一个条件字段(就是覆盖索引的depno),然后再缩小范围查找mgr字段。
但如果使用了Or,代表我的条件是两个,都得搜寻,才能找出所有数据。而未覆盖索引的那个条件依旧需要全表扫描。

# 语句1:是用到索引
mysql> explain select empno,empname,job from emp where depno=1 and mgr=1;
+----+-------------+-------+------------+------+-----------------------+-----------------------+---------+-------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys         | key                   | key_len | ref   | rows | filtered | Extra       |
+----+-------------+-------+------------+------+-----------------------+-----------------------+---------+-------+------+----------+-------------+
|  1 | SIMPLE      | emp   | NULL       | ref  | idx_depno_empname_job | idx_depno_empname_job | 3       | const | 3705 |    10.00 | Using where |
+----+-------------+-------+------------+------+-----------------------+-----------------------+---------+-------+------+----------+-------------+
1 row in set, 1 warning (0.00 sec)

# 语句2:全表扫描
mysql> explain select empno,empname,job from emp where depno=1 or mgr=1;
+----+-------------+-------+------------+------+-----------------------+------+---------+------+---------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys         | key  | key_len | ref  | rows    | filtered | Extra       |
+----+-------------+-------+------------+------+-----------------------+------+---------+------+---------+----------+-------------+
|  1 | SIMPLE      | emp   | NULL       | ALL  | idx_depno_empname_job | NULL | NULL    | NULL | 4982087 |    10.74 | Using where |
+----+-------------+-------+------------+------+-----------------------+------+---------+------+---------+----------+-------------+
1 row in set, 1 warning (0.00 sec)

# 语句3:全表扫描
mysql> explain select empno,empname,job from emp where mgr=1 or depno=7 ;
+----+-------------+-------+------------+------+-----------------------+------+---------+------+---------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys         | key  | key_len | ref  | rows    | filtered | Extra       |
+----+-------------+-------+------------+------+-----------------------+------+---------+------+---------+----------+-------------+
|  1 | SIMPLE      | emp   | NULL       | ALL  | idx_depno_empname_job | NULL | NULL    | NULL | 4982087 |    10.74 | Using where |
+----+-------------+-------+------------+------+-----------------------+------+---------+------+---------+----------+-------------+
1 row in set, 1 warning (0.00 sec)

这种情况的优化方式就是在Or的时候两边列都覆盖入索引,就可以避免全表扫描

2.2.7 条件使用in关键字

in方式检索数据,我们还是经常用的。
平时我们做项目的时候,建议少用表连接,比如电商中需要查询订单的信息和订单中商品的名称,可以先查询查询订单表,然后订单表中取出商品的id列表,采用in的方式到商品表检索商品信息,由于商品id是商品表的主键,所以检索速度还是比较快的。
通过id在500万数据中检索100条数据,看看效果:

mysql>  select * from emp a where
a.id in (800000, 800001, 800002, 800003, 800004, 800005, 800006, 800007, 800008, 800009, 800010, 800011, 800012, 800013,
 800014, 800015, 800016, 800017, 800018, 800019, 800020, 800021, 800022, 800023, 800024, 800025, 800026, 800027, 800028,
800029, 800030, 800031, 800032, 800033, 800034, 800035, 800036, 800037, 800038, 800039, 800040, 800041, 800042, 800043, 800044,
800045, 800046, 800047, 800048, 800049, 800050, 800051, 800052, 800053, 800054, 800055, 800056, 800057, 800058, 800059, 800060,
800061, 800062, 800063, 800064, 800065, 800066, 800067, 800068, 800069, 800070, 800071, 800072, 800073, 800074, 800075, 800076,
800077, 800078, 800079, 800080, 800081, 800082, 800083, 800084, 800085, 800086, 800087, 800088, 800089, 800090, 800091, 800092,
800093, 800094, 800095, 800096, 800097, 800098, 800099);
+--------+--------+---------+---------+-----+---------------------+------+------+-------+
| id     | empno  | empname | job     | mgr | hiredate            | sal  | comn | depno |
+--------+--------+---------+---------+-----+---------------------+------+------+-------+
| 800000 | 800000 | qVFqPY  | SALEMAN |   1 | 2021-01-23 16:43:02 | 2000 | 400  |   105 |
| 800001 | 800001 | KVzJXL  | SALEMAN |   1 | 2021-01-23 16:43:02 | 2000 | 400  |   107 |
| 800002 | 800002 | vWvpkj  | SALEMAN |   1 | 2021-01-23 16:43:02 | 2000 | 400  |   102 |
............
| 800099 | 800099 | roxtAx  | SALEMAN |   1 | 2021-01-23 16:43:02 | 2000 | 400  |   107 |
+--------+--------+---------+---------+-----+---------------------+------+------+-------+
100 rows in set  (0.001 sec)

耗时1毫秒左右,还是相当快的。
这个相当于多个分解为多个唯一记录检索,然后将记录合并。所以这个其实也是快的,只要in里面的数据不是极端海量的即可。
单次查询选择的数据范围很大,比如占整个表的30%以上时,MySQL优化器可能会认为全表扫描比使用索引更快,因此选择不使用索引。这是因为全表扫描可能避免了索引查找和回表的开销,从而在某些情况下提供了更好的性能。
所以使用 in 在结果集中不能超过30%。

2.2.8 使用 not in 或 not exists

情况同于 2.2.7 节

2.2.9 条件中使用比较算法

如下代码:

  • 第一个语句中使用了不等比较 (!= 或者<>) 导致索引失效,不等于需要所有索引数据拿出来比较,所以等同于全表扫描,也是慢的。
  • 第二个语句中使用了比较符,虽然走索引,但是扫描数据超过30%,编译器会认为全表扫描性能比走索引更好,就不走索引了。这点可参考 2.2.7 节
  • 第三个语句扫描的数据量(122054)远低于500w的30%,走索引。查询执行计划中包含Using index condition和Using MRR时,意味着正在使用高效的索引和存储引擎优化技术来加速查询。
# 使用不等比较(!= 或者<>) 导致索引失效
mysql> explain select * from emp  where depno <> 7;
+----+-------------+-------+------------+------+-----------------------+------+---------+------+---------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys         | key  | key_len | ref  | rows    | filtered | Extra       |
+----+-------------+-------+------------+------+-----------------------+------+---------+------+---------+----------+-------------+
|  1 | SIMPLE      | emp   | NULL       | ALL  | idx_depno_empname_job | NULL | NULL    | NULL | 4982087 |    52.45 | Using where |
+----+-------------+-------+------------+------+-----------------------+------+---------+------+---------+----------+-------------+
1 row in set, 1 warning (0.00 sec)

# 扫描数据量超30%,不走索引
mysql> explain select * from emp  where depno > 7;
+----+-------------+-------+------------+------+-----------------------+------+---------+------+---------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys         | key  | key_len | ref  | rows    | filtered | Extra       |
+----+-------------+-------+------------+------+-----------------------+------+---------+------+---------+----------+-------------+
|  1 | SIMPLE      | emp   | NULL       | ALL  | idx_depno_empname_job | NULL | NULL    | NULL | 4982087 |    50.00 | Using where |
+----+-------------+-------+------------+------+-----------------------+------+---------+------+---------+----------+-------------+
1 row in set, 1 warning (0.00 sec)

# 扫描数据量少,走索引
mysql> explain select * from emp  where depno < 7;
+----+-------------+-------+------------+-------+-----------------------+-----------------------+---------+------+--------+----------+----------------------------------+
| id | select_type | table | partitions | type  | possible_keys         | key                   | key_len | ref  | rows   | filtered | Extra                            |
+----+-------------+-------+------------+-------+-----------------------+-----------------------+---------+------+--------+----------+----------------------------------+
|  1 | SIMPLE      | emp   | NULL       | range | idx_depno_empname_job | idx_depno_empname_job | 3       | NULL | 122054 |   100.00 | Using index condition; Using MRR |
+----+-------------+-------+------------+-------+-----------------------+-----------------------+---------+------+--------+----------+----------------------------------+
1 row in set, 1 warning (0.00 sec)

3 总结

总结一下常见失效场景:

  • 违反最左匹配原则
  • 索引列使用函数计算
  • 索引列使用计算表达式
  • 索引列进行类型转换(自动或手动)
  • 模糊查询(Like)左边包含%号
  • 条件使用OR关键字,且在 OR 前后存在非索引的列
  • 条件使用in关键字,且查询结果超过30%数据比
  • 条件使用 not in 或 not exists,且查询结果超过30%数据比
  • 条件中使用不等号(!= 或 <>)

除此之外,还有一些索引覆盖,规避回表的策略,我们后面的篇章再讨论。

作者引言

  • 自从19年开始接触到RPC,当时完全没有相关概念,接触到的都是http,tcp等,当时公司用的是zeroc出品的ice框架,对应rpc非常强大,跨平台,跨语言。可惜的国内并不是主流,主流是gRPC,万物诸途同归,最终的目地是一样的。主要上看谁简单,方便,好理解。就在去年重新出一个新的RPC框架
    IceRPC
    ,从0重新构建,并以
    QUIC
    为基础,开创一个新的RPC.我感到非常欣慰。特别是有一句话,我深有同感:
    更少的代码意味着更简单的逻辑、更少的依赖关系和更快的执行以及更少的bug
    ,作为一直在一线开的人员,我想一样很有感触。本是平凡人,只能默默干,什么时候当韭菜,看天决定。
  • 不知道如何是推广新的
    IceRPC
    ,为RPC做个小小的贡献,我还是从[
    https://github.com/icerpc
    ]源码README.md翻译开始吧,如果读者英语棒棒的好,还是直接看原文吧,本翻译是我平生第一次翻译,如有不爽的地方,希望大家可以回复交流,我会及时改
  • 万事开头难,就怕不断干。希望以后能不断更新,同时想认识到更多的朋友

IceRPC - C# 语言版

IceRPC是一个现代化、模块化PRC框架,可以帮助您以最少的工作量,快速构建网络应用(生产力强,更多选择).

以QUIC协议构建

IceRPC 充分利用
HTTP/3
的新型多路传输
QUIC
协议,从零开始构建RPC.

QUIC 协议天生适合RPC: RPC 映射到双向QUIC流,承载请求/响应对.
多个请求/响应对,可以在同一个QUIC连接内,并行进行,并且不会相互干扰.

IceRPC 使用自已的应用程序协议,
icerpc
, 交换连接设置,处理传输请求及响应,确保连接有序关闭. 这个新的以RPC为中心的协议,是QUIC上的一层薄薄的协议.

除了 QUIC,还支持更多协议

IceRPC的主要传输是QUIC, 但目前仍处QUIC协议的早期阶段,所以仅支持QUIC协议并不是实际.

为了弥合这一差距, IceRPC 提供了一个名为
Slic
的多路复用适配器. Slic 实现了类似QUIC的多路复用,并通过任何双工传输(如TCP)进行传输. 通过这种方式,可以将IceRPC与QUIC,及TCP一起使用(通过Slic), 也可以与其他传统传输方式使用,如蓝牙和命名管道.

现代化的 C# 与 .NET

IceRPC(C#) 充分利用了最新C#语法和.NET功能,并提供了现代化C# API接口.

最主的特性就是 async/await. Async/await 允许在进行调用时,有效地利用线程来等待 I/O, 而 RPCs 都与网络 I/O 有关. Async/await 还使代码更易于阅读及维护:
所有的RPC调用都有
Async
Api接口,这些接口都是可等待的,与本地同步调用时,可以快速查看. 如下所示:

// 异步代码 (旧RPC风格)

//目前尚不清楚这是一个需要几毫秒的远程呼叫还是一个需要
//至多几微秒。在任何情况下,此调用都会保持其线程,直到
//完成。
string greeting = greeter.Greet(name);
//异步代码(现代RPC风格)

//由于wait和Async后缀,我们看到这是一个特殊的调用。GreetSync发布
//线程,同时等待来自对等方的响应,并且编写起来和
//同步版本一样。
string greeting = await greeter.GreetAsync(name);

使用IceRPC,所有进行网络I/O的调用,都是异步的,而且只有异步。
IceRPC不提供并行阻塞、同步接口。

IceRPC 利用
System.IO.Pipelines
获取最大效率管道. IceRPC从相同的可配置内存池中,租用其所有字节缓冲区.

IceRPC天然地支持取消(cancellation),就像所有现代C#库一样,带有取消令牌参数.
如取消“跨线”工作:当取消一个未完成的RPC调用时,会通知远程服务
,进而可以取消进一步的操作处理.

模块化和可扩展性

使用IceRPC进行RPC时,请求和响应通过调用管道(在客户端)
和一个调度管道(在服务器端)进行传输:

---
title: Client-side
---
flowchart LR
subgraph pipeline[Invocation pipeline]
direction LR
di[Deadline\ninterceptor] --> ri[Retry\ninterceptor] --> connection[network\nconnection] --> ri --> di
end
client -- request --> di
client -- response --- di

---
title: Server-side
---
flowchart LR
subgraph pipeline [Dispatch pipeline]
direction LR
lm[Logger\nmiddleware] --> dm[Deadline\nmiddleware] --> service --> dm --> lm

end
connection[network\nconnection] -- request --> lm
connection -- response --- lm

这些管道会拦截请求和响应,由您来决定如何处理它们.
如果想打个日志记录, 就加 Logger 拦截器到调用管道中去,或将Logger中间件添加到调度中就行了.
如果想自动重试,失败的请求,可以加个“重试”拦截器到调用管道中就行了.
IceRPC 提供了许多拦截器和中间件如: compression, deadlines, logging,
metrics, OpenTelemetry integration, 等.
我们自已也可以轻松地创建和安装自己的拦截器或中间件.或者自定义上面的拦截器、中间件。

所有这些功能(自带拦截器和中间件)都是可选的,因此可以准确地选择,所需要的行为功能.
比如, 如果不压缩任何内容,则移除Compress拦截器: 如果都没有安装这个Compress拦截器,就根本无法压缩请求数据.
更少的代码意味着更简单的逻辑、更少的依赖关系和更快的执行以及更少的bug

在IceRPC中,模块化和可扩展性无处不在. 可以很轻松实现新的双工或多路复用传输,然后作为IceRPC的插件。所有传输接口都是公共的,并且有完整详细的文档说明.

可以选择的将IceRPC与
DI container
一起使用.

选择 IDL(接口描述语言)

IceRPC提供了一个一流的面向字节的API,使用
IDL
来构建RPC,及可选的序列化格式.

IceRPC完全支持两个IDLs: Slice (详细见如下) and
Protobuf
. 可以使用Slice或Protobuf来定义客户端和服务器之间的接口契约.

Slice

Slice
IDL和序列化格式,可以清晰简洁的定义RPCs功能设置. Slice 与IceRPC无绑定关系: 可以在不使用任何RPC框架的情况下使用Slice,也可以使用其他RPC框架.

当前仓库提供了IceRPC+Slice集成,可以无缝地将IceRPC和Slice一起使用.

在Slice中自定义的“Greeter”接口非常简单:

// Interface Greeter由服务器中托管的服务实现.
interface Greeter {
    // The greet request carries the name of the person to greet and
    // the greet response carries the greeting created by the service
    // that implements Greeter.
    greet(name: string) -> string
}

不需要编写特殊的请求和回复消息类型:可以内联指定对应参数.

然后,Slice编译器用这个“Greeter”接口文件,生成可读且简洁的C#代码:

  • 客户端,生成具有单个“GreetSync”方法的“IGreeter”接口.

  • 客户端 通过
    GreeterProxy
    代理使用IceRPC发送请求/接收响应来实现“IGreeter”

  • 服务端 实现
    IGreeterService
    接口来实现Greeter的服务功能

Slice 还支持双向流. 如下示:

interface Generator {
    // Returns a (possibly infinite) stream of int32
    generateNumbers() -> stream int32
}

interface Uploader {
    // Uploads an image (can be very large)
    uploadImage(image: stream uint8)
}

uint8
的流被映射到C#的
PipeReader
,而任何其他类型的流则被映射到
IAsyncEnumerable<T>
.

Slice 提供了易于理解的基元类型,如下:

  • string
  • bool
  • fixed-size integral types (int8, int16, int32, int64, uint8, uint16, uint32, uint64)
  • variable-size integral types (varint32, varint62, varuint32, varuint62)
  • floating point types (float32, float64)

可以使用
struct
,
enum
, 各
custom
定义新类型,也可以使用
Sequence<T>

Dictionary<Key, Value>
定义集和等. 可以让Slice处理在成功或失败时,返回不同类型,如
Result<Success, Failure>
.

custom
允许通过Slice发送任何想要的C#类型,这是IceRPC的模块化和
可扩展性魔法。只需要提供对自定义类型的实例进行编码和解码的方法.

Protobuf

Protocol Buffers, 简称 Protobuf, 是Google开发的一种流行的 IDL 和 序列化格式. 它是很多RPC框架的首选IDL,包括
gRPC
.

IceRPC+Protobuf集成,只需几行代码就可以使用IceRPC调用和实现Protobuf服务
.

Ice 相关交互操作

IceRPC(C#)提供了与
Ice
的高级别互操作性。可以使用IceRPC为旧的
Ice服务器写客户端,可以从Ice客户端调用,由IceRPC服务器托管的服务.

IceRPC for Ice users
提供了所有详细信息.

License 许可证

IceRPC is licensed under the
Apache License version 2.0
, a permissive open-source license.

This license allows you to use IceRPC in both open-source and closed source applications, free of charge. Please refer
to the license for the full terms and conditions.

作者结语

  • 从第一行翻译开始,查看各种翻译,比如 百度翻译,有道翻译,加上自已的理解,有点小累啊
  • 翻译的不好,请手下留情,人也要成长的,谢谢
  • 如果对我有点小兴趣,如可加我个人微信哦,大家交个朋友,一起探讨人生。
    image

引言

在过去的Java版本中,日期和时间的处理主要依赖于
java.util.Date

java.util.Calendar
类,然而随着业务系统的复杂以及技术层面的提升,这些传统的日期时间类暴露出了若干显著的不足之处。随着
Java8
的发布,其引入了一套全新的日期时间API,彻底改变了我们处理日期和时间的方式。

传统的日期时间类

相比较Java8中新引入的
java.time
包下的时间处理类,传统的日期时间处理类在易用性,线程安全,不支持市时区等缺点。

  1. 设计复杂性

    Date
    类的设计较为简单,但它实际上混合了日期和时间信息,并且没有提供直观的方法来单独操作日期或时间部分。
    Calendar
    类虽然提供了更多灵活性,但由于其内部状态和方法的复杂性,使得开发者在使用过程中容易出现错误和混淆,尤其是在进行日期时间计算和格式化时。比如:
Date currentDate = new Date();  
// 输出原始的日期时间,通常不是人类可读格式   Fri Mar 08 03:13:47 CST 2024
System.out.println(currentDate);

// 要改变日期的某个部分,必须先将其转换为 Calendar,然后设置  
Calendar calendar = Calendar.getInstance();  
calendar.setTime(currentDate);  
// 修改日期的天数  
calendar.add(Calendar.DAY_OF_MONTH, 1);
  1. 线程安全性

    Date

    Calendar
    类,以及格式化日期的SimpleDateFormat类都不是线程安全的,这意味着在多线程环境下的并发访问可能会导致数据不一致。


    • Date
      类内部维护了一个 long 类型的瞬时值,当调用如
      setTime()
      方法来更新这个瞬时值时,不同的线程同时调用就会互相覆盖彼此的值,造成数据不一致。

    • Calendar
      类不仅包含了日期和时间信息,还有一系列内部状态变量,如年、月、日、小时、分钟、秒等。
      Calendar
      类的方法通常会修改这些内部状态,例如
      add()

      set()
      等。在多线程环境下,若多个线程尝试同时修改同一个
      Calendar
      实例,也会导致不可预期的结果。

    • SimpleDateFormat
      类在执行格式化和解析日期时间操作时,内部会维护一个
      Calendar
      对象以及其他一些状态变量。在
      format()

      parse()
      方法执行过程中,这些状态会被更新以完成格式转换。并且
      SimpleDateFormat
      中的方法并非原子操作,因此在多线程并发调用时,可能在一个线程还未完成整个操作时就被另一个线程打断,导致错误的日期时间处理结果。

  2. 时区处理能力

    Date
    类虽能表示时间戳,但它不直接关联时区信息,难以进行时区相关的转换。而
    Calendar
    虽然支持时区,但操作过程相当复杂。

  3. 精度差异

    Date
    类精度只到毫秒级。

Java8中日期时间类

Java8中引入的
LocalDate

LocalTime

LocalDateTime
这几个位于
java.time
下的类克服了上述传统类别的局限性,提供了更强大、直观和精准的日期时间处理能力,成为现代Java开发中处理日期时间首选的工具类。相比较传统的日期时间类,具备以下显著优势:

  1. 功能丰富
    java.time
    包下的类如
    LocalDate

    LocalTime

    LocalDateTime

    ZonedDateTime
    等都有明确的职责划分,分别处理日期、时间、日期时间以及带时区的日期时间,结构清晰,易于理解和使用。并且它们提供了一系列直观、面向对象的API,如
    plusXxx()

    minusXxx()

    withXxx()
    等方法,使日期时间操作变得简单明了。

  2. 时区支持
    除此之外,还支持时区,通过
    ZonedDateTime

    ZoneId
    等类提供了对时区的更好支持,可以方便地进行时区转换和处理。

  3. 线程安全
    这些类都是不可变对象,线程安全,可以在多线程环境下安全使用,不会出现因并发操作导致的数据不一致问题。

  4. 更高的精度
    支持纳秒级精度,相比
    Date
    类的毫秒精度更胜一筹。

java.time
下主要有如下一些关键类:

  1. LocalDate
    LocalDate
    类表示一个不包含时间信息的日期,只包含年、月、日三个部分,且不关联任何特定时区。

  2. LocalTime
    LocalTime
    类表示一个不包含日期信息的具体时间,包含时、分、秒和纳秒四个部分。

  3. LocalDateTime
    LocalDateTime
    类结合了日期和时间,表示一个具体的日期和时间点,但是不包含时区信息。

  4. ZonedDateTime
    ZonedDateTime
    类表示一个带有时区的日期时间,它可以明确表示某一特定时区内的某一确切时间点。

  5. Instant
    Instant
    类表示时间线上某一瞬时点,通常以Unix纪元(1970-01-01T00:00:00Z)以来的秒数和纳秒数表示,它是全球通用的时间点表示。

  6. Period
    Period
    类用于表示两个日期之间的期间,包括年、月、日的数量。

  7. Duration
    Duration
    类表示两个时间点之间的时间差,包含秒和纳秒的持续时间,主要用于表示时间间隔而非日历单位。

  8. DateTimeFormatter
    DateTimeFormatter
    类用于日期时间的格式化和解析,提供了标准化和自定义的日期时间格式化方式。

  9. TemporalAdjusters
    TemporalAdjusters
    类提供了一系列实用方法,用于调整日期时间,例如获取下一个工作日、月初、月末等。

这些类共同构成了一个强大、灵活且易于使用的日期时间处理体系,大大改善了Java在处理日期时间问题时的效率和准确性。接下来我们在使用上分别介绍这些类,以及使用他们的方式,感受他们的强大。

Java8中日期时间类使用

创建

NOW方法获取当前 时刻、日期、时间
LocalTime localTime = LocalTime.now();  
System.out.println("localTime:"+localTime);  
  
LocalDate localDate = LocalDate.now();  
System.out.println("localDate:"+localDate);  
  
LocalDateTime localDateTime = LocalDateTime.now();  
System.out.println("localDateTime:"+localDateTime);

输出为:

localTime:15:28:45.241181
localDate:2024-03-11
localDateTime:2024-03-11T15:28:45.260655

针对
LocalTime

LocalDateTime
获取当前时刻默认会带有毫秒,如果不需要毫秒的话,可以通过设置纳秒为0 保留秒
1秒 = 十亿纳秒
。例如:

LocalTime localTime = LocalTime.now().withNano(0);
System.out.println("localTime:"+localTime);  

LocalDateTime localDateTime = LocalDateTime.now().withNano(0);  
System.out.println("localDateTime:"+localDateTime);

输出为:

localTime:15:32:31
localDateTime:2024-03-11T15:32:31

而对于
LocalDateTime
获取当前日期,默认
toString
会带有
T
分隔日期和时间,在项目中,可以通过全局序列化,进行统一的时间格式输出为
yyyy-MM-dd HH:mm:ss
。但是一般不建议这么干,毕竟改变全局序列化配置,建议不使用
toString
,可以使用
DateTimeFormatter
进行自定义转换。

of()方法指定年、月、日、时刻创建
// of方法直接传递对应的年、月、日  
LocalDate localDate = LocalDate.of(2024, 3, 11);  
System.out.println("localDate:"+localDate);  
localDate = LocalDate.of(2024, Month.MARCH, 11);  
System.out.println("localDate:"+localDate);  
localDate = LocalDate.ofYearDay(2024, 71);  
System.out.println("localDate:"+localDate);  
  
// 北京时间对应的时区  
ZoneId chinaTimeZone = ZoneId.of("Asia/Shanghai");  
// 创建一个 Instant,这里使用当前时间的 InstantInstant instant = Instant.now();  
localDate = LocalDate.ofInstant(instant, chinaTimeZone);  
System.out.println("localDate:"+localDate);  
  
// 使用ofEpochDay()方法,则EpochDay为从公元1970年1月1日(Unix纪元)开始的第多少天  
localDate = LocalDate.ofEpochDay(LocalDate.now().toEpochDay());  
System.out.println("localDate:"+localDate);  
  
  
LocalTime localTime = LocalTime.of(1, 30);  
System.out.println("localTime:"+localTime);  
localTime = LocalTime.of(1, 30, 30);  
System.out.println("localTime:"+localTime);  
  
localTime = LocalTime.ofInstant(instant, chinaTimeZone);  
System.out.println("localTime:"+localTime);  
// 根据一天中的总秒数构建时间
localTime = LocalTime.ofSecondOfDay(localTime.toSecondOfDay());  
System.out.println("localTime:"+localTime);  
  
  
LocalDateTime localDateTime = LocalDateTime.of(2024, 3, 11, 1, 30, 30);  
System.out.println("localDateTime:"+localDateTime);  
localDateTime = LocalDateTime.of(2024, Month.MARCH, 11, 1, 30, 30);  
System.out.println("localDateTime:"+localDateTime);  
// 使用LocalDate和LocalTime组合构造  
localDateTime = LocalDateTime.of(localDate, localTime);  
System.out.println("localDateTime:"+localDateTime);  
  
localDateTime = LocalDateTime.ofInstant(instant, chinaTimeZone);  
System.out.println("localDateTime:"+localDateTime);

输出为:

localDate:2024-03-11
localDate:2024-03-11
localDate:2024-03-11
localDate:2024-03-11
localDate:2024-03-11
localTime:01:30
localTime:01:30:30
localTime:16:41:37.893310
localTime:16:41:37
localDateTime:2024-03-11T01:30:30
localDateTime:2024-03-11T01:30:30
localDateTime:2024-03-11T16:41:37
localDateTime:2024-03-11T16:41:37.893310
from()方法转换

from()方法

TemporalAccessor
类型(如
ZonedDateTime
)转换为相对应的日期或者时间。
TemporalAccessor
接口是一个用于读取或写入日期、时间或者日期时间的通用接口。

// 创建一个ZonedDateTime实例  
ZonedDateTime zonedDateTime = ZonedDateTime.now(ZoneId.of("Asia/Shanghai"));  
LocalTime localTime = LocalTime.from(zonedDateTime);  
System.out.println("localTime:"+localTime);  

LocalDate localDate = LocalDate.from(zonedDateTime);  
System.out.println("localDate:"+localDate);  

LocalDateTime localDateTime = LocalDateTime.from(zonedDateTime);  
System.out.println("localDateTime:"+localDateTime);

输出为:

localTime:17:18:27.942911
localDate:2024-03-11
localDateTime:2024-03-11T17:18:27.942911
parse()方法转换

将字符串按照指定格式(可选)解析为对应的日期时间类。

LocalTime localTime = LocalTime.parse("17:25:30");  
System.out.println("localTime:"+localTime);  
localTime = LocalTime.parse("17:25:30", DateTimeFormatter.ofPattern("HH:mm:ss"));  
System.out.println("localTime:"+localTime);  
  
LocalDate localDate = LocalDate.parse("2024-03-11");  
System.out.println("localDate:"+localDate);  
localDate = LocalDate.parse("2024/03/11", DateTimeFormatter.ofPattern("yyyy/MM/dd"));  
System.out.println("localDate:"+localDate);  
  
LocalDateTime localDateTime = LocalDateTime.parse("2024-03-11T17:25:30");  
System.out.println("localDateTime:"+localDateTime);  
localDateTime = LocalDateTime.parse("2024/03/11 17:25:30", DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss"));  
System.out.println("localDateTime:"+localDateTime);

输出为:

localTime:17:25:30
localTime:17:25:30
localDate:2024-03-11
localDate:2024-03-11
localDateTime:2024-03-11T17:25:30
localDateTime:2024-03-11T17:25:30

日期时间类的相互转换

LocalTime、LocalDate、LocalDateTime 相互转化
// LocalTime + LocalDate = LocalDateTime
LocalDateTime localDateTime = LocalTime.now().atDate(LocalDate.now());
System.out.println("localDateTime:"+localDateTime);
localDateTime = LocalDate.now().atTime(LocalTime.now());
System.out.println("localDateTime:"+localDateTime);
localDateTime = LocalDateTime.of(LocalDate.now(), LocalTime.now());
System.out.println("localDateTime:"+localDateTime);

// LocalDateTime 转 LocalDate
LocalDate localDate = LocalDateTime.now().toLocalDate();
System.out.println("localDate:"+localDate);
// LocalDateTime 转 LocalTime
LocalTime localTime = LocalDateTime.now().toLocalTime();
System.out.println("localTime:"+localTime);

// 获取今日开始时间 2024-03-11T00:00
localDateTime = LocalDate.now().atStartOfDay();
System.out.println("localDateTime:"+localDateTime);
// 获取今日开始时间 2024-03-11T00:00
LocalDateTime startDateTime = LocalDateTime.of(LocalDate.now(), LocalTime.MIN);
System.out.println("startDateTime:"+ startDateTime);
// 获取今日结束时间 2024-03-11T23:59:59.999999999
LocalDateTime endDateTime = LocalDateTime.of(LocalDate.now(), LocalTime.MAX);
System.out.println("endDateTime:"+ endDateTime); 

输出为:

localDateTime:2024-03-11T18:04:22.348539
localDateTime:2024-03-11T18:04:22.370562
localDateTime:2024-03-11T18:04:22.370768
localDate:2024-03-11
localTime:18:04:22.371062
localDateTime:2024-03-11T00:00
startDateTime:2024-03-11T00:00
endDateTime:2024-03-11T23:59:59.999999999
String 与 LocalTime、LocalDate、LocalDateTime 相互转化

主要使用
format

parse
进行转换,使用方法基本相同。使用
DateTimeFormatter.ofPattern()
定义时间格式,再进行转换。
DateTimeFormatter
线程安全。

// LocalTime 转 String 自定义输出格式,例如:**时**分**秒 该转化的 00 不会被省略
String localTimeStr = LocalTime.now().format(DateTimeFormatter.ofPattern("HH时mm分ss秒"));
System.out.println("localTimeStr:"+localTimeStr);

String localDateStr = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
System.out.println("localDateStr:"+localDateStr);

// LocalDateTime 转 String
String localDateTimeStr = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
System.out.println("localDateTimeStr:"+localDateTimeStr);

// String 转 LocalDateTime
LocalDateTime localDateTime = LocalDateTime.parse("2023-04-14 15:59:40", DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
System.out.println("localDateTime:"+localDateTime);

输出结果:

localTimeStr:19时02分58秒
localDateStr:2024-03-11
localDateTimeStr:2024-03-11 19:02:58
localDateTime:2023-04-14T15:59:40
Date 与 LocalDate、LocalDateTime 相互转化
// Date 转 LocalDateTime
Date currentDate = new Date();
// 转换为Instant 
Instant instant = currentDate.toInstant();
// 通过zoneId设置时区(这里使用系统时区),转换为带带时区的 ZoneDateTime
ZonedDateTime zonedDateTime = instant.atZone(ZoneId.systemDefault());
// 然后通过ZonedDateTime转换为LocalDateTime
LocalDateTime localDateTime = zonedDateTime.toLocalDateTime();
System.out.println("localDateTime:"+localDateTime);
// LocalDateTime 转 Date,同理也是通过ZonedDateTime转换为Date
Date localDateTimeToDate = Date.from(LocalDateTime.now().atZone(ZoneId.systemDefault()).toInstant());
System.out.println(localDateTimeToDate);
// Date转LocalDate 同理 LocalDateTime转换
LocalDate localDate = currentDate.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
System.out.println("localDate:"+localDate);
// LocalDate 转 Date  需要先将 LocalDate 转 LocalDateTime
Date localDateToDate = Date.from(LocalDate.now().atStartOfDay(ZoneId.systemDefault()).toInstant());

这里介绍一下ZoneId。
java.time.ZoneId
是Java 8中
java.time
包中用于表示时区的类。时区是地球上的地理位置,用于确定在该位置观察太阳升落以及规定当地居民生活和商业活动时间的标准时间。
ZoneId
使用IANA时区数据库提供的时区标识符,这个标识符是唯一的,这些标识符通常是地区/城市对,例如“Asia/Shanghai”代表中国上海所在的时区,
America/New_York
代表美国纽约城市。
其实例获取有两种方式:

  • ZoneId.systemDefault()
    :获取系统默认的时区ID。
  • ZoneId.of(String zoneId)
    :根据提供的时区ID字符串获取ZoneId实例。至于zoneId的值,可以查看源码。可以通过
    ZoneId.getAvailableZoneIds()
    查看获取。
Long 与 LocalDate、LocalDateTime 相互转化

时间戳转换。

long timeMillis = System.currentTimeMillis();

// 时间戳(Long) 转 LocalDateTime
LocalDateTime localDateTime = Instant.ofEpochMilli(timeMillis).atZone(ZoneOffset.ofHours(8)).toLocalDateTime();
System.out.println("localDateTime:"+localDateTime);
localDateTime = Instant.ofEpochMilli(timeMillis).atZone(ZoneId.systemDefault()).toLocalDateTime();
System.out.println("localDateTime:"+localDateTime);
// LocalDateTime 转 时间戳(Long) 秒级
long localDateTimeToSecond = LocalDateTime.now().toEpochSecond(ZoneOffset.ofHours(8));
System.out.println("localDateTimeToSecond:"+ localDateTimeToSecond);
// LocalDateTime 转 时间戳(Long) 毫秒级
long localDateTimeToMilliSecond = LocalDateTime.now().toInstant(ZoneOffset.ofHours(8)).toEpochMilli();
System.out.println("localDateTimeToMilliSecond:"+ localDateTimeToMilliSecond);

// 时间戳(Long) 转 LocalDate
LocalDate localDate = Instant.ofEpochMilli(timeMillis).atZone(ZoneOffset.ofHours(8)).toLocalDate();
System.out.println("localDate:"+ localDate);
// LocalDate 转 时间戳(Long) 秒级
long localDateToSecond =  LocalDate.now().atStartOfDay().toEpochSecond(ZoneOffset.ofHours(8));
System.out.println("localDateToSecond:"+ localDateToSecond);
// LocalDate 转 时间戳(Long) 毫秒级
long localDateToMilliSecond = LocalDate.now().atStartOfDay().toInstant(ZoneOffset.ofHours(8)).toEpochMilli();
System.out.println("localDateToMilliSecond:"+ localDateToMilliSecond);

输出结果为:

localDateTime:2024-03-11T19:37:02.335
localDateTime:2024-03-11T19:37:02.335
localDateTimeToSecond:1710157022
localDateTimeToMilliSecond:1710157022365
localDate:2024-03-11
localDateToSecond:1710086400
localDateToMilliSecond:1710086400000

java.time.ZoneOffset
是Java8中
java.time
包内用来表示时区偏移量的类,它表示的是格林尼治标准时间或协调世界时间(UTC)基础上的固定偏移量。每一个时区都可以通过一个或多个偏移量来表示,比如“+02:00”表示比UTC时间快两个小时的时区偏移。
其实例创建有如下方式:

  • ZoneOffset.ofHours(int hours)
    :根据小时数创建偏移量,例如
    ZoneOffset.ofHours(2)
    表示比UTC早2小时的时区。
  • ZoneOffset.ofHoursMinutes(int hours, int minutes)
    :根据小时数和分钟数创建偏移量。
  • ZoneOffset.ofHoursMinutesSeconds(int hours, int minutes, int seconds)
    :根据小时、分钟和秒数创建偏移量。
  • ZoneOffset.ofTotalSeconds(int totalSeconds)
    :根据相对于UTC的总秒数创建偏移量。
  • ZoneOffset.of(String offsetId)
    :根据偏移量ID(如 "+02:00")创建实例。

日期时间类的操作

日期时间的增减

java.time
包中日期时间类(如
LocalDateTime

LocalDate

LocalTime
)可以通过
plusXxx()

minusXxx()
方法,用于对日期时间对象进行加减操作,以增加或减少指定的时间或日期单位。

1、LocalDateTime 加减

  • plusHours(int hours)
    ,
    plusMinutes(int minutes)
    ,
    plusSeconds(int seconds)
    :分别用于向
    LocalDateTime
    对象添加指定的小时数、分钟数和秒数。
  • plus(1, ChronoUnit.XXX)
    :这里的
    ChronoUnit
    参数可以是
    HOURS

    MINUTES

    SECONDS
    等,也可以是
    YEARS

    MONTHS

    DAYS

    WEEKS
    等,用于向日期时间对象添加指定单位的数量。
  • plus(Duration duration)
    :使用
    Duration
    对象来增加时间,
    Duration
    可以包含秒和纳秒的精度。
  • plus(Period period)
    :使用
    Period
    对象来增加日期,
    Period
    可以表示年、月、日的数量。

  • plusXxx()
    方法相对应,
    minusXxx()
    方法用于从日期时间对象中减少指定的单位。例如
    minusHours(int hours)

    minusMinutes(int minutes)

    minusSeconds(int seconds)
    等方法用于减少小时、分钟、秒数。
// LocalDateTime 加减
LocalDateTime localDateTime = LocalDateTime.now();
// 以下为增加时、分、秒
LocalDateTime plusLocalDateTime = localDateTime.plusHours(1).plusMinutes(1).plusSeconds(1);
System.out.println("plusLocalDateTime:"+plusLocalDateTime);
plusLocalDateTime = localDateTime.plus(1, ChronoUnit.HOURS).plus(1, ChronoUnit.MINUTES).plus(1, ChronoUnit.SECONDS);
System.out.println("plusLocalDateTime:"+plusLocalDateTime);
plusLocalDateTime = localDateTime.plus(Duration.ofHours(1)).plus(Duration.of(1, ChronoUnit.MINUTES)).plus(Duration.of(1, ChronoUnit.SECONDS));
System.out.println("plusLocalDateTime:"+plusLocalDateTime);

// 以下为增加年、月、日
plusLocalDateTime = localDateTime.plusYears(1).plusMonths(1).plusWeeks(1).plusDays(1);
System.out.println("plusLocalDateTime:"+plusLocalDateTime);
plusLocalDateTime = localDateTime.plus(1, ChronoUnit.YEARS).plus(1, ChronoUnit.MONTHS).plus(1, ChronoUnit.WEEKS).plus(1, ChronoUnit.DAYS);
System.out.println("plusLocalDateTime:"+plusLocalDateTime);
plusLocalDateTime = localDateTime.plus(Duration.of(1, ChronoUnit.YEARS)).plus(Duration.of(1, ChronoUnit.MONTHS)).plus(Duration.of(1, ChronoUnit.WEEKS)).plus(Duration.ofDays(1));
System.out.println("plusLocalDateTime:"+plusLocalDateTime);
plusLocalDateTime = localDateTime.plus(Period.ofYears(1)).plus(Period.ofMonths(1)).plus(Period.ofWeeks(1)).plus(Period.ofDays(1));
System.out.println("plusLocalDateTime:"+plusLocalDateTime);

// 以下为减少时、分、秒
LocalDateTime minusLocalDateTime = localDateTime.minusHours(1).minusMinutes(1).minusSeconds(1);
System.out.println("minusLocalDateTime:"+minusLocalDateTime);
minusLocalDateTime = localDateTime.minus(1, ChronoUnit.HOURS).minus(1, ChronoUnit.MINUTES).minus(1, ChronoUnit.SECONDS);
System.out.println("minusLocalDateTime:"+minusLocalDateTime);
minusLocalDateTime = localDateTime.minus(Duration.ofHours(1)).minus(Duration.of(1, ChronoUnit.MINUTES)).minus(Duration.of(1, ChronoUnit.SECONDS));
System.out.println("minusLocalDateTime:"+minusLocalDateTime);

// 以下为减少年、月、日
minusLocalDateTime = localDateTime.minusYears(1).minusMonths(1).minusWeeks(1).minusDays(1);
System.out.println("minusLocalDateTime:"+minusLocalDateTime);
minusLocalDateTime = localDateTime.minus(1, ChronoUnit.YEARS).minus(1, ChronoUnit.MONTHS).minus(1, ChronoUnit.WEEKS).minus(1, ChronoUnit.DAYS);
System.out.println("minusLocalDateTime:"+minusLocalDateTime);
minusLocalDateTime = localDateTime.minus(Duration.of(1, ChronoUnit.YEARS)).minus(Duration.of(1, ChronoUnit.MONTHS)).minus(Duration.of(1, ChronoUnit.WEEKS)).minus(Duration.ofDays(1));
System.out.println("minusLocalDateTime:"+minusLocalDateTime);
minusLocalDateTime = localDateTime.minus(Period.ofYears(1)).minus(Period.ofMonths(1)).minus(Period.ofWeeks(1)).minus(Period.ofDays(1));
System.out.println("plusLocalDateTime:"+minusLocalDateTime);

2、LocalDate 加减

  • 同样的,
    plusYears(int years)
    ,
    plusMonths(int months)
    ,
    plusDays(int days)
    分别用于增加年、月、日。
  • plus(1, ChronoUnit.XXX)

    plus(Duration/Period duration/period)
    方法在此同样适用,用于增加指定的日期单位。

  • plusXxx()
    方法相对应,
    minusXxx()
    方法用于从日期时间对象中减少指定的单位。例如
    minusYears(int years)

    minusMonths(int months)

    minusWeeks(int weeks)

    minusDays(int days)
    等方法用于减少年、月、周、天数。
// LocalDate 加减
LocalDate localDate = LocalDate.now();
LocalDate plusLocalDate = localDate.plusYears(1).plusMonths(1).plusWeeks(1).plusDays(1);
System.out.println("plusLocalDate:"+plusLocalDate);
plusLocalDate = localDate.plus(1, ChronoUnit.YEARS).plus(1, ChronoUnit.MONTHS).plus(1, ChronoUnit.WEEKS).plus(1, ChronoUnit.DAYS);
System.out.println("plusLocalDate:"+plusLocalDate);
plusLocalDate = localDate.plus(Duration.of(1, ChronoUnit.YEARS)).plus(Duration.of(1, ChronoUnit.MONTHS)).plus(Duration.of(1, ChronoUnit.WEEKS)).plus(Duration.ofDays(1));
System.out.println("plusLocalDate:"+plusLocalDate);
plusLocalDate = localDate.plus(Period.ofYears(1)).plus(Period.ofMonths(1)).plus(Period.ofWeeks(1)).plus(Period.ofDays(1));
System.out.println("plusLocalDate:"+plusLocalDate);

LocalDate minusLocalDate = localDate.minusYears(1).minusMonths(1).minusWeeks(1).minusDays(1);
System.out.println("minusLocalDate:"+minusLocalDate);
minusLocalDate = localDate.minus(1, ChronoUnit.YEARS).minus(1, ChronoUnit.MONTHS).minus(1, ChronoUnit.WEEKS).minus(1, ChronoUnit.DAYS);
System.out.println("minusLocalDate:"+minusLocalDate);
minusLocalDate = localDate.minus(Duration.of(1, ChronoUnit.YEARS)).minus(Duration.of(1, ChronoUnit.MONTHS)).minus(Duration.of(1, ChronoUnit.WEEKS)).minus(Duration.ofDays(1));
System.out.println("minusLocalDate:"+minusLocalDate);
minusLocalDate = localDate.minus(Period.ofYears(1)).minus(Period.ofMonths(1)).minus(Period.ofWeeks(1)).minus(Period.ofDays(1));
System.out.println("minusLocalDate:"+minusLocalDate);

3、LocalTime 加减

  • plusHours(int hours)
    ,
    plusMinutes(int minutes)
    ,
    plusSeconds(int seconds)
    :分别用于向
    LocalTime
    对象添加指定的小时数、分钟数和秒数。
  • 同样支持
    plus(1, ChronoUnit.XXX)

    plus(Duration duration)
    方法,用于增加时间单位。

  • plusXxx()
    方法相对应,
    minusXxx()
    方法用于从日期时间对象中减少指定的单位。
    minus(1, ChronoUnit.XXX)
    ,
    minus(Duration duration)
    ,
    minus(Period period)
    方法也分别用于减少指定的日期或时间单位。例如
    minusHours(int hours)

    minusMinutes(int minutes)

    minusSeconds(int seconds)
    等方法用于减少小时、分钟、秒数。
// LocalTime 加减
LocalTime localTime = LocalTime.now();
LocalTime plusLocalTime = localTime.plusHours(1).plusMinutes(1).plusSeconds(1);
System.out.println("plusLocalTime:"+plusLocalTime);
plusLocalTime = localTime.plus(1, ChronoUnit.HOURS).plus(1, ChronoUnit.MINUTES).plus(1, ChronoUnit.SECONDS);
System.out.println("plusLocalTime:"+plusLocalTime);
plusLocalTime = localTime.plus(Duration.ofHours(1)).plus(Duration.of(1, ChronoUnit.MINUTES)).plus(Duration.of(1, ChronoUnit.SECONDS));
System.out.println("plusLocalTime:"+plusLocalTime);

LocalTime minusLocalTime = localTime.minusHours(1).minusMinutes(1).minusSeconds(1);
System.out.println("minusLocalTime:"+minusLocalTime);
minusLocalTime = localTime.minus(1, ChronoUnit.HOURS).minus(1, ChronoUnit.MINUTES).minus(1, ChronoUnit.SECONDS);
System.out.println("minusLocalDateTime:"+minusLocalTime);
minusLocalTime = localTime.minus(Duration.ofHours(1)).minus(Duration.of(1, ChronoUnit.MINUTES)).minus(Duration.of(1, ChronoUnit.SECONDS));
System.out.println("minusLocalDateTime:"+minusLocalTime);
日期时间修改指定值

LocalDate

LocalTime

LocalDateTime

ZonedDateTime
可以通过相对应的
withXxx()
方法修改指定的值。

1、LocalDate

  • LocalDate.withYear(int year)
    :修改年份字段。
  • LocalDate.withMonth(int month)
    :修改月份字段(注意月份是从1开始计数的)。
  • LocalDate.withDayOfMonth(int dayOfMonth)
    :修改日期字段。
LocalDate localDate = LocalDate.of(2024, 3, 12);
LocalDate newDate = localDate.withYear(2025).withMonth(4).with(ChronoField.DAY_OF_MONTH, 13);
System.out.println("newDate:"+newDate);

2、LocalTime

  • LocalTime.withHour(int hour)
    :修改小时字段。
  • LocalTime.withMinute(int minute)
    :修改分钟字段。
  • LocalTime.withSecond(int second)
    :修改秒字段。
  • LocalTime.withNano(int nanoOfSecond)
    :修改纳秒字段。
LocalTime localTime = LocalTime.of(17, 25, 30);
LocalTime newTime = localTime.withHour(18).withMinute(26).with(ChronoField.SECOND_OF_MINUTE, 31);
System.out.println("newTime:"+newTime);

3、LocalDateTime

  • LocalDateTime.withYear(int year)
  • LocalDateTime.withMonth(int month)
  • LocalDateTime.withDayOfMonth(int dayOfMonth)
  • LocalDateTime.withHour(int hour)
  • LocalDateTime.withMinute(int minute)
  • LocalDateTime.withSecond(int second)
  • LocalDateTime.withNano(int nanoOfSecond)
LocalDateTime localDateTime = LocalDateTime.of(2024, 3, 12, 17, 25, 30);
LocalDateTime newDateTime = localDateTime.withYear(2025).withMonth(4).with(ChronoField.DAY_OF_MONTH, 13).withHour(18).withMinute(26).with(ChronoField.SECOND_OF_MINUTE, 31);
System.out.println("newDateTime:"+ newDateTime);

4、ZonedDateTime

  • 除了上述的日期和时间字段外,还有时区相关的
    withZoneSameInstant(ZoneId zone)
    方法,可以改变时区的同时保持同一瞬间不变。
ZonedDateTime zonedDateTime = ZonedDateTime.of(2024, 3, 12, 17, 25, 30, 0, ZoneId.of("Europe/London"));
ZonedDateTime newZonedDateTime = zonedDateTime.withZoneSameInstant(ZoneId.of("America/New_York"));
System.out.println("newZonedDateTime:"+ newZonedDateTime);

除此之外,调整日期时间还可以通过
TemporalAdjusters

TemporalAdjuster
是一个函数式接口,用于根据给定的规则调整日期时间对象。Java8的
java.time.temporal
包中预定义了一系列常用的
TemporalAdjuster
实现,例如获取下一个工作日、月初、月末等。

LocalDate date = LocalDate.of(2024, 3, 11);
// 下一个工作日
LocalDate nextWorkingDay = date.with(TemporalAdjusters.next(DayOfWeek.MONDAY)); // 如果11号不是周一,则返回下一个周一的日期
// 下一个月的第一天
LocalDate firstDayNextMonth = date.with(TemporalAdjusters.firstDayOfMonth()); // 返回4月1日
// 当月的最后一个工作日
LocalDate lastWorkingDay = date.with(TemporalAdjusters.lastInMonth(DayOfWeek.FRIDAY)); // 返回3月最后一个周五的日期

// 自定义 TemporalAdjuster
TemporalAdjuster adjuster = temporal -> {
    return temporal.plusDays(10).with(TemporalAdjusters.lastDayOfMonth());
};
LocalDate tenthDayNextMonthEnd = date.with(adjuster); // 返回4月最后一个日期,前提是先加10天
日期时间的比较

在Java8及其以后版本的日期时间API中,
isBefore()

isAfter()
方法是
java.time
包中的
LocalDate

LocalTime

LocalDateTime

ZonedDateTime
等日期时间类所共有的方法,用于比较两个日期时间对象的先后顺序。

isBefore()

  • 此方法用于判断当前对象是否早于另一个日期时间对象。
  • 如果当前对象的时间点在参数对象之前,则返回
    true
    ;否则返回
    false
LocalDate date1 = LocalDate.of(2024, 3, 11);
LocalDate date2 = LocalDate.of(2024, 3, 12);
boolean isEarlier = date1.isBefore(date2); // 返回 true,因为 date1 在 date2 之前

isAfter()

  • 此方法用于判断当前对象是否晚于另一个日期时间对象。
  • 如果当前对象的时间点在参数对象之后,则返回
    true
    ;否则返回
    false
LocalDateTime time1 = LocalDateTime.of(2024, 3, 11, 10, 0);
LocalDateTime time2 = LocalDateTime.of(2024, 3, 11, 9, 0);
boolean isLater = time1.isAfter(time2); // 返回 true,因为 time1 在 time2 之后

compareTo()
在Java 8的
java.time
包中,大部分日期时间类如
LocalDate

LocalTime

LocalDateTime

ZonedDateTime
都实现了
Comparable
接口,从而可以直接使用
compareTo()
方法进行比较。
compareTo()
方法用于比较两个日期时间对象的先后顺序,返回值含义如下:

  • 如果当前对象早于(时间点在前)参数对象,返回负数。
  • 如果当前对象等于参数对象,返回0。
  • 如果当前对象晚于(时间点在后)参数对象,返回正数。
LocalDate date1 = LocalDate.of(2024, 3, 11);
LocalDate date2 = LocalDate.of(2024, 3, 12);

int comparisonResult = date1.compareTo(date2);

if (comparisonResult < 0) {
    System.out.println("date1 is before date2");
} else if (comparisonResult > 0) {
    System.out.println("date1 is after date2");
} else {
    System.out.println("date1 is equal to date2");
}

LocalDateTime dateTime1 = LocalDateTime.of(2024, 3, 11, 10, 30);
LocalDateTime dateTime2 = LocalDateTime.of(2024, 3, 11, 11, 00);

int timeComparisonResult = dateTime1.compareTo(dateTime2);
其他操作

在Java8的
java.time
包中,各个日期时间类如
LocalDate

LocalTime

LocalDateTime
提供了一系列
get
方法,用于获取特定字段的值。

获取日期中的特定字段:

LocalDate date = LocalDate.of(2024, 3, 11);
int dayOfMonth = date.getDayOfMonth(); // 获取当月的第几天,此处返回11
int monthValue = date.getMonthValue(); // 获取月份值,此处返回3
Month month = date.getMonth(); // 获取Month枚举,此处返回March
int year = date.getYear(); // 获取年份,此处返回2024

对于时间部分,类似地可以获取小时、分钟、秒和纳秒:

LocalTime time = LocalTime.of(19, 30, 45);
int hour = time.getHour(); // 获取小时数,此处返回10
int minute = time.getMinute(); // 获取分钟数,此处返回30
int second = time.getSecond(); // 获取秒数,此处返回45
int nano = time.getNano(); // 获取纳秒数

在SpringBoot中使用

SpringBoot
默认集成了
Jackson
作为
JSON
处理库,
Jackson
已经能自动处理
LocalDate

LocalTime

LocalDateTime
类型。

如果需要使用自定义日期时间格式,我们有两种方式:

手动更改全局配置
: 如果需要自定义日期格式,可以通过
ObjectMapper
的配置类来注册自定义的日期格式化器:

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-json</artifactId>
</dependency>
@Configuration
public class JacksonConfig {
    @Bean
    public Jackson2ObjectMapperBuilderCustomizer jsonCustomizer() {
        return builder -> {
            builder.simpleDateFormat("yyyy-MM-dd");
            // 使用Java 8时间API的日期格式器
            builder.dateFormat(new StdDateFormat().withColonInTimeZone(true));
            // 注册LocalDateTime的序列化和反序列化模块
            builder.modules(new JavaTimeModule());
        };
    }
}

手动绑定格式化配置
SpringBoot支持自动绑定HTTP请求参数到控制器方法参数中,包括
LocalDate

LocalTime

LocalDateTime
类型。客户端需发送符合日期格式的字符串,Spring Boot会自动转换成相应类型。

@PostMapping("/events")
    public ResponseEntity<Event> createEvent(@RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate date,
                                             @RequestParam @DateTimeFormat(pattern = "HH:mm:ss") LocalTime startTime,
                                             @RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime timestamp) {
        // ...
    }

或者请求或者响应VO中:

 public static class ResponseVO{

		@DateTimeFormat(pattern = "yyyy-MM-dd")
		private  LocalDate date;
		
		@DateTimeFormat(pattern = "HH:mm:ss") 
		private LocalTime startTime;
		
		@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
		private  LocalDateTime timestamp;
    }

Mybatis中使用

在MyBatis中查询MySQL数据库时,使用Java 8的
java.time.LocalDate

java.time.LocalTime

java.time.LocalDateTime
类型。

  1. 数据库表结构
    : 在MySQL数据库中,通常需要使用适合的日期时间类型来存储这些Java 8的日期时间对象。例如:
    • LocalDate
      对应
      MySQL

      DATE
      类型。
    • LocalTime
      对应
      MySQL

      TIME
      类型。
    • LocalDateTime
      对应
      MySQL

      DATETIME

      TIMESTAMP
      类型。
CREATE TABLE `test_date`(
    `id` BIGINT ( 20 ) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自增主键',
    test_local_date DATE ,
    test_local_time TIME,
    test_local_date_time DATETIME,
    PRIMARY KEY ( `id` )
)
ENGINE = INNODB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8mb4 COMMENT = '日期时间测试';
  1. 实体类映射
    : 在Java实体类中,对应字段应声明为
    LocalDate

    LocalTime

    LocalDateTime
    类型。
@Data
public class TestDate implements Serializable {
    /**
    * 自增主键
    */
    private Long id;

    private LocalDate testLocalDate;

    private LocalTime testLocalTime;

    private LocalDateTime testLocalDateTime;

    private static final long serialVersionUID = 1L;
}

3.
MyBatis配置

  • 自动类型转换
    :如果你使用的是较新的MyBatis版本(>=3.4.5),MyBatis已经内置了对Java 8日期时间类型的处理。这意味着在执行SQL查询时,MyBatis会自动将数据库中的日期时间字段转换为相应的Java8类型。
@Test
public void testInsertDate(){
	TestDate testDate = new TestDate();
	testDate.setTestLocalDate(LocalDate.of(2024, 3, 12));
	testDate.setTestLocalTime(LocalTime.of(20,10,30));
	testDate.setTestLocalDateTime(LocalDateTime.of(2024, 3, 12,20,10,30,0));
	testDateMapper.insert(testDate);
}

@Test
public void testQueryDate(){
	TestDate testDate = testDateMapper.selectByPrimaryKey(1L);
	System.out.println("testLocalDate:"+testDate.getTestLocalDate());
	System.out.println("testLocalTime:"+testDate.getTestLocalTime());
	System.out.println("testLocalDateTime:"+testDate.getTestLocalDateTime());
}

image.png

结论

综上所述,本文深入探讨了Java 8引入的全新日期时间API相较于传统的Date和Calendar类的优势及实际应用。鉴于Java 8新日期时间API在设计上的先进性和易用性,我们强烈建议开发者积极采纳并替换掉陈旧的Date和Calendar类,转而采用如LocalDate、LocalDateTime、ZonedDateTime等现代日期时间类。

Java 8新日期时间API提供了更为清晰、直观的操作接口,支持不可变对象设计模式,增强了类型安全性,并具备丰富的日期时间运算、解析与格式化功能,显著提高了代码质量与可读性。此外,新API对日期时间单位的精确度控制、时区管理以及与其他日期时间规范的兼容性等方面均表现出卓越的表现力和灵活性,使得开发者在处理各类复杂日期时间逻辑时能够更加得心应手,提升开发效率。

因此,无论是处于对代码现代化改造的需求,还是出于提高开发效率和程序稳定性的考量,迁移到Java 8的新日期时间API无疑是明智之举。通过充分利用这些强大且功能完备的工具,开发者将在日期时间处理领域实现飞跃,为项目的长期维护和发展打下坚实基础。

本文已收录于我的个人博客:
码农Academy的博客,专注分享Java技术干货,包括Java基础、Spring Boot、Spring Cloud、Mysql、Redis、Elasticsearch、中间件、架构设计、面试题、程序员攻略等

目录

一、帮助命令

  • docker version
    : 查看Docker版本信息
  • docker info
    : 查看Docker信息
  • docker --help
    : 查看帮助信息

二、运行第一个容器:hello-world

2.1 运行命令

在命令行中输入以下命令并执行:

docker run hello-world

通过运行
hello-world
镜像来验证
Docker Engine
是否已正确安装。

2.2 命令执行流程图

docker run hello-world
命令执行流程图如下。

三、镜像相关命令及其基本操作

官方文档:
https://docs.docker.com/reference/

3.1 登录私有镜像仓库

  • 命令格式

    docker login [选项] [镜像仓库URL]

以下以阿里云举例:

sudo docker login --username=你的阿里云用户名 registry.cn-hangzhou.aliyuncs.com/stupid_kid/stupid_kid

3.2 拉取镜像

  • 格式:docker pull
    [镜像仓库URL]/[命名空间名称]/[仓库名称]:[镜像版本号]

  • 示例 :
    docker pull docker.io/library/busybox:latest

  • 字段说明


    URL 命名空间 仓库名称 版本号
    docker.io library busybox latest
# docker pull docker.io/library/busybox:latest
latest: Pulling from library/busybox
ea97eb0eb3ec: Pull complete 
Digest: sha256:bde48e1751173b709090c2539fdf12d6ba64e88ec7a4301591227ce925f3c678
Status: Downloaded newer image for busybox:latest
docker.io/library/busybox:latest
  • 简化:
    docker pull busybox:latest
    (不指定则默认仓库)

image-20201206103920171

  • 可以直接使用
    docker run
    ,如果本地没有镜像会自动去仓库拉取。

    [root@localhost ~ ]# docker run hello-world
    Unable to find image 'hello-world:latest' locally
    latest: Pulling from library/hello-world
    0e03bdcc26d7: Pull complete 
    Digest: sha256:e7c70bb24b462baa86c102610182e3efcb12a04854e8c582838d92970a09f323
    Status: Downloaded newer image for hello-world:latest
    ...
    

    下载的时候,我们可以看到有若干层组成,像 0e03bdcc26d7 这样的字符串是层的唯一 ID(实际上,完整的 ID 包括 256 比特, 64 个十六进制字符组成)。使用 docker pull 命令下载中会获取并输出镜像的各层信息。当不同的镜像包括相同的层的时候,本地仅存一份内容,减小存储空间。

3.3 查看镜像基本信息

3.3.1 docker images 命令查看镜像基本信息

使用
docker images

docker images ls
命令可以列举本地主机上已有镜像的基本信息。

  • 基本用法:
    docker images [选项]
    /
    docker images ls [选项]
[root@Stupidkid ~]# docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
busybox             latest              219ee5171f80        2 days ago          1.23MB
hello-world         latest              bf756fb1ae65        11 months ago       13.3kB
这些镜像都是存储在 Docker 宿主机的 /var/lib/docker 目录下。
  • 字段说明
REPOSITORY TAG IMAGE ID CREATED SIZE
仓库名称 版本号(又称镜像标签)(latest:表示最新版本) 镜像ID 镜像创建时间到现在 镜像文件的体积

同一仓库源可以有多个"TAG",代表这个仓库源的不同个版本,我们使用" REPOSITORY:TAG" 来定义不同的镜像

如果你不指定一个镜像的版本标签,例如你只使用"ubuntu","docker"将默认使用"ubuntu:latest"镜像

其中镜像的 ID 信息十分重要,它唯一标识了镜像。在使用镜像 ID 的时候,一般可以使用该 ID 的前若干个字符组成的可区分串来替代完整的 ID。

TAG 信息用于标记来自同一个仓库的不同镜像。TAG 在同一个仓库中是唯一的。

镜像大小信息只是表示了该镜像的逻辑体积大小,实际上由于相同的镜像层本地只会存储一份,物理上占用 的存储空间会小于各镜像逻辑体积之和。

(一)、
docker images
命令常用选项

-a
: 显示所有的镜像(包括临时镜像文件)
[root@Stupidkid ~]# docker images -a
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
busybox             latest              219ee5171f80        2 days ago          1.23MB
hello-world         latest              bf756fb1ae65        11 months ago       13.3kB

-q : 只显示镜像ID
[root@Stupidkid ~]# docker images -q
219ee5171f80
bc9a0695f571
bf756fb1ae65
--digests
: 显示镜像再要信息
[root@Stupidkid ~]# docker images --digests
REPOSITORY          TAG                 DIGEST                                                                    IMAGE ID            CREATED             SIZE
busybox             latest              sha256:bde48e1751173b709090c2539fdf12d6ba64e88ec7a4301591227ce925f3c678   219ee5171f80        2 days ago          1.23MB
nginx               latest              sha256:6b1daa9462046581ac15be20277a7c75476283f969cb3a61c8725ec38d3b01c3   bc9a0695f571        11 days ago         133MB
hello-world         latest              sha256:e7c70bb24b462baa86c102610182e3efcb12a04854e8c582838d92970a09f323   bf756fb1ae65        11 months ago       13.3kB

--digests=true | false:列出镜像的数字摘要值
[root@Stupidkid ~]# docker images --digests=true
REPOSITORY          TAG                 DIGEST                                                                    IMAGE ID            CREATED             SIZE
busybox             latest              sha256:bde48e1751173b709090c2539fdf12d6ba64e88ec7a4301591227ce925f3c678   219ee5171f80        2 days ago          1.23MB
nginx               latest              sha256:6b1daa9462046581ac15be20277a7c75476283f969cb3a61c8725ec38d3b01c3   bc9a0695f571        11 days ago         133MB
hello-world         latest              sha256:e7c70bb24b462baa86c102610182e3efcb12a04854e8c582838d92970a09f323   bf756fb1ae65        11 months ago       13.3kB
[root@Stupidkid ~]# docker images --digests=false
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
busybox             latest              219ee5171f80        2 days ago          1.23MB
nginx               latest              bc9a0695f571        11 days ago         133MB
hello-world         latest              bf756fb1ae65        11 months ago       13.3kB

--no-trunc
: 显示完整的镜像信息
[root@Stupidkid ~]# docker images --no-trunc
REPOSITORY          TAG                 IMAGE ID                                                                  CREATED             SIZE
busybox             latest              sha256:219ee5171f8006d1462fa76c12b9b01ab672dbc8b283f186841bf2c3ca8e3c93   2 days ago          1.23MB
nginx               latest              sha256:bc9a0695f5712dcaaa09a5adc415a3936ccba13fc2587dfd76b1b8aeea3f221c   11 days ago         133MB
hello-world         latest              sha256:bf756fb1ae65adf866bd8c456593cd24beb6a0a061dedf42b26a993176745f6b   11 months ago       13.3kB

3.4 查看镜像详细信息

3.4.1 使用
docker inspect
命令获取镜像的详细信息

使用
docker inspect
命令获取镜像的详细信息,包括 PID、作者、架构等等。

  • 基本格式:
    docker inspect [镜像ID] / [镜像名称:版本号]

  • 示例:

    nginx:latest  none          
    [root@Stupidkid ~]# docker inspect nginx:latest 
    [
        {
            "Id": "sha256:bc9a0695f5712dcaaa09a5adc415a3936ccba13fc2587dfd76b1b8aeea3f221c",
            "RepoTags": [
                "nginx:latest"
            ],
            "RepoDigests": [
                "nginx@sha256:6b1daa9462046581ac15be20277a7c75476283f969cb3a61c8725ec38d3b01c3"
            ],
            "Parent": "",
            "Comment": "",
            "Created": "2020-11-25T00:30:19.011398516Z",
            "Container": "279e6916c4aaaf5d61e468508abd96933f4e48194bd979dc692e0196cde2d59d",
    ...
    

3.4.2
docker inspect
命令选项

-f : 可以使用golang的语言模板语法获取所需信息。
[root@Stupidkid ~]#  docker inspect -f '{{.Id}}' nginx
sha256:bc9a0695f5712dcaaa09a5adc415a3936ccba13fc2587dfd76b1b8aeea3f221c

3.5 为镜像添加tag

为了方便后续工作中使用特定的镜像,还可以使用 docker tag 命令来为本地的镜像添加标签、修改镜像名称和版本号。

  • 格式 :
    docker tag [原镜像仓库url]/[原镜像命名空间]/[原镜像仓库名称]:[版本号] [新镜像仓库url]/[新镜像命名空间]/[新镜像仓库名称]:[版本号]

    或者如下:

    docker tag [原镜像仓库url]/[原镜像命名空间]/[原镜像仓库名称]:[版本号] \
    					[新镜像仓库url]/[新镜像命名空间]/[新镜像仓库名称]:[版本号]  
    
  • 示例

    [root@Stupidkid ~]# docker tag nginx:latest registry-vpc.cn-hangzhou.aliyuncs.com/stupid_kid/stupid_kid/nginx:v1
    
    # 修改镜像名与TAG
    [root@Stupidkid ~]#  docker tag nginx:latest nginx_1:v1
    [root@Stupidkid ~]# docker images
    REPOSITORY                                                          TAG                 IMAGE ID            CREATED             SIZE
    busybox                                                             latest              219ee5171f80        2 days ago          1.23MB
    registry.cn-shanghai.aliyuncs.com/python16-shawn/busybox            v1                  219ee5171f80        2 days ago          1.23MB
    nginx                                                               latest              bc9a0695f571        11 days ago         133MB
    nginx_1                                                             v1                  bc9a0695f571        11 days ago         133MB
    registry-vpc.cn-hangzhou.aliyuncs.com/stupid_kid/stupid_kid/nginx   v1                  bc9a0695f571        11 days ago         133MB
    hello-world                                                         latest              bf756fb1ae65        11 months ago       13.3kB
    
    

修改tag后,源镜像还会存在,ID则为同一个,
在删除镜像时若指定的是ID则会删除所有匹配到ID的镜像
,若指定的是
镜像名称:版本
则仅删除指定名称的单个镜像。

3.6 查看镜像构建历史

  • 格式 :
    docker history [镜像名字:镜像版本号] / [镜像ID]

  • 示例:

    [root@Stupidkid ~]# docker history nginx:latest 
    IMAGE               CREATED             CREATED BY                                      SIZE                COMMENT
    bc9a0695f571        11 days ago         /bin/sh -c #(nop)  CMD ["nginx" "-g" "daemon…   0B                  
    <missing>           11 days ago         /bin/sh -c #(nop)  STOPSIGNAL SIGQUIT           0B                  
    <missing>           11 days ago         /bin/sh -c #(nop)  EXPOSE 80                    0B                  
    <missing>           11 days ago         /bin/sh -c #(nop)  ENTRYPOINT ["/docker-entr…   0B                  
    <missing>           11 days ago         /bin/sh -c #(nop) COPY file:0fd5fca330dcd6a7…   1.04kB              
    <missing>           11 days ago         /bin/sh -c #(nop) COPY file:08ae525f517706a5…   1.95kB              
    <missing>           11 days ago         /bin/sh -c #(nop) COPY file:e7e183879c35719c…   1.2kB               
    <missing>           11 days ago         /bin/sh -c set -x     && addgroup --system -…   63.6MB              
    <missing>           11 days ago         /bin/sh -c #(nop)  ENV PKG_RELEASE=1~buster     0B                  
    <missing>           11 days ago         /bin/sh -c #(nop)  ENV NJS_VERSION=0.4.4        0B                  
    <missing>           11 days ago         /bin/sh -c #(nop)  ENV NGINX_VERSION=1.19.5     0B                  
    <missing>           2 weeks ago         /bin/sh -c #(nop)  LABEL maintainer=NGINX Do…   0B                  
    <missing>           2 weeks ago         /bin/sh -c #(nop)  CMD ["bash"]                 0B                  
    <missing>           2 weeks ago         /bin/sh -c #(nop) ADD file:d2abb0e4e7ac17737…   69.2MB           
    

3.7 搜索镜像

3.7.1 docker search 搜索镜像

在 docker 中搜索镜像主要使用 Search 子命令,默认只搜索 Docker Hub 官方镜像仓库中的镜像。

  • 格式 :
    docker search [所搜索的镜像名称] [选项]

  • 示例:

    [root@Stupidkid ~]# docker search python
    NAME                             DESCRIPTION                                     STARS               OFFICIAL            AUTOMATED
    python                           Python is an interpreted, interactive, objec…   5661                [OK]                
    django                           Django is a free web application framework, …   1024                [OK]                
    pypy                             PyPy is a fast, compliant alternative implem…   256                 [OK]                
    nikolaik/python-nodejs           Python with Node.js                             55                                      [OK]
    joyzoursky/python-chromedriver   Python with Chromedriver, for running automa…   54                                      [OK]
    arm32v7/python                   Python is an interpreted, interactive, objec…   53                                      
    circleci/python                  Python is an interpreted, interactive, objec…   41                                      
    centos/python-35-centos7         Platform for building and running Python 3.5…   38                                      
    centos/python-36-centos7         Platform for building and running Python 3.6…   30                                      
    hylang                           Hy is a Lisp dialect that translates express…   28                  [OK]                
    arm64v8/python                   Python is an interpreted, interactive, objec…   24                                      
    centos/python-27-centos7         Platform for building and running Python 2.7…   17                                      
    bitnami/python                   Bitnami Python Docker Image                     10                                      [OK]
    publicisworldwide/python-conda   Basic Python environments with Conda.           6                                       [OK]
    dockershelf/python               Repository for docker images of Python. Test…   5                                       [OK]
    clearlinux/python                Python programming interpreted language with…   4                                       
    d3fk/python_in_bottle            Simple python:alpine completed by Bottle+Req…   4                                       [OK]
    i386/python                      Python is an interpreted, interactive, objec…   3                                       
    centos/python-34-centos7         Platform for building and running Python 3.4…   2                                       
    ppc64le/python                   Python is an interpreted, interactive, objec…   2                                       
    amd64/python                     Python is an interpreted, interactive, objec…   1                                       
    saagie/python                    Repo for python jobs                            0                                       
    s390x/python                     Python is an interpreted, interactive, objec…   0                                       
    ccitest/python                   CircleCI test images for Python                 0                                       [OK]
    openshift/python-33-centos7      DEPRECATED: A Centos7 based Python v3.3 imag…   0                                       
    
    
  • 字段说明:


    • NAME
      :镜像名称
    • DESCRIPTION
      :镜像描述
    • STARS
      :用户评价,反映一个镜像的受欢迎程度(收藏个数)
    • OFFICIAL
      :是否为官方构建
    • AUTOMATED
      :自动构建,表示该镜像由 Docker Hub 自动构建流程创建的。

3.7.2
docker search
命令常用选项

-f :过滤
# 搜索被收藏超过 300 个的并且关键词包括 Python 的镜像
docker search -f stars=300 python

# 搜索官方提供的带有 Redis 关键字的镜像
docker search -f is-official=true redis
NAME        DESCRIPTION              STARS            OFFICIAL            AUTOMATED
redis      Redis is an open source key-value store that…   8792                [OK]
-s
:列出搜藏不小于指定值的镜像
  • 示例:
    docker search python -s 300
--automated
: 只列出 automated build 类型的镜像
  • 示例:
    docker search python --automated
--limit
: 限制输出结果
--no-trunc: 不截断输出结果

3.8 删除镜像

3.8.1 使用docker rmi命令删除镜像

使用
docker rmi
命令,相当于
docker image rm
命令。

  • 格式 :
    docker rmi [镜像名称:版本号] / [镜像ID]

示例:

[root@Stupidkid ~]# docker images
REPOSITORY                                                          TAG                 IMAGE ID            CREATED             SIZE
busybox                                                             latest              219ee5171f80        2 days ago          1.23MB
registry.cn-shanghai.aliyuncs.com/python16-shawn/busybox            v1                  219ee5171f80        2 days ago          1.23MB
nginx                                                               latest              bc9a0695f571        11 days ago         133MB
nginx_1                                                             v1                  bc9a0695f571        11 days ago         133MB
registry-vpc.cn-hangzhou.aliyuncs.com/stupid_kid/stupid_kid/nginx   v1                  bc9a0695f571        11 days ago         133MB
hello-world                                                         latest              bf756fb1ae65        11 months ago       13.3kB
[root@Stupidkid ~]# docker rmi nginx:v1
Error: No such image: nginx:v1
[root@Stupidkid ~]# docker images
REPOSITORY                                                          TAG                 IMAGE ID            CREATED             SIZE
busybox                                                             latest              219ee5171f80        2 days ago          1.23MB
registry.cn-shanghai.aliyuncs.com/python16-shawn/busybox            v1                  219ee5171f80        2 days ago          1.23MB
nginx                                                               latest              bc9a0695f571        11 days ago         133MB
nginx_1                                                             v1                  bc9a0695f571        11 days ago         133MB
registry-vpc.cn-hangzhou.aliyuncs.com/stupid_kid/stupid_kid/nginx   v1                  bc9a0695f571        11 days ago         133MB
hello-world                                                         latest              bf756fb1ae65        11 months ago       13.3kB

(一)、docker rmi命令选项
-f : 强制删除。镜像已经运行为容器则无法删除,可以使用-f强制删除。

示例:删除单个

[root@localhost ~ ]# docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                   NAMES
9bf5ef44a314        nginx               "/docker-entrypoint.…"   51 minutes ago      Up 51 minutes       0.0.0.0:32770->80/tcp   funny_northcutt
[root@localhost ~ ]# docker rmi nginx:latest
Error response from daemon: conflict: unable to remove repository reference "nginx:latest" (must force) - container b1bc639c450e is using its referenced image bc9a0695f571
[root@localhost ~ ]# docker rmi -f nginx:latest
Untagged: nginx:latest
[root@localhost ~ ]# docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
nginx_2             v1                  2c45f814f15b        38 minutes ago      131MB
<none>              <none>              bc9a0695f571        7 days ago          133MB

删除多个:

  • docker rmi -f [镜像名1:TAG] [镜像名2:TAG]...

删除全部:

  • docker rmi -f $(docker images -qa)

3.9 清理镜像

使用一段时间之后,docker 会产生很多临时镜像文件,以及一些没有被使用的镜像, 我们可以通过 docker image prune 命令来进行清理。

  • 格式:docker image prune [选项]
docker image prune
命令选项
  • -a:清理所有没有当前使用的镜像,不仅是临时镜像。
  • -f: 强制删除。等同于rmi删除。

3.10 推送镜像

push将本地镜像推送到网上的个人的私有仓库中,例如阿里云的私有仓库。

  • 格式 :
    docker push [镜像仓库URL]/[命名空间名称]/[仓库名称]:[版本号]

  • 示例:


    1. 先登录私有仓库。

      docker login --username=“登录名,最好纯英文” 仓库URL
      
      [root@Stupidkid ~]# docker login --username="兴欣工作室" registry.cn-hangzhou.aliyuncs.com/stupid_kid/stupid_kid
      Password: 
      WARNING! Your password will be stored unencrypted in /root/.docker/config.json.
      Configure a credential helper to remove this warning. See
      https://docs.docker.com/engine/reference/commandline/login/#credentials-store
      
      Login Succeeded
      
      
    2. 将镜像改名

      # 私有仓库名最好设置为镜像名,在私有仓库内只能查看版本,而没有镜像名。
      docker tag 本地镜像名 [镜像仓库URL]/[命名空间名称]/[仓库名称]:[版本号]
      
    3. 推送镜像,push一次只能推送一个镜像。

      [root@Centos7 docker]# docker push registry.cn-hangzhou.aliyuncs.com/alvinos/py15-nginx:1.19.2
      The push refers to repository [registry.cn-hangzhou.aliyuncs.com/alvinos/py15-nginx]
      908cf8238301: Pushed 
      eabfa4cd2d12: Pushed 
      60c688e8765e: Pushed 
      f431d0917d41: Pushed 
      07cab4339852: Pushed 
      1.19.2: digest: sha256:794275d96b4ab96eeb954728a7bf66666656570e8372ecd5ed0cbc7280313a27d19 size: 1362
      

3.11 构建镜像

构建镜像一般有三种情况,基于容器导入、基于本地模板导入、基于 Dockerfile 创建,本节主讲基于容器保存镜像和本地保存镜像文件导入。

1、保存容器为镜像

  • 格式:
    docker commit [选项] [容器ID] / [容器名称:版本号]
docker commit
命令选项
  • -a : 指定作者。
  • -m : 简介。
  • -p : 保存镜像时,容器暂停运行。

示例:

[root@localhost ~ ]# docker commit -a 'chirou' -m 'nginx_demo' -p 50cf6c577510
sha256:94738e2585944aa455e0c3e1bb174fba02dc18ea17135811d1b874ea0beaab7e
[root@localhost ~ ]# docker images
REPOSITORY               TAG             IMAGE ID            CREATED             SIZE
<none>                  <none>           94738e258594       4 seconds ago		 133MB

# 使用inspect查看详细信息。
[root@localhost ~ ]# docker inspect 94738e258594 | grep chirou
        "Author": "chirou",
[root@localhost ~ ]# docker inspect 94738e258594 | grep nginx_demo
        "Comment": "nginx_demo",

2、导入与导出容器

某些时候,需要将容器或镜像保存成文件从一个系统迁移到另外一个系统,此时可以使用 Docker 的导入和导出功能,这也是Docker 自身提供的一个重要特性。导出的文件是一个tar包,可以通过压缩命令进行压缩,然后进行传输。

export

import
比较
export

export将
容器
导出到标准输出,可以使用输出重定向或-o选项至文件中。

  • 格式:
    docker export [容器名或ID] > [文件名称]

  • 示例:将nginx容器导出


    • docker export nginx > export_nginx.tar
import

import 是将
export导出
的文件导入为镜像,可以自定义导入的镜像名称和版本号。

import虽然可以导入save保存的文件但是导入后无法运行。

  • 格式:
    docker import [文件名称] [自定义镜像名称]:[版本号]

  • 示例:将上面导出的文件导入

    [root@localhost ~ ]# docker import export_nginx.tar import_nginx:v1
    87b71baffd0c8e5b2b98884caee97a9d3abcee444e9bad10c865db0daaafa024
    
    [root@localhost ~ ]# docker images
    REPOSITORY             TAG              IMAGE ID            CREATED             SIZE
    import_nginx           v1              87b71baffd0c        44 seconds ago      131MB
    nginx                 latest           bc9a0695f571        10 days ago         133MB
    
    # 通过export导出的文件,使用import导入后无法直接运行。
    [root@localhost mnt ]# docker run -d import_nginx:v1
    docker: Error response from daemon: No command specified.
    

可以看到,通过export导出的容器文件,再使用import导入后,SIZE是要比源镜像小的。这是因为export导出的是容器,并没有源镜像的全部内容,比如会丢失构建历史记录和元数据信息等文件,相当于仅保存容器当时的快照状态,这会导致export导出的文件无法直接通过run命令运行,解决方法:

运行时通过-it选项,给容器分配一个伪终端。

docker run -dit import_nginx:v1 sh

推荐使用commit来保存容器,然后再使用save保存,能避免上述问题。

3、导入和导出镜像

save和load

save能将
镜像
完整的保存下来,包括镜像ID和构建历史。一样可用输出重定向或-o选项保存至tar包中,并且save支持将多个镜像保存至一个tar包中。

格式:

docker save [镜像名或ID ...] > [压缩包名称]
docker save -o [压缩包名称] [镜像名称或ID ...]

实例:

# 不指定版本号则默认为latest,表示最新版。
[root@localhost ~ ]# docker save busybox nginx > box_nginx.tar
[root@localhost ~ ]# ll
-rw-r--r--  1 root root 138553344 12月  5 19:30 box_nginx.tar

load是将
save保存的镜像文件
载入为镜像。save保存时若使用镜像ID保存镜像,导入时则没有镜像名称,load在导入时也不能自定义镜像名称,可以在导入后使用docker tag命令修改。

格式:

docker load < [压缩包名称]
<相当于-i选项,指定导入的文件,默认是STDIN。

实例:

[root@localhost ~ ]# docker save dc3bacd8b5ea bc9a0695f571 > box_nginx.tar

# 将原镜像删除
[root@localhost ~ ]# docker rmi bc9a0695f571 dc3bacd8b5ea
# 载入镜像
[root@localhost ~ ]# docker load < box_nginx.tar 
Loaded image ID: sha256:bc9a0695f5712dcaaa09a5adc415a3936ccba13fc2587dfd76b1b8aeea3f221c
Loaded image ID: sha256:dc3bacd8b5ea796cea5d6070c8f145df9076f26a6bc1c8981fd5b176d37de843

# 此时ID还是原来的,但tag信息都为none
[root@localhost ~ ]# docker images
<none>              <none>              bc9a0695f571        10 days ago         133MB
<none>              <none>              dc3bacd8b5ea        11 days ago        1.23MB

四、容器常用命令及基本操作

4.1
docker ps
查看容器

ps:该子命令能查看当前正在运行的容器

示例:

[root@localhost ~ ]# docker ps
CONTAINER ID     IMAGE     COMMAND     CREATED      STATUS       PORTS           NAMES
b1a13dfe7105    nginx     "/docker-entrypoint.…"   3 seconds ago    Up 2 seconds        0.0.0.0:32774->80/tcp   reverent_maxwell

字段说明:

CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
容器 ID 所属镜像 命令 创建时间 状态 端口 容器名称

4.1.1
docker ps
常用选项

-a: 查看所有容器(包括运行和停止)

docker ps -a

-f:查看停止的容器

docker ps -f status=exited

-q:静默模式,仅显示ID信息

[root@localhost ~ ]# docker ps -aq
b1a13dfe7105

-n:列出最近创建的 n 个容器。

docker ps -n 5

-l : 查看最后一次运行的容器

docker ps -l

inspect

该命令能够查看该容器详细信息

[root@localhost ~ ]# docker inspect b1a13dfe7105
[
    {
        "Id": "b1a13dfe71056d8c02fb90e52a503bbedc60301f62e9c49604fa86c2258fd310",
        "Created": "2020-12-05T12:18:17.241753416Z",
        "Path": "/docker-entrypoint.sh",
        "Args": [
            "nginx",
            "-g",
            "daemon off;"
        。。。省略行。。。

同样支持 -f 选项过滤指定信息。

[root@localhost ~ ]# docker inspect -f '{{.Id}}' b1a13dfe7105
b1a13dfe71056d8c02fb90e52a503bbedc60301f62e9c49604fa86c2258fd310

4.2 查看容器日志

4.2.1
docker logs
查看容器日志

  • 格式:
    docker logs [option] [容器名称或ID]

4.2.2
docker logs
命令选项

  • -f:跟踪日志输出。类似tail -f命令。
  • --since:显示某个开始时间的所有日志。
  • -t:显示时间戳。
  • --tail N:仅列出最新N条容器日志。

示例:

[root@localhost ~ ]# docker logs b1a13dfe7105
/docker-entrypoint.sh: /docker-entrypoint.d/ is not empty, will attempt to perform configuration
/docker-entrypoint.sh: Looking for shell scripts in /docker-entrypoint.d/
/docker-entrypoint.sh: Launching /docker-entrypoint.d/10-listen-on-ipv6-by-default.sh
10-listen-on-ipv6-by-default.sh: Getting the checksum of /etc/nginx/conf.d/default.conf

显示时间戳:

[root@localhost ~ ]# docker logs -t b1a13dfe7105
2020-12-05T12:18:17.500960288Z /docker-entrypoint.sh: /docker-entrypoint.d/ is not empty, will attempt to perform configuration
2020-12-05T12:18:17.500995723Z /docker-entrypoint.sh: Looking for shell scripts in /docker-entrypoint.d/
。。。省略行。。。

仅显示三条日志:

# 仅查看3条日志。
[root@localhost ~ ]# docker logs --tail 3 b1a13dfe7105
192.168.112.1 - - [05/Dec/2020:13:46:53 +0000] "GET /favicon.ico HTTP/1.1" 404 153 "http://192.168.112.129:32775/" "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:83.0) Gecko/20100101 Firefox/83.0" "-"
192.168.112.1 - - [05/Dec/2020:13:46:54 +0000] "GET / HTTP/1.1" 304 0 "-" "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:83.0) Gecko/20100101 Firefox/83.0" "-"
192.168.112.1 - - [05/Dec/2020:13:46:55 +0000] "GET / HTTP/1.1" 304 0 "-" "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:83.0) Gecko/20100101 Firefox/83.0" "-"

查看2020年12月5日之后的日志:

# 日期格式不能错误。
[root@localhost ~ ]# docker logs --since="2020-12-04" b1a13dfe7105
/docker-entrypoint.sh: /docker-entrypoint.d/ is not empty, will attempt to perform configuration
/docker-entrypoint.sh: Looking for shell scripts in /docker-entrypoint.d/
/docker-entrypoint.sh: Launching /docker-entrypoint.d/10-listen-on-ipv6-by-default.sh
10-listen-on-ipv6-by-default.sh: Getting the checksum of /etc/nginx/conf.d/default.conf
10-listen-on-ipv6-by-default.sh: Enabled listen on IPv6 in /etc/nginx/conf.d/default.conf
/docker-entrypoint.sh: Launching /docker-entrypoint.d/20-envsubst-on-templates.sh
/docker-entrypoint.sh: Configuration complete; ready for start up
/docker-entrypoint.sh: /docker-entrypoint.d/ is not empty, will attempt to perform configuration
。。。省略行。。。

4.3 运行容器

4.3.1 docker run 运行容器

docker run
能将一个镜像运行为容器。容器当中至少有一个进程运行在前台。

  • 格式:
    docker run [选项] [镜像名称|镜像ID] [容器启动后内部执行的命令]

4.3.2
docker run
命令常用选项

-d : 以守护进程的方式运行(在 run 后面加上 -d 参数,则会创建一个守护式容器在后台运行(这样创建容器后不会自动登录容器,如果只加 -i -t 两个参数,创建容器后就会自动进容器里))

-p : 指定端口映射前者是宿主机端口,后者是容器内的映射端口。可以使用多个 -p 做多个端口映射。

# 格式
docker run -p 宿主主机端口:容器向外暴露的端口 [镜像名称:tag | 镜像ID]
docker run -d -p 8899:80 nginx:1.19.2

-P : 随机端口映射,随机使用宿主机的可用端口与容器内暴露的端口映射。

docker run -d -P nginx:1.19.2

--name: 指定容器的名称(为创建的容器命名),同一台宿主主机上的docker名称不能重复。

docker run -d --name 自定义容器名 -P nginx:1.19.2

--rm:当一个容器停止后,就立即删除。

docker run -d --rm nginx:1.19.2

-i : 表示运行容器

-t : 表示容器启动后会进入其命令行。加入这两个参数后,容器创建就能登录进去。即分配一个伪终端;通常和-i连用

# 运行容器后执行bash命令。
docker run -ditP nginx bash

-e : 在容器内设置一个环境变量。

docker run -d -e NGINX_NAME=nginx nginx:1.19.2

--network 指定网络模式,下篇再讲docker网络。

--link:链接到另一个容器。

-h:指定容器内的主机名。

-v: 映射存储卷,可以映射文件及文件夹。表示目录映射关系(前者是宿主机目录,后者是映射到宿主机上的目录),可以使用多个 -v 做多个目录或文件映射。注意:最好做目录映射,在宿主机上做修改,然后共享到容器上;映射后在宿主机文件内的修改都会映射到容器内的文件中。

docker run -d -v 宿主机文件路径:容器内文件路径 nginx:1.19.2

4.5 目录挂载(容器数据卷操作)

我们可以在创建容器的时候,将宿主机的目录与容器内的目录进行映射,这样我们就可以通过修改宿主机某个目录的文件从而去影响容器,而且这个操作是双向绑定的,也就是说容器内的操作也会影响到宿主机,实现备份功能。

但是容器被删除的时候,宿主机的内容并不会被删除。如果多个容器挂载同一个目录,其中一个容器被删除,其他容器的内容也不会受到影响。

容器与宿主机之间的数据卷属于引用的关系,数据卷是从外界挂载到容器内部中的,所以可以脱离容器的生命周期而独立存在,正是由于数据卷的生命周期并不等同于容器的生命周期,在容器退出或者删除以后,数据卷仍然不会受到影响,数据卷的生命周期会一直持续到没有容器使用它为止。

创建容器添加
-v
参数,格式为
宿主机目录:容器目录
,例如:

docker run -di -v /mydata/docker_centos/data:/usr/local/data --name centos7-01 centos:7
# 多目录挂载
docker run -di -v /宿主机目录:/容器目录 -v /宿主机目录2:/容器目录2 镜像名

目录挂载操作可能会出现权限不足的提示。这是因为 CentOS7 中的安全模块 SELinux 把权限禁掉了,在 docker run 时通过
--privileged=true
给该容器加权限来解决挂载的目录没有权限的问题。

4.5.1 匿名挂载

匿名挂载只需要写容器目录即可,容器外对应的目录会在
/var/lib/docker/volume
中生成。

# 匿名挂载
docker run -di -v /usr/local/data --name centos7-02 centos:7
# 查看 volume 数据卷信息
docker volume ls

/resources/articles/docker/image-20200813201808718.png

4.5.2 具名挂载

具名挂载就是给数据卷起了个名字,容器外对应的目录会在
/var/lib/docker/volume
中生成。

# 匿名挂载
docker run -di -v docker_centos_data:/usr/local/data --name centos7-03 centos:7
# 查看 volume 数据卷信息
docker volume ls

/resources/articles/docker/image-20200813202118346.png

4.5.3 指定目录挂载

一开始给大家讲解的方式就属于指定目录挂载,这种方式的挂载不会在
/var/lib/docker/volume
目录生成内容。

docker run -di -v /mydata/docker_centos/data:/usr/local/data --name centos7-01 centos:7
# 多目录挂载
docker run -di -v /宿主机目录:/容器目录 -v /宿主机目录2:/容器目录2 镜像名

4.5.4 查看目录挂载关系

通过
docker volume inspect 数据卷名称
可以查看该数据卷对应宿主机的目录地址。

[root@localhost ~]# docker volume inspect docker_centos_data
[
    {
        "CreatedAt": "2020-08-13T20:19:51+08:00",
        "Driver": "local",
        "Labels": null,
        "Mountpoint": "/var/lib/docker/volumes/docker_centos_data/_data",
        "Name": "docker_centos_data",
        "Options": null,
        "Scope": "local"
    }
]

通过
docker inspect 容器ID或名称
,在返回的 JSON 节点中找到
Mounts
,可以查看详细的数据挂载信息。

/resources/articles/docker/image-20200813203856160.png

4.5.5 只读/读写

# 只读。只能通过修改宿主机内容实现对容器的数据管理。
docker run -it -v /宿主机目录:/容器目录:ro 镜像名
# 读写,默认。宿主机和容器可以双向操作数据。
docker run -it -v /宿主机目录:/容器目录:rw 镜像名

4.5.6 volumes-from(继承)

# 容器 centos7-01 指定目录挂载
docker run -di -v /mydata/docker_centos/data:/usr/local/data --name centos7-01 centos:7
# 容器 centos7-04 和 centos7-05 相当于继承 centos7-01 容器的挂载目录
docker run -di --volumes-from centos7-01:ro --name centos7-04 centos:7
docker run -di --volumes-from centos7-01:rw --name centos7-05 centos:7

4.6 退出容器

  • exit
    : 容器停止退出
  • Ctrl+P+Q
    : 容器不停止退出

4.7 重启容器

当修改了容器内某些配置文件后,可以使用此命令使配置生效

  • 格式:
    docker restart [容器名称 | 容器ID]

4.8 启动容器

  • docker start [容器ID / 容器名]

4.9 停止容器

  • docker stop [容器ID / 容器名]
  • docker stop -f $(docker ps -qa)
    (所有)

4.10 强制停止容器

  • docker kill [容器ID / 容器名]

4.11 刪除已停止的容器

  • docker rm [容器ID]
  • docker rm -f $(docker ps -qa)

4.12 进入容器

在使用容器的过程中,我们难免需要进入容器进行排查问题。下面来介绍进入容器的四种方式。

4.12.1 attach

通过管道,连接容器内PID=1的进程,容器至少有一个进程运行前台。attach 是最早 docker 官方推出的进入容器的命令了,不过使用该命令有一个问题。当多个窗口同时使用该命令进入该容器时,所有的窗口都会同步显示。如果有一个窗口阻塞了,那么其他窗口也无法再进行操作,当所有窗口退出时,容器结束。

  • 格式 :
    docker attach [容器名或ID]

4.12.2
exec
(官方推荐使用)

继attach 之后,exec 是官方推出的有一个新的进入容器的命令,这也是目前推荐使用的进入容器的方式。这个命令相当于在容器中执行一个命令。

  • 格式 :
    docker exec [参数] [容器名或ID] [命令]

示例:

[root@localhost ~ ]# docker exec -it reverent_maxwell sh
用这种方式相当于进入该容器,并且在退出时不会将容器也停止。

4.12.3 nsenter

进入容器中,但不进入容器内的进程。Nsenter 是 Linux 提供的命令。需要配合 docker inspect 来使用(早期没有 exec 命令时,企业当中最长用的方式之一),Docker 是用 golang 语言开发,所以它也支持 go 语言的模板语法。

  • 配合
    docker inspect
    来使用
  • 格式示例:nsenter --target $( docker inspect -f {{.State.Pid}} nginxv1 ) --mount --uts --ipc --net --pid

4.12.4 ssh

在镜像(或容器) 中安装 SSH Server,这样就能保证多人进入容器且相互之间不受干扰了,相信大家在当前的生产环境中(没有使用 Docker 的情况)也是这样做的。但是使用了 Docker 容器之后不建议使用 ssh 进入到 Docker 容器内。

  • 在容器里面安装一个 sshd 服务

4.12.5 创建并进入容器

下面这行命令的意思就是通过镜像 AA 创建一个容器 BB,运行容器并进入容器的
/bin/bash

docker run -it --name 容器名称 镜像名称:标签 /bin/bash

注意:Docker 容器运行必须有一个前台进程, 如果没有前台进程执行,容器认为是空闲状态,就会自动退出。

4.12.6 守护式方式创建容器

docker run -di --name 容器名称 镜像名称:标签

4.12.7 登录守护式容器方式

docker exec -it 容器名称|容器ID /bin/bash

4.12.8 退出当前容器

exit

4.13 停止与启动容器

# 停止容器
docker stop 容器名称|容器ID
# 启动容器
docker start 容器名称|容器ID

4.14 复制文件

复制命令类似于 Linux 系统中的 scp 命令,是将宿主主机上的内容上传到容器中,也可能是将容器中的文件下载到宿主主机中。

4.14.1 从容器内复制文件到宿主主机

  • 格式:
    docker cp [容器ID:容器内文件路径] 宿主主机路径

示例

[root@Stupidkid ~]# docker cp d78575358e04:/usr ./
[root@Stupidkid ~]# ll
总用量 919344
-rw-------.  1 root root      1526 11月 13 20:15 anaconda-ks.cfg
-rw-r--r--.  1 root root         0 12月  6 19:47 export_nginx.tar
-rw-r--r--.  1 root root      2340 12月  6 10:05 pic_spider.py
-rw-r--r--.  1 root root 941396992 12月  4 19:40 python3_django.tar
drwxr-xr-x. 10 root root       105 11月 17 08:00 usr

4.14.2 从宿主机复制文件到容器

  • 格式 :
    docker cp 宿主主机路径 [容器ID:容器内文件路径]

  • 示例:

    [root@Stupidkid ~]# docker cp ./export_nginx.tar d78575358e04:/home
    [root@Stupidkid ~]# docker exec d78575358e04 ls /home
    export_nginx.tar
    

4.15 删除容器

stop命令仅仅是将容器停止,若要删除容器则使用docker rm命令。

格式:

docker rm [option] [镜像名称或ID]

rm能删除已停止的容器,对于正在运行的容器可以使用-f选项强制删除,一般不推荐直接将一个正在运行的容器强制删除。

实例:

# 查看正在运行的容器。
[root@localhost ~ ]# docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                NAMES
1a9b4ae8766b        nginx               "/docker-entrypoint.…"   About an hour ago   Up About an hour    0.0.0.0:80->80/tcp   pedantic_hopper
# 直接使用rm无法删除。
[root@localhost ~ ]# docker rm 1a9b4ae8766b
Error response from daemon: You cannot remove a running container 1a9b4ae8766b9266f0e5256df1ac56b9647483815f60b447ae9f15d6c3355dec. Stop the container before attempting removal or force remove
# 使用-f选项可以强制删除。
[root@localhost ~ ]# docker rm -f 1a9b4ae8766b
1a9b4ae8766b

使用docker ps -a与rm和stop命令配合,可以达到一个批量处理容器的方式:

# 停止所有正在运行的容器
docker stop $(docker ps)

# 删除所有容器,包括正在运行的容器
docker rm -f $(docker ps -a)

4.16 查看容器IP地址

我们可以通过以下命令查看容器的元信息。

docker inspect 容器名称|容器ID

也可以直接执行下面的命令直接输出 IP 地址。

docker inspect --format='{{.NetworkSettings.IPAddress}}' 容器名称|容器ID