8 个基本的 Docker 容器管理命令

利用这 8 个命令可以学习 Docker 容器的基本管理方式。这是一个为 Docker 初学者准备的,带有示范命令输出的指南。

在这篇文章中,我们将带你学习 8 个基本的 Docker 容器命令,它们操控着 Docker 容器的基本活动,例如 运行run、 列举list、 停止stop、 查看历史纪录logs、 删除delete 等等。如果你对 Docker 的概念很陌生,推荐你看看我们的 介绍指南,来了解 Docker 的基本内容以及 如何 在 Linux 上安装 Docker。 现在让我们赶快进入要了解的命令:

如何运行 Docker 容器?

众所周知,Docker 容器只是一个运行于宿主操作系统host OS上的应用进程,所以你需要一个镜像来运行它。Docker 镜像以进程的方式运行时就叫做 Docker 容器。你可以加载本地 Docker 镜像,也可以从 Docker Hub 上下载。Docker Hub 是一个提供公有和私有镜像来进行拉取pull操作的集中仓库。官方的 Docker Hub 位于 hub.docker.com。 当你指示 Docker 引擎运行容器时,它会首先搜索本地镜像,如果没有找到,它会从 Docker Hub 上拉取相应的镜像。

让我们运行一个 Apache web 服务器的 Docker 镜像,比如 httpd 进程。你需要运行 docker container run 命令。旧的命令为 docker run, 但后来 Docker 添加了子命令部分,所以新版本支持下列命令:

root@kerneltalks # docker container run -d -p 80:80 httpd
Unable to find image 'httpd:latest' locally
latest: Pulling from library/httpd
3d77ce4481b1: Pull complete
73674f4d9403: Pull complete
d266646f40bd: Pull complete
ce7b0dda0c9f: Pull complete
01729050d692: Pull complete
014246127c67: Pull complete
7cd2e04cf570: Pull complete
Digest: sha256:f4610c3a1a7da35072870625733fd0384515f7e912c6223d4a48c6eb749a8617
Status: Downloaded newer image for httpd:latest
c46f2e9e4690f5c28ee7ad508559ceee0160ac3e2b1688a61561ce9f7d99d682

Docker 的 run 命令将镜像名作为强制参数,另外还有很多可选参数。常用的参数有:

  • -d:从当前 shell 脱离容器
  • -p X:Y:绑定容器的端口 Y 到宿主机的端口 X
  • –name:命名你的容器。如果未指定,它将被赋予随机生成的名字
  • -e:当启动容器时传递环境编辑及其值

通过以上输出你可以看到,我们将 httpd 作为镜像名来运行容器。接着,本地镜像没有找到,Docker 引擎从 Docker Hub 拉取了它。注意,它下载了镜像 httpd:latest, 其中 : 后面跟着版本号。如果你需要运行特定版本的容器,你可以在镜像名后面注明版本名。如果不提供版本名,Docker 引擎会自动拉取最新的版本。

输出的最后一行显示了你新运行的 httpd 容器的唯一 ID。

如何列出所有运行中的 Docker 容器?

现在,你的容器已经运行起来了,你可能想要确认这一点,或者你想要列出你的机器上运行的所有容器。你可以使用 docker container ls 命令。在旧的 Docker 版本中,对应的命令为 docker ps。

root@kerneltalks # docker container ls
CONTAINER ID        IMAGE               COMMAND              CREATED             STATUS              PORTS                NAMES
c46f2e9e4690        httpd               "httpd-foreground"   11 minutes ago      Up 11 minutes       0.0.0.0:80->80/tcp   cranky_cori

列出的结果是按列显示的。每一列的值分别为:

  • Container ID :一开始的几个字符对应你的容器的唯一 ID
  • Image :你运行容器的镜像名
  • Command :容器启动后运行的命令
  • Created :创建时间
  • Status :容器当前状态
  • Ports :与宿主端口相连接的端口信息
  • Names :容器名(如果你没有命名你的容器,那么会随机创建)

如何查看 Docker 容器的历史纪录?

在第一步我们使用了 -d 参数来将容器,在它一开始运行的时候,就从当前的 shell 中脱离出来。在这种情况下,我们不知道容器里面发生了什么。所以为了查看容器的历史纪录,Docker 提供了 logs 命令。它采用容器名称或 ID 作为参数。

root@kerneltalks # docker container logs cranky_cori
AH00558: httpd: Could not reliably determine the server's fully qualified domain name, using 172.17.0.2. Set the 'ServerName' directive globally to suppress this message
AH00558: httpd: Could not reliably determine the server's fully qualified domain name, using 172.17.0.2. Set the 'ServerName' directive globally to suppress this message
[Thu May 31 18:35:07.301158 2018] [mpm_event:notice] [pid 1:tid 139734285989760] AH00489: Apache/2.4.33 (Unix) configured -- resuming normal operations
[Thu May 31 18:35:07.305153 2018] [core:notice] [pid 1:tid 139734285989760] AH00094: Command line: 'httpd -D FOREGROUND'

这里我使用了容器名称作为参数。你可以看到在我们的 httpd 容器中与 Apache 相关的历史纪录。

如何确定 Docker 容器的进程?

容器是一个使用宿主资源来运行的进程。这样,你可以在宿主系统的进程表中定位容器的进程。让我们在宿主系统上确定容器进程。

Docker 使用著名的 top 命令作为子命令的名称,来查看容器产生的进程。它采用容器的名称或 ID 作为参数。在旧版本的 Docker 中,只可运行 docker top 命令。在新版本中,docker topdocker container top 命令都可以生效。

root@kerneltalks # docker container top  cranky_cori
UID                 PID                 PPID                C                   STIME               TTY                 TIME                CMD
root                15702               15690               0                   18:35               ?                   00:00:00            httpd -DFOREGROUND
bin                 15729               15702               0                   18:35               ?                   00:00:00            httpd -DFOREGROUND
bin                 15730               15702               0                   18:35               ?                   00:00:00            httpd -DFOREGROUND
bin                 15731               15702               0                   18:35               ?                   00:00:00            httpd -DFOREGROUND
root@kerneltalks # ps -ef |grep -i 15702
root     15702 15690  0 18:35 ?        00:00:00 httpd -DFOREGROUND
bin      15729 15702  0 18:35 ?        00:00:00 httpd -DFOREGROUND
bin      15730 15702  0 18:35 ?        00:00:00 httpd -DFOREGROUND
bin      15731 15702  0 18:35 ?        00:00:00 httpd -DFOREGROUND
root     15993 15957  0 18:59 pts/0    00:00:00 grep --color=auto -i 15702

在第一个输出中,列出了容器产生的进程的列表。它包含了所有细节,包括用户号uid、进程号pid,父进程号ppid、开始时间、命令,等等。这里所有的进程号你都可以在宿主的进程表里搜索到。这就是我们在第二个命令里做得。这证明了容器确实是宿主系统中的进程。

如何停止 Docker 容器?

只需要 stop 命令!同样,它采用容器名称或 ID 作为参数。

root@kerneltalks # docker container stop cranky_cori
cranky_cori

如何列出停止的或不活动的 Docker 容器?

现在我们停止了我们的容器,这时如果我们使用 ls 命令,它将不会出现在列表中。

root@kerneltalks # docker container ls
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES

所以,在这种情况下,如果想要查看停止的或不活动的容器,你需要在 ls 命令里同时使用 -a 参数。

root@kerneltalks # docker container ls -a
CONTAINER ID        IMAGE               COMMAND              CREATED             STATUS                     PORTS               NAMES
c46f2e9e4690        httpd               "httpd-foreground"   33 minutes ago      Exited (0) 2 minutes ago                       cranky_cori

有了 -a 参数,现在我们可以查看已停止的容器。注意这些容器的状态被标注为 已退出exited。既然容器只是一个进程,那么用“退出”比“停止”更合适!

如何(重新)启动 Docker 容器?

现在,我们来启动这个已停止的容器。这和运行一个容器有所区别。当你运行一个容器时,你将启动一个全新的容器。当你启动一个容器时,你将开始一个已经停止并保存了当时运行状态的容器。它将以停止时的状态重新开始运行。

root@kerneltalks #  docker container start c46f2e9e4690
c46f2e9e4690
root@kerneltalks # docker container ls -a
CONTAINER ID        IMAGE               COMMAND              CREATED             STATUS              PORTS                NAMES
c46f2e9e4690        httpd               "httpd-foreground"   35 minutes ago      Up 8 seconds        0.0.0.0:80->80/tcp   cranky_cori

如何移除 Docker 容器?

我们使用 rm 命令来移除容器。你不可以移除运行中的容器。移除之前需要先停止容器。你可以使用 -f 参数搭配 rm 命令来强制移除容器,但并不推荐这么做。

root@kerneltalks # docker container rm cranky_cori
cranky_cori
root@kerneltalks # docker container ls -a
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES

你看,一旦移除了容器,即使再使用 ls -a 命令也查看不到容器了。

Ubuntu apt 安装redis 修改redis配置

apt安装redis

sudo apt update
sudo apt install redis-server 

安装完成默认是启动了redis服务

  • 关闭
sudo service redis-server stop
  • 开启服务
sudo servcie redis-server start
  • 重启
sudo service redis-server restart

redis简单使用

redis-cli

修改redis配置 远程访问

sudo vim /etc/redis/redic.conf

注释掉本机ip

# bind 127.0.0.1 
 protected-mode no #将yes修改成no

重启一下服务: sudo service redis-server restart 就可以了。
就可以远程 RedisDesktop Manager 可视化客户端连接了。

在Ubuntu 18.04上使用apt安装Java

安装默认的JRE/JDK

安装Java的最简单方法是使用与Ubuntu一起打包的版本。默认情况下,Ubuntu 18.04包含Open JDK,它是JRE和JDK的开源版本。

该软件包将安装OpenJDK 10或11。

  • 在现在,这将安装OpenJDK 10。
  • 在2018年9月以后,这将安装OpenJDK 11。

要安装此版本,请先更新软件包索引:

sudo apt update

接下来,检查 Java 是否已经安装:

java -version

如果Java当前未安装,你将看到以下输出:

Command 'java' not found, but can be installed with:

apt install default-jre
apt install openjdk-11-jre-headless
apt install openjdk-8-jre-headless
apt install openjdk-9-jre-headless

执行以下命令来安装OpenJDK:

sudo apt install default-jre

该命令将安装Java运行时环境(JRE)。这将允许你运行几乎所有的Java软件。

验证安装:

java -version

你将看到以下输出:

openjdk version "10.0.1" 2018-04-17
OpenJDK Runtime Environment (build 10.0.1+10-Ubuntu-3ubuntu1)
OpenJDK 64-Bit Server VM (build 10.0.1+10-Ubuntu-3ubuntu1, mixed mode)

除了JRE之外,你可能还需要Java开发工具包(JDK)才能编译和运行一些特定的基于Java的软件。要安装JDK,请执行以下命令,该命令也将安装JRE:

sudo apt install default-jdk

通过检查javac Java编译器的版本来验证是否安装了JDK :

javac -version

你将看到以下输出:

javac 10.0.1

接下来,我们来看看指定我们要安装的OpenJDK版本。

安装OpenJDK的特定版本

虽然你可以安装默认的OpenJDK软件包,但你也可以安装不同版本的
OpenJDK。

OpenJDK 8

Java 8是目前的长期支持版本,虽然公共维护在2019年1月结束,但仍然得到广泛支持。要安装OpenJDK 8,请执行以下命令:

sudo apt install openjdk-8-jdk

验证安装:

java -version

你会看到这样的输出:

openjdk version "1.8.0_162"
OpenJDK Runtime Environment (build 1.8.0_162-8u162-b12-1-b12)
OpenJDK 64-Bit Server VM (build 25.162-b12, mixed mode)

也可以只安装JRE,你可以通过执行sudo apt install openjdk-8-jre来安装它。

OpenJDK 10/11

Ubuntu的存储库包含一个安装Java 10或11的软件包。在2018年9月之前,该软件包将安装OpenJDK 10.一旦Java 11发布,该软件包将安装Java 11。

要安装OpenJDK 11,请执行以下命令:

sudo apt install openjdk-11-jdk

要仅安装JRE,请使用以下命令:

sudo apt install openjdk-11-jre

安装Oracle JDK

如果你想安装由Oracle发布的正式版本Oracle JDK,则需要为要使用的版本添加新的软件包存储库。

要安装作为最新LTS版本的Java 8,请首先添加其软件包存储库:

sudo add-apt-repository ppa:webupd8team/java

当你添加存储库时,你会看到如下消息:

 Oracle Java (JDK) Installer (automatically downloads and installs Oracle JDK8). There are no actual Jav
a files in this PPA.

Important -> Why Oracle Java 7 And 6 Installers No Longer Work: http://www.webupd8.org/2017/06/why-oracl
e-java-7-and-6-installers-no.html

Update: Oracle Java 9 has reached end of life: http://www.oracle.com/technetwork/java/javase/downloads/j
dk9-downloads-3848520.html

The PPA supports Ubuntu 18.04, 17.10, 16.04, 14.04 and 12.04.

More info (and Ubuntu installation instructions):
- for Oracle Java 8: http://www.webupd8.org/2012/09/install-oracle-java-8-in-ubuntu-via-ppa.html

Debian installation instructions:
- Oracle Java 8: http://www.webupd8.org/2014/03/how-to-install-oracle-java-8-in-debian.html

For Oracle Java 10, see a different PPA: https://www.linuxuprising.com/2018/04/install-oracle-java-10-in-ubuntu-or.html

More info: https://launchpad.net/~webupd8team/+archive/ubuntu/java
Press [ENTER] to continue or Ctrl-c to cancel adding it.

按ENTER继续。然后更新你的软件包列表

sudo apt update

包列表更新后,安装Oracle Java 8:

sudo apt install oracle-java8-installer

你的系统将从Oracle下载JDK并要求你接受许可协议。接受协议并安装JDK。

管理Java版本

你可以在一台服务器上安装多个Java。你可以使用update-alternatives命令配置哪个版本是命令行上使用的默认版本。

sudo update-alternatives --config java

如果你已经在本教程中安装了所有版本的Java,则输出结果如下所示:

There are 3 choices for the alternative java (providing /usr/bin/java).

  Selection    Path                                            Priority   Status
------------------------------------------------------------
* 0            /usr/lib/jvm/java-11-openjdk-amd64/bin/java      1101      auto mode
  1            /usr/lib/jvm/java-11-openjdk-amd64/bin/java      1101      manual mode
  2            /usr/lib/jvm/java-8-openjdk-amd64/jre/bin/java   1081      manual mode
  3            /usr/lib/jvm/java-8-oracle/jre/bin/java          1081      manual mode

选择与 Java 版本关联的数字以将其用作默认值,或按下ENTER以保留当前设置。

你可以为其他Java命令执行此操作,如compiler(javac):

sudo update-alternatives --config javac

可以运行该命令的其他命令包括但不限于:keytool,javadoc和jarsigner。

设置JAVA_HOME环境变量
许多使用Java编写的程序使用JAVA_HOME环境变量来确定Java的安装位置。

要设置此环境变量,请先确定Java的安装位置。使用update-alternatives命令:

sudo update-alternatives --config java

该命令显示Java的每个安装版本及其安装路径:

There are 3 choices for the alternative java (providing /usr/bin/java).

  Selection    Path                                            Priority   Status
------------------------------------------------------------
* 0            /usr/lib/jvm/java-11-openjdk-amd64/bin/java      1101      auto mode
  1            /usr/lib/jvm/java-11-openjdk-amd64/bin/java      1101      manual mode
  2            /usr/lib/jvm/java-8-openjdk-amd64/jre/bin/java   1081      manual mode
  3            /usr/lib/jvm/java-8-oracle/jre/bin/java          1081      manual mode

Press <enter> to keep the current choice[*], or type selection number:

在这种情况下,安装路径如下所示:

OpenJDK 11位于 /usr/lib/jvm/java-11-openjdk-amd64/bin/java.
OpenJDK 8位于/usr/lib/jvm/java-8-openjdk-amd64/jre/bin/java。
Oracle Java 8位于/usr/lib/jvm/java-8-oracle/jre/bin/java。

复制首选安装的路径。然后打开/etc/environment使用nano或你最喜爱的文本编辑器:

sudo nano /etc/environment

在该文件的末尾,添加以下行,确保使用自己的复制路径替换突出显示的路径:

JAVA_HOME="/usr/lib/jvm/java-11-openjdk-amd64/bin/java"

修改此文件将为JAVA_HOME系统上的所有用户设置路径。

保存文件并退出编辑器。

现在重新加载此文件以将更改应用于当前会话:

source /etc/environment

验证是否设置了环境变量:

echo $JAVA_HOME

你会看到你刚刚设置的路径:

/usr/lib/jvm/java-11-openjdk-amd64/bin/java

其他用户需要执行该命令source /etc/environment或注销并重新登录才能应用此设置。

Nginx与Apache环境防盗链设置方法

说明:很多人的VPS流量是有限的,而一般情况下我们放在网站上的媒体文件都是可以被别人引用的,我们的文件也就成了别人的免费外链,可想而知流量会消耗的有多快,这时候设置一下防盗链还是很有必要的。

根据我们搭建的系统环境不同,我们在Nginx和Apache中设置防盗链的方法也是不同的。

Nginx防盗链方法

location ~ .*.(gif|jpg|jpeg|png|bmp|swf|mp3|wav|zip|rar)$ {
    valid_referers none blocked xirik.cn *.xirik.cn;
    if ($invalid_referer){
        return 403;
    }
    expires 30d;
}

在网站所在的配置文件*.conf中添加以上代码,添加后重启nginx就可以生效,网址记得替换成自己的。

Apache防盗链方法

首先我们需要保证开启了Apache的伪静态模块

然后把下面代码中的网址替换成自己的,复制到网站根目录下的伪静态文件.htaccess中即可生效。

RewriteEngine On
RewriteCond %{HTTP_REFERER} !^http://xirik.cn/.*$ [NC]
RewriteCond %{HTTP_REFERER} !^http://xirik.cn$ [NC]
RewriteCond %{HTTP_REFERER} !^http://www.xirik.cn/.*$ [NC]
RewriteCond %{HTTP_REFERER} !^http://www.xirik.cn$ [NC]
RewriteRule .*.(gif|jpg|jpeg|png|bmp|swf|mp3|wav|zip|rar)$ http://xirik.cn/404.html [R,NC]

apache默认对进行了编码的url 返回 404

我们通常使用 urlencode()之类的函数将斜线编码成%开头的字符串

但是默认情况下

apache发现请求的URL中有对斜线的编码后的字符,是会返回404页面的

此时,就用到了 AllowEncodedSlashes on 指令允许请求继续被处理

如果apache配置的https,那么http和https需要分别配置。

我用的apache是 Apache/2.4.6 (CentOS)

http的配置文件和ssl配置文件是分开的。

在httpd.conf 中直接在文件中增加 AllowEncodedSlashes On

重启就生效了,而https 直接放到配置ssl.conf中是没有生效。后来查到是放到 这个标签里边。

重启后终于生效。

未分类

Apache防盗链和隐藏版本信息

实验要求:

三台虚拟机分别是:linux和两台windows虚拟机,linux虚拟机为服务器,Windows7-1为客户端,Windows7-2为盗链端。

实验步骤:

一、防盗链

1.把httpd、apr、apr-util安装包解压缩到/opt目录中

tar zxvf apr-1.4.6.tar.gz -C /opt/
tar zxvf apr-util-1.4.1.tar.gz -C /opt/
tar zxvf httpd-2.4.2.tar.gz -C /opt/

2.切换到opt目录中,把解压的文件夹递归复制到apr文件夹中

未分类

3.用yum仓库安装gcc 、gcc-c++ 、pcre 、pcre-devel、zlib-devel工具包

未分类

4.进行配置文件的配置

未分类

5.转换为二进制文件且进行安装

未分类

6.开启httpd功能且建立软连

未分类

未分类

7.在httpd的主配置文件/etc/httpd.conf 下修改监听本地主机的IP和主机名域名

未分类

未分类

8.开启httpd服务关闭防火墙和增强性安全功能

service httpd start 
service iptables stop
Setenforce 0

9.切换到/usr/local/httpd/htdocs/目录下编辑首页 文件

未分类

未分类

10.把/opt目录下的图片(game.jpg、error.png)复制到当前目录下

未分类

未分类

11.打开windows7-2盗链客户端,创建文件添加首页文件和图片且更改为扩展文件,放置于C盘inetpub wwwroot目录中。

未分类

未分类

12.构建DNS解析服务,安装bind包,编辑主配置文件/etc/named.conf修改监听地址。

未分类

未分类

13.编辑区域配置文件/etc/named.rfc1912.zones 添加域名和区域数据模块

未分类

14.复制区域数据模板到benet.com.zone进行编辑

未分类

未分类

15.启动named服务

未分类

16.在站点目录/usr/local/httpd/conf/编辑vim httpd.conf文件中开启重写模块且引用防盗链规则

未分类

未分类

17.重启httpd服务,打开Windows7-1客户机就行验证
Service httpd restart

未分类

二、隐藏版本信息

1.打开抓包工具进行测试

未分类

2.到/etc/httpd.conf中开启default.conf

未分类

3.切换到/usr/local/httpd/conf/extra/目录中,编辑httpd-default.conf文件把serverToken Full改为serverToken prod。

未分类

未分类

4.重启httpd服务,进行验证。
service httpd restart

未分类

Vagrant 中安装 Mysql 如何从外边链接

在 Vagrant 中安装 Mysql 后从外部链接需要三步

  • 设置私有 ip
  • 去掉绑定 127.0.0.1
  • 对所有 ip 开放

设置私有 ip

修改 Vagrantfile 添加 private_network,这样外部可以通过该 ip 链接

config.vm.network "private_network", ip: "192.168.33.10"

这步需要放在第一步来完成,随后重新加载配置

$ vagrant reload

去掉绑定 127.0.0.1

如果你是使用 rpm 来安装的话,修改 /etc/mysql/mysql.conf.d/mysql.cnf,将绑定 127.0.0.1 这一行注释掉

# bind-address            = 127.0.0.1

对所有 ip 开放

登陆 Mysql 并对所有外网 ip 开放权限

$ mysql -uroot -p
> GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' IDENTIFIED BY 'your_mysql_password' WITH GRANT OPTION;

这样从外部通过如下命令就可以访问了

$ mysql -uroot -p -h 192.168.33.10

注意

如果在生产环境上的话建议只对指定 ip 开放权限

> GRANT ALL PRIVILEGES ON *.* TO 'root'@'213.1.2.1' IDENTIFIED BY 'your_mysql_password' WITH GRANT OPTION;

亲测Mysql表结构为InnoDB类型从ibd文件恢复数据

客户的机器系统异常关机,重启后mysql数据库不能正常启动,重装系统后发现数据库文件损坏,悲催的是客户数据库没有进行及时备份,只能想办法从数据库文件当中恢复,查找资料,试验各种方法,确认下面步骤可行:

一、找回表结构,如果表结构没有丢失直接到下一步

a、先创建一个数据库,这个数据库必须是没有表和任何操作的。
b、创建一个表结构,和要恢复的表名是一样的。表里的字段无所谓。一定要是innodb引擎的。CREATE TABLE `test`(  `testID` bigint(20)) ENGINE=InnoDB DEFAULT CHARSET=utf8;
c、关闭mysql, service mysqld stop;
d、用需要恢复的frm文件覆盖刚新建的frm文件;  
e、修改my.ini 里  innodb_force_recovery=1 , 如果不成修改为 2,3,4,5,6。
f、 启动mysql,service mysqld start;show create table test就能够看到表结构信息了。

二、找回数据

a、建立一个数据库,根据上面导出的创建表的sql执行创建表。
b、找到记录点。先要把当前数据库的表空间废弃掉,使当前ibd的数据文件和frm分离。  ALTER TABLE test DISCARD TABLESPACE;
c、把之前要恢复的 .ibd文件复制到新的表结构文件夹下。 使当前的ibd 和frm发生关系。ALTER TABLE test  IMPORT TABLESPACE;

d、将恢复好的数据导出就行了

InnoDB锁原理

背景

MySQL是一个支持插件式存储引擎的数据库系统,其中InnoDB是MySQL的事务安全的存储引擎,在OLTP系统中使用非常广乏。InnoDB最大的特性是支持事务,事务的特性包括原子性、一致性、隔离性、持久性,其中事务的隔离性,就是通过锁来实现的。在正式介绍锁之前,先来回顾一下MySql/InnoDB的隔离级别:

  • READ UNCOMMITTED 可以读取到未提交的数据,会产生脏读的问题
  • READ COMMITTED 读取已经提交的数据,不会有脏读,但是会有不可重复读和幻读。
  • REPEATABLE READ 在同一个事务中,可以重复读取,同时InnoDB在此隔离级别下不会有幻象读现象。
  • SERIALIZABLE 读取和写入都需要加锁,效率比较低。

锁的类型

InnoDB存储引擎有两种行级锁

  • 共享锁 S,允许事务读取一行数据。
  • 排他锁 X,允许事务修改一行数据。

他们的兼容性如下:

未分类

除此之外,InnoDB还有一种表级别锁,意向锁:

  • 意向共享锁 IS,表示事务想要获取表中某几行的共享锁。
  • 意向排他锁 IX,表示事务想要获取表中某几行的排他锁。

InnoDB的意向锁主要用户多粒度的锁并存的情况。比如事务A要在一个表上加S锁,如果表中的一行已被事务B加了X锁,那么该锁的申请也应被阻塞。如果表中的数据很多,逐行检查锁标志的开销将很大,系统的性能将会受到影响。为了解决这个问题,可以在表级上引入新的锁类型来表示其所属行的加锁情况,这就引出了“意向锁”的概念。举个例子,如果表中记录1亿,事务A把其中有几条记录上了行锁了,这时事务B需要给这个表加表级锁,如果没有意向锁的话,那就要去表中查找这一亿条记录是否上锁了。如果存在意向锁,那么假如事务A在更新一条记录之前,先加意向锁,再加X锁,事务B先检查该表上是否存在意向锁,存在的意向锁是否与自己准备加的锁冲突,如果有冲突,则等待直到事务A释放,而无须逐条记录去检测。事务B更新表时,其实无须知道到底哪一行被锁了,它只要知道反正有一行被锁了就行了。

说白了意向锁的主要作用是处理行锁和表锁之间的矛盾,能够显示“某个事务正在某一行上持有了锁,或者准备去持有锁”。

读取

innoDB中的数据读取分为锁定读取和非锁定读取。

非锁定读取

非锁定读指的是在读取的时候不需要加任何锁,读写不冲突。对于简单的查询语句select * from table where ?;在离级别READ UNCOMMITTED,READ COMMITTED和REPEATABLE READ下,是非锁定读取。在隔离级别SERIALIZABLE下,是锁定读取,需要获取行锁,此隔离级别效率极低,线上都不会采用。

在读多写少的OLTP系统当中,非锁定读取可以极大提高系统的并发处理能力。innoDB通过多版本的并发控制协议——MVCC (Multi-Version Concurrency Control)来实现非锁定读取,在读取的时候不用等待行上的锁释放,直接去读取行的一个快照数据。流程如下图所示:

未分类

对于行上的快照数据,innoDB是通过undo log来实现的,undo log可用于回滚事务,也可以用来实现MVCC功能。一个行上可能不只有一个版本的快照数据,对于事务隔离级别READ COMMITTED和REPEATABLE READ,他们所读取的快照版本是不一样的。在READ COMMITTED下,读取的快照数据总是最新的版本,在REPEATABLE READ下,总是读取事务开始时的行数据。

锁定读取

在某些情况下,为了保证数据的一致性,要先获取行锁,再进行数据读取。如下所示语句都会产生锁定读取:

  • select … lock in share mode; S
  • select … for update; X
  • insert into table values (..); 由于插入时需要唯一性检查,所以需要X锁。
  • update table set ? where ?; X
  • delete from table where ?; X

行锁的算法

  • Record Lock:单个行记录上锁。
  • Gap Lock:间隙锁,锁定一个范围,不包含记录本身。
  • Next-Key Lock:Gap Lock + Record Lock,锁定一个范围,并且锁定记录本身。

Record lock单条索引记录上加锁,Record lock锁住的永远是索引,而非记录本身。索引分为主键索引和非主键索引两种,如果一条sql语句操作了主键索引,MySQL就会锁定这条主键索引;如果一条语句操作了非主键索引,MySQL会先锁定该非主键索引,再锁定相关的主键索引。即使该表上没有任何索引,那么innodb会在后台创建一个隐藏的聚集主键索引,那么锁住的就是这个隐藏的聚集主键索引。所以说当一条sql没有走任何索引时,那么将会在每一条聚集索引后面加X锁。

Gap Lock锁定的是索引之间的间隙,并不是记录本身。例如有一个索引有3,5,6,10和20这几个值,他们之前的间隙包括(-∞,3)、(3,5)、(5,6)、(10,20)、(20,+∞),对于Gap Lock锁定就是这几个范围。

InnoDB在不同的隔离级别下使用的锁算法也不同。

READ COMMITTED

对于innoDB的READ COMMITTED隔离级别下,会存在幻象读问题。在该隔离级别下,除了外键约束和唯一性检查依然需要Gap Lock,其余情况均使用Record Lock进行锁定。

REPEATABLE READ

InnoDB在该隔离级别下,没有幻读现象。幻读是指在同一事务下,连续执行两次同样的SQL语句可能导致不同的结果,第二次执行可能返回之前不存在的行。例如对下述语句:select * from table where id > 10 for update; 在事务1查询之后,如果另一个事务2可以插入id大于10的记录,在事务1下次查询的时候也会返回事务2插入的记录,两次的读取结果不一样,这就是幻读。

InnoDB通过Next-key Lock来避免幻读的现象,除了锁住记录本身之外,还要锁住可能涉及到的间隙(Gap)。对于上述例子select * from table where id > 10 for update; 在REPEATABLE READ隔离级别下锁定是(10,+∞)这个范围。

总结

本文主要介绍了InnoDB的锁的类型及RC和RR级别下的加锁情况,希望能给大家带来帮助。