Golang如何安全使用协程

什么是协程?

协程(Coroutine)是在1963年由Melvin E. Conway USAF, Bedford, MA等人提出的一个概念。而且协程的概念是早于线程(Thread)提出的。但是由于协程是非抢占式的调度,无法实现公平的任务调用。也无法直接利用多核优势。因此,我们不能武断地说协程是比线程更高级的技术。

尽管,在任务调度上,协程是弱于线程的。但是在资源消耗上,协程则是极低的。一个线程的内存在MB级别,而协程只需要KB级别。而且线程的调度需要内核态与用户的频繁切入切出,资源消耗也不小。

我们把协程的基本特点归纳为:

  • 协程调度机制无法实现公平调度
  • 协程的资源开销是非常低的,一台普通的服务器就可以支持百万协程。

那么,近几年为何协程的概念可以大热。我认为一个特殊的场景使得协程能够广泛的发挥其优势,并且屏蔽掉了劣势 — 网络编程。

与一般的计算机程序相比,网络编程有其独有的特点。

  • 高并发(每秒钟上千数万的单机访问量)
  • Request/Response。程序生命期端(毫秒,秒级)
  • 高IO,低计算(连接数据库,请求API)。

最开始的网络程序其实就是一个线程一个请求设计的(Apache)。后来,随着网络的普及,诞生了C10K问题。Nginx 通过单线程异步IO把网络程序的执行流程进行了乱序化,通过IO事件机制最大化的保证了CPU的利用率。

至此,现代网络程序的架构已经形成。基于IO事件调度的异步编程。其代表作恐怕就属NodeJS了吧。

异步编程的槽点

异步编程为了追求程序的性能,强行的将线性的程序打乱,程序变得非常的混乱与复杂。对程序状态的管理也变得异常困难。写过Nginx C Module的同学应该知道我说的是什么。

我们开始吐槽NodeJS 那恶心的层层Callback。

Golang

再我们疯狂被NodeJS的层层回调恶心到的时候,Golang 作为名门之后开始走入我们的视野。并且迅速的在Web后端极速的跑马圈地。其代表者Docker 以及围绕这Docker展开的整个容器生态圈欣欣向荣起来。其最大的卖点 – 协程 开始真正的流行与讨论起来。

我们开始向写PHP一样来写全异步IO的程序。看上去美好极了,仿佛世界就是这样了。

在网络编程中,我们可以理解为Golang 的协程本质上其实就是对IO事件的封装,并且通过语言级的支持让异步的代码看上去像同步执行的一样。

安全正确的使用Golang协程

我们在使用Golang的时候不一定就要非常深入的理解与学习协程的种种技术细节。但我们得时时刻刻的警惕起来。因为无论我们的代码写得是多么的同步化(看上去所有的IO好像都是阻塞的),但其本质还是异步。在协程里面会进行各种各种的中断与调度。

  • 谨慎的使用全局变量

当我们在定义一个全局变量的时候,我们就是在埋下一颗炸弹。它可能会以你想象不到的姿势点燃并爆炸。最容易出错的就是协程竞争与数据污染。

  • 协程竞争

协程竞争指的是不同的协程同时对一个对象(尤其是原生的map)进行数据读写操作。golang 在出现这种竞争的时候将会抛出fatal错误,并且程序退出。

无论是操作struct 、int、 string map 、 slice 都请做好资源锁。

我建议的全局变量应该是只读的,最进行一次初始化。通常在main函数或package的init函数中执行。

  • 数据污染

数据污染指的是程序经过一段运行时间之后,全局变量里面的值变得与期望值不一样。

  • 协程饿死

由于协程是非抢占的式的调度,那么就有可能某一个协程长时间的运行一个协程,导致其他协程无法被调度,直到饿死。

这样的情况,通常发生再大规模的处理程序中。比如数据迭代处理,大型多媒体资源处理等生命期长的协程中。

在Golang中,提供了runtime.Gosched() 函数来主动挂起当前协程,把CPU出让,让调度器有机会调度其他协程。

func BigJob() {

    for i := 0;i < 100000000;i ++ {
        //do something ...
        //每执行1W个任务出让CPU
        if i % 10000 == 0 {
            runtime.Gosched()
        }
    }
}

总结

Golang 再语言级别提供了协程支持。让异步编程变得更加轻松,但是它并没有解决存在于并发环境(多线程)中的种种问题。我们需要掌握并发环境中编程的技术知识用来规避和解决问题。

我们要非常了解协程的特点与调度模型。并且通过仔细的检验来保证协程尽可能的被公平调度(至少在web系统中,这是一个非常重要的特性)。

Golang协程详解

一、Golang 线程和协程的区别

备注:需要区分进程、线程(内核级线程)、协程(用户级线程)三个概念。

进程、线程 和 协程 之间概念的区别

对于 进程、线程,都是有内核进行调度,有 CPU 时间片的概念,进行 抢占式调度(有多种调度算法)

对于 协程(用户级线程),这是对内核透明的,也就是系统并不知道有协程的存在,是完全由用户自己的程序进行调度的,因为是由用户程序自己控制,那么就很难像抢占式调度那样做到强制的 CPU 控制权切换到其他进程/线程,通常只能进行 协作式调度,需要协程自己主动把控制权转让出去之后,其他协程才能被执行到。

goroutine 和协程区别

本质上,goroutine 就是协程。 不同的是,Golang 在 runtime、系统调用等多方面对 goroutine 调度进行了封装和处理,当遇到长时间执行或者进行系统调用时,会主动把当前 goroutine 的CPU (P) 转让出去,让其他 goroutine 能被调度并执行,也就是 Golang 从语言层面支持了协程。Golang 的一大特色就是从语言层面原生支持协程,在函数或者方法前面加 go关键字就可创建一个协程。

其他方面的比较

1、内存消耗方面

  • 每个 goroutine (协程) 默认占用内存远比 Java 、C 的线程少。
  • goroutine:2KB
  • 线程:8MB

2、线程和 goroutine 切换调度开销方面

  • 线程/goroutine 切换开销方面,goroutine 远比线程小
  • 线程:涉及模式切换(从用户态切换到内核态)、16个寄存器、PC、SP…等寄存器的刷新等。
  • goroutine:只有三个寄存器的值修改 – PC / SP / DX.

二、协程底层实现原理

线程是操作系统的内核对象,多线程编程时,如果线程数过多,就会导致频繁的上下文切换,这些 cpu 时间是一个额外的耗费。所以在一些高并发的网络服务器编程中,使用一个线程服务一个 socket 连接是很不明智的。于是操作系统提供了基于事件模式的异步编程模型。用少量的线程来服务大量的网络连接和I/O操作。但是采用异步和基于事件的编程模型,复杂化了程序代码的编写,非常容易出错。因为线程穿插,也提高排查错误的难度。

协程,是在应用层模拟的线程,他避免了上下文切换的额外耗费,兼顾了多线程的优点。简化了高并发程序的复杂度。举个例子,一个高并发的网络服务器,每一个socket连接进来,服务器用一个协程来对他进行服务。代码非常清晰。而且兼顾了性能。

那么,协程是怎么实现的呢?

他和线程的原理是一样的,当 a线程 切换到 b线程 的时候,需要将 a线程 的相关执行进度压入栈,然后将 b线程 的执行进度出栈,进入 b线程 的执行序列。协程只不过是在 应用层 实现这一点。但是,协程并不是由操作系统调度的,而且应用程序也没有能力和权限执行 cpu 调度。怎么解决这个问题?

答案是,协程是基于线程的。内部实现上,维护了一组数据结构和 n 个线程,真正的执行还是线程,协程执行的代码被扔进一个待执行队列中,由这 n 个线程从队列中拉出来执行。这就解决了协程的执行问题。那么协程是怎么切换的呢?答案是:golang 对各种 io函数 进行了封装,这些封装的函数提供给应用程序使用,而其内部调用了操作系统的异步 io函数,当这些异步函数返回 busy 或 bloking 时,golang 利用这个时机将现有的执行序列压栈,让线程去拉另外一个协程的代码来执行,基本原理就是这样,利用并封装了操作系统的异步函数。包括 linux 的 epoll、select 和 windows 的 iocp、event 等。

由于golang是从编译器和语言基础库多个层面对协程做了实现,所以,golang的协程是目前各类有协程概念的语言中实现的最完整和成熟的。十万个协程同时运行也毫无压力。关键我们不会这么写代码。但是总体而言,程序员可以在编写 golang 代码的时候,可以更多的关注业务逻辑的实现,更少的在这些关键的基础构件上耗费太多精力。

三、协程的历史以及特点

协程(Coroutine)是在1963年由Melvin E. Conway USAF, Bedford, MA等人提出的一个概念。而且协程的概念是早于线程(Thread)提出的。但是由于协程是非抢占式的调度,无法实现公平的任务调用。也无法直接利用多核优势。因此,我们不能武断地说协程是比线程更高级的技术。

尽管,在任务调度上,协程是弱于线程的。但是在资源消耗上,协程则是极低的。一个线程的内存在 MB 级别,而协程只需要 KB 级别。而且线程的调度需要内核态与用户的频繁切入切出,资源消耗也不小。

我们把协程的基本特点归纳为:

  • 协程调度机制无法实现公平调度
  • 协程的资源开销是非常低的,一台普通的服务器就可以支持百万协程。

那么,近几年为何协程的概念可以大热。我认为一个特殊的场景使得协程能够广泛的发挥其优势,并且屏蔽掉了劣势 –> 网络编程。与一般的计算机程序相比,网络编程有其独有的特点。

  • 高并发(每秒钟上千数万的单机访问量)
  • Request/Response。程序生命期端(毫秒,秒级)
  • 高IO,低计算(连接数据库,请求API)。

最开始的网络程序其实就是一个线程一个请求设计的(Apache)。后来,随着网络的普及,诞生了C10K问题。Nginx 通过单线程异步 IO 把网络程序的执行流程进行了乱序化,通过 IO 事件机制最大化的保证了CPU的利用率。

至此,现代网络程序的架构已经形成。基于IO事件调度的异步编程。其代表作恐怕就属 NodeJS 了吧。

异步编程的槽点

异步编程为了追求程序的性能,强行的将线性的程序打乱,程序变得非常的混乱与复杂。对程序状态的管理也变得异常困难。写过Nginx C Module的同学应该知道我说的是什么。我们开始吐槽 NodeJS 那恶心的层层Callback。

Golang

在我们疯狂被 NodeJS 的层层回调恶心到的时候,Golang 作为名门之后开始走入我们的视野。并且迅速的在Web后端极速的跑马圈地。其代表者 Docker 以及围绕这 Docker 展开的整个容器生态圈欣欣向荣起来。其最大的卖点 – 协程 开始真正的流行与讨论起来。

我们开始向写PHP一样来写全异步IO的程序。看上去美好极了,仿佛世界就是这样了。

在网络编程中,我们可以理解为 Golang 的协程本质上其实就是对 IO 事件的封装,并且通过语言级的支持让异步的代码看上去像同步执行的一样。

四、Golang 协程的应用

我们知道,协程(coroutine)是Go语言中的轻量级线程实现,由Go运行时(runtime)管理。

在一个函数调用前加上go关键字,这次调用就会在一个新的goroutine中并发执行。当被调用的函数返回时,这个goroutine也自动结束。需要注意的是,如果这个函数有返回值,那么这个返回值会被丢弃。

先看一下下面的程序代码:

func Add(x, y int) {
    z := x + y
    fmt.Println(z)
}

func main() {
    for i:=0; i<10; i++ {
        go Add(i, i)
    }
}

  
执行上面的代码,会发现屏幕什么也没打印出来,程序就退出了。
  
对于上面的例子,main()函数启动了10个goroutine,然后返回,这时程序就退出了,而被启动的执行 Add() 的 goroutine 没来得及执行。我们想要让 main() 函数等待所有 goroutine 退出后再返回,但如何知道 goroutine 都退出了呢?这就引出了多个goroutine之间通信的问题。

在工程上,有两种最常见的并发通信模型:共享内存 和 消息。

下面的例子,使用了锁变量(属于一种共享内存)来同步协程,事实上 Go 语言主要使用消息机制(channel)来作为通信模型

package main

import (
    "fmt"
    "sync"
    "runtime"
)

var counter int = 0

func Count(lock *sync.Mutex) {
    lock.Lock() // 上锁
    counter++
    fmt.Println("counter =", counter)
    lock.Unlock()   // 解锁
}

func main() {
    lock := &sync.Mutex{}

    for i:=0; i<10; i++ {
        go Count(lock)
    }
    for {
        lock.Lock() // 上锁
        c := counter
        lock.Unlock()   // 解锁

        runtime.Gosched() // 出让时间片

        if c >= 10 {
            break
        }
    }
}

channel

消息机制认为每个并发单元是自包含的、独立的个体,并且都有自己的变量,但在不同并发单元间这些变量不共享。每个并发单元的输入和输出只有一种,那就是消息。

channel 是 Go 语言在语言级别提供的 goroutine 间的通信方式,我们可以使用 channel 在多个 goroutine 之间传递消息。channel是进程内的通信方式,因此通过 channel 传递对象的过程和调用函数时的参数传递行为比较一致,比如也可以传递指针等。channel 是类型相关的,一个 channel 只能传递一种类型的值,这个类型需要在声明 channel 时指定。

channel的声明形式为:

var chanName chan ElementType

  
举个例子,声明一个传递int类型的channel:

var ch chan int

  
使用内置函数 make() 定义一个channel:

ch := make(chan int)

  
在channel的用法中,最常见的包括写入和读出:

// 将一个数据value写入至channel,这会导致阻塞,直到有其他goroutine从这个channel中读取数据
ch <- value

// 从channel中读取数据,如果channel之前没有写入数据,也会导致阻塞,直到channel中被写入数据为止
value := <-ch

默认情况下,channel的接收和发送都是阻塞的,除非另一端已准备好。

我们还可以创建一个带缓冲的channel:

c := make(chan int, 1024)

// 从带缓冲的channel中读数据
for i:=range c {
  ...
}

此时,创建一个大小为1024的int类型的channel,即使没有读取方,写入方也可以一直往channel里写入,在缓冲区被填完之前都不会阻塞。

可以关闭不再使用的channel:

close(ch)

应该在生产者的地方关闭channel,如果在消费者的地方关闭,容易引起panic;

现在利用channel来重写上面的例子:

func Count(ch chan int) {
    ch <- 1
    fmt.Println("Counting")
}

func main() {

    chs := make([] chan int, 10)

    for i:=0; i<10; i++ {
        chs[i] = make(chan int)
        go Count(chs[i])
    }

    for _, ch := range(chs) {
        <-ch
    }
}

  
在这个例子中,定义了一个包含10个channel的数组,并把数组中的每个channel分配给10个不同的goroutine。在每个goroutine完成后,向goroutine写入一个数据,在这个channel被读取前,这个操作是阻塞的。在所有的goroutine启动完成后,依次从10个channel中读取数据,在对应的channel写入数据前,这个操作也是阻塞的。这样,就用channel实现了类似锁的功能,并保证了所有goroutine完成后main()才返回。

另外,我们在将一个channel变量传递到一个函数时,可以通过将其指定为单向channel变量,从而限制该函数中可以对此channel的操作。

select

在UNIX中,select()函数用来监控一组描述符,该机制常被用于实现高并发的socket服务器程序。Go语言直接在语言级别支持select关键字,用于处理异步IO问题,大致结构如下:

select {
    case <- chan1:
    // 如果chan1成功读到数据

    case chan2 <- 1:
    // 如果成功向chan2写入数据

    default:
    // 默认分支
}

  
select默认是阻塞的,只有当监听的channel中有发送或接收可以进行时才会运行,当多个channel都准备好的时候,select是随机的选择一个执行的。

Go语言没有对channel提供直接的超时处理机制,但我们可以利用select来间接实现,例如:

timeout := make(chan bool, 1)

go func() {
    time.Sleep(1e9)
    timeout <- true
}()

switch {
    case <- ch:
    // 从ch中读取到数据

    case <- timeout:
    // 没有从ch中读取到数据,但从timeout中读取到了数据
}

这样使用select就可以避免永久等待的问题,因为程序会在timeout中获取到一个数据后继续执行,而无论对ch的读取是否还处于等待状态。

Golang方法和接口使用说明

在编程语言中,方法和函数的概念需要搞清楚。函数指的是一个封装的代码块,我们可以直接调用它,并返回结果。而方法其实也是一种函数,只不过方法需要和某个对象绑定。Golang并没有类的概念,不过仍然有方法和接口这些概念。

方法

方法接收者

方法接收者是一个特殊参数,给函数指定了这个参数之后,函数就成为方法了。这个特性有点像Kotlin和C#中的扩展方法,定义了带有接收者的方法之后,接收者这个类型就好像定义了这个方法一样,我们可以直接在该类型上调用方法。这在功能上,和面向对象的概念是很类似的。

例如下面这样,定义了一个汽车结构,然后定义了一个接受者方法。然后就可以用面向对象的方式来调用这个方法了。

func Method() {
    //方法接收者
    car := Car{id: 1}
    car.beep()
}

type Car struct {
    id int
}

func (car Car) beep() {
    fmt.Printf("Car %v beeps", car.id)
}

接收者方法也有一些限制,这也是它和扩展方法之间的区别。接收者方法的接受者类型,必须和接收者方法定义在同一个包中。所以很多非自定义的类型,以及基本类型都不能当做接收者的类型。当然也可以投机取巧,在自己的包中重新为这些类型取个名字即可。

//把基本类型重新定义一下,就可以当做接收者类型了
type MyString string

func (str MyString) hello() {
    fmt.Println("hello" + str)
}

指针接收者

接收者的类型可以是指针,如果希望在接收者方法中修改接收者的属性,就需要指针类型了。下面的代码对Car结构体添加了两个方法,第一个由于没有指针类型,所以不会修改原始结构体中的值;而第二个方法会修改汽车的id。

func (car Car) beep() {
    fmt.Printf("Car %v beeps", car.id)
}

func (car Car) changeId() {
    car.id += 1
}
func (car *Car) changeRealId() {
    car.id += 1
}

接口

听起来很奇怪,如果Golang没有类型,为什么会有接口的概念?让我们来看看Golang如何解决这些问题。

定义接口

在Golang中,接口就是一组方法签名的集合。下面就定义了一个接口。

type ICar interface {
    beep()
    drive(driver string)
}

实现接口

在Golang中,其实并没有“实现接口”这一说法。在Golang中接口是隐式实现的,也就是说我们不需要implements这些关键字。只要一个类型的接收者方法和接口中定义的方法一致,Golang就认为这个类型实现了该接口。下面是一个简单的例子。

func Interface() {
    car := MyCar{id: 1}
    var icar ICar = car
    icar.beep()
    icar.drive("yitian")
}

type ICar interface {
    beep()
    drive(driver string)
}

type MyCar struct {
    id int
}

func (car MyCar) beep() {
    fmt.Printf("car %v beepsn", car.id)
}
func (car MyCar) drive(driver string) {
    fmt.Printf("%v drives car %vn", driver, car.id)
}

空接口

什么方法都没定义的接口就是空接口。根据Golang的概念,空接口被任何类型隐式实现,所以空接口可以容纳任何类型。

//空接口可以作为任何类型使用
type Everything interface {
}

var e Everything = "123"
fmt.Println(e)

类型细化

定义和实现接口是一个类型泛化的过程,在这个过程中,我们抹消掉了类型特有的部分,让类型公有的部分能够统一利用。不过有时候需要反过来,将一个接口对象转换为原始的具体类,让我们能够获取更具体的行为。

现在来看看在Golang中,这件事情应该怎么做。再次使用上面定义的类型。可以看到和C系语言的括号强转方式不同,在Golang中是.(T)类型的语法。

//特化类型
myCar := icar.(MyCar)
//myCar是MyCar类型变量
myCar.beep()

这个语法还有一个携带一个成功标志的版本 t, ok := i.(T)。当成功标志为真时,表示成功将接口转换为具体类型,否则表示该接口不是具体类型的实例。

如果要进行多次判断,可以利用switch语句。下面是一个例子。

func testType(i interface{}) {
    switch i.(type) {
    case string:
        fmt.Printf("%v is stringn", i)
    case int:
        fmt.Printf("%v is intn", i)
    default:
        fmt.Printf("%v is interface{}n", i)
    }
}

对这个方法调用多次,可以看到针对不同的类型,方法会返回不同结果。

//类型检测
testType("abc")
testType(123)
testType(nil)

Lua的table库函数insert remove concat sort详细介绍

函数列表:

table.insert(table,[ pos,] value) 
table.remove(table[, pos]) 
table.concat(table[, sep[, i[, j]]]) 
table.sort(table[, comp])

1. insert 和 remove 只能用于数组元素的插入和移出, 进行插入和移出时,会将后面的元素对齐起来。

所以在 for 循环中进行 insert 和 remove 的时候要注意插入和移除时是否漏掉了某些项:

        local t = {1,2,3,3,5,3,6} 
        for i,v in ipairs(t) do 
            if v == 3 then 
                table.remove(t,i) 
            end 
        end 
        -- 错误,第四个 3 没有被移除,ipairs 内部会维护一个变量记录遍历的位置,remove 掉第三个数字 3 之后,ipairs 下一个返回的值是 5 而不是 3 

        local t = {1,2,3,3,5,3,6} 
        for i=1, #t do 
            if t[i] == 3 then 
                table.remove(t,i) 
                i = i-1 
            end 
        end 
        -- 错误,i=i-1 这段代码没有用,i 的值始终是从 1 到 #t,for 循环里修改 i 的值不起作用 

        local t = {1,2,3,3,5,3,6} 
        for i=#t, 1, -1 do 
            if t[i] == 3 then 
                table.remove(t,i) 
            end 
        end 
        -- 正确,从后往前遍历 

        local t = {1,2,3,3,5,3,6} 
        local i = 1 
        while t[i] do 
            if t[i] == 3 then 
                table.remove(t,i) 
            else 
                i = i+1 
            end 
        end 
        -- 正确,自己控制 i 的值是否增加 

2. concat 可以将 table 的数组部分拼接成一个字符串,中间用 seq 分隔。

lua 中字符串的存储方式与 C 不一样,lua 中的每个字符串都是单独的一个拷贝,拼接两个字符串会产生一个新的拷贝,如果拼接操作特别多,就会影响性能:

        local beginTime = os.clock() 
        local str = "" 
        for i=1, 30000 do 
            str = str .. i 
        end 
        local endTime = os.clock() 
        print(endTime - beginTime) 
        -- 消耗 0.613 秒,产生了 30000 个字符串拷贝,但只有最后一个是有用的

        local beginTime = os.clock() 
        local t = {} 
        for i=1, 30000 do 
            t[i] = i 
        end 
        local str = table.concat(t, "") 
        local endTime = os.clock() 
        print(endTime - beginTime) 
        -- 消耗 0.024 秒,利用 concat,一次性把字符串拼接出来,只产生了一个字符串拷贝 

3. sort 可以将 table 数组部分的元素进行排序,需要提供 comp 函数,comp(a, b) 如果 a 应该排到 b 前面,则 comp 要返回 true 。

注意,对于 a==b 的情况,一定要返回 false :

        local function comp(a,b) 
            return a <= b 
        end 
        table.sort(t,comp) 
        -- 错误,可能出现异常:attempt to compare number with nil 

        local function comp(a,b) 
            if a == nil or b == nil then 
                return false 
            end 
            return a <= b 
        end 
        table.sort(t,comp) 
        -- 错误,可能出现异常:invalid order function for sorting 
        -- 也可能不报这个异常,但结果是错误的; 
    之所以 a==b 返回true 会引发这些问题,是因为 table.sort 在实现快速排序时没有做边界检测: 
        for (;;) { 
          while (lua_rawgeti(L, 1, ++i), sort_comp(L, -1, -2)) {  // 未检测边界, i 会一直增加 
            if (i>=u) luaL_error(L, "invalid order function for sorting"); 
            lua_pop(L, 1); 
          } 
          while (lua_rawgeti(L, 1, --j), sort_comp(L, -3, -1)) {  // 未检测边界, j 会一直减少 
            if (j<=l) luaL_error(L, "invalid order function for sorting"); 
            lua_pop(L, 1); 
          } 
          if (j<i) { 
            lua_pop(L, 3); 
            break; 
          } 
          set2(L, i, j); 
        } 

看以上代码,如果 a==b 时返回 true 且边界上的几个值是相等的话, sort_comp 就无法阻止 i 继续增长,直到超出边界引发异常 attempt to compare number with nil;即使我们对 a 和 b 进行非空判断,也会因为 i 超过边界而引发异常 invalid order function for sorting

快速排序是什么,lua 如何实现快速排序,可以参考 lua 源码中的描述,这里不多介绍。

使用Mysqldump备份和还原MySQL数据库

登入MySQL数据库

mysql -uroot -pmypassword

root为mysql数据库用户名,mypassword为密码

未分类

我的Mysql未设置密码,所以没有-p,退出Mysql快捷键CTRL+D

备份MySQL数据库

1、备份单个数据库

mysqldump -uroot -pmypassword wp > wpbak.sql

root为mysql数据库用户名,mypassword为密码,wp为数据库名,wpbak.sql为备份成的文件。

2、备份多个数据库

mysqldump -uroot -pmypassword --databases wp stuff > wpbak.sql

root为mysql数据库用户名,mypassword为密码,wp、stuff为数据库名,wpbak.sql为备份成的文件。

3、备份所有数据库

mysqldump -uroot -pmypassword --all-databases > all-databases.sql

root为mysql数据库用户名,mypassword为密码,all-databases.sql为备份成的文件。

还原MySQL数据库

1、还原单个数据库

mysql -uroot -pmypassword [database_name] < dumpfilename.sql

root为mysql数据库用户名,mypassword为密码,[database_name]为数据库名,dumpfilename.sql为还原的数据

2、还原多个数据库

mysql -uroot -pmypassword < all-databases.sql

root为mysql数据库用户名,mypassword为密码,all-databases.sql为还原的数据

mysqldump与innobackupex备份过程知多少(三)

1.3 mysqldump有什么坑吗?

想必大家都知道,mysqldump备份时可以使用–single-transaction + –master-data两个选项执行备份(老实讲,为图方便,本人之前很长一段时间,生产库也是使用mysqldudmp远程备份的),这样备份过程中既可以尽量不锁表,也可以获取到binlog pos位置,备份文件可以用于数据恢复,也可以用于搭建备库。看起来那么美好,然而……其实一不小心你就发现踩到坑了

1.3.1 坑一

使用–single-transaction + –master-data时,myisam表持续不断插入,并用于搭建备库。

首先在A库上把myisam表的数据行数弄到100W以上:

......
root@localhost : luoxiaobo 11:26:42> insert into t_luoxiaobo2(test,datet_time) select test,now() from t_luoxiaobo2;
Query OK, 1572864 rows affected (4.47 sec)
Records: 1572864  Duplicates: 0  Warnings: 0
root@localhost : luoxiaobo 11:26:47> select count(*) from t_luoxiaobo2;
+----------+
| count(*) |
+----------+
|  3145728 |
+----------+
1 row in set (0.00 sec)

A库新开一个ssh会话2,使用如下脚本持续对表t_luoxiaobo2进行插入操作(该表为myisam表),限于篇幅,请点击此处获取。

A库新开一个ssh会话3,清空查询日志:

[root@localhost ~]# echo > /home/mysql/data/mysqldata1/mydata/localhost.log 

现在,A库在ssh会话3中,使用mysqldump备份整个实例:

[root@localhost ~]# mysqldump -h 192.168.2.111 -uadmin -pletsg0 --single-transaction --master-data=2 --triggers --routines --events -A >
 backup_`date +%F_%H_%M_%S`.sql 
mysqldump: [Warning] Using a password on the command line interface can be insecure.
[root@localhost ~]# ls -lh backup_2017-07-03_00_47_50.sql 
-rw-r--r-- 1 root root 112M 7月   3 00:47 backup_2017-07-03_00_47_50.sql

备份完成之后,A库在ssh会话2中,停止持续造数脚本。

A库在ssh会话2中,查看备份文件中的binlog pos:

[root@localhost ~]# head -100 backup_*.sql |grep -i 'change master to'
-- CHANGE MASTER TO MASTER_LOG_FILE='mysql-bin.000005', MASTER_LOG_POS=76657819;

A库在ssh会话3中,查看查询日志,可以发现在UNLOCK TABLES之后,select *…t_luoxiaobo2表之前,还有数据插入到该表中:

2017-07-03T00:47:50.366670+08:00    87364 Query SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ
2017-07-03T00:47:50.366795+08:00    87363 Query insert into t_luoxiaobo2(test,datet_time) values(11377,now())
2017-07-03T00:47:50.366862+08:00    87364 Query START TRANSACTION /*!40100 WITH CONSISTENT SNAPSHOT */
2017-07-03T00:47:50.367023+08:00    87364 Query SHOW VARIABLES LIKE 'gtid_mode'
2017-07-03T00:47:50.372331+08:00    87364 Query SELECT @@GLOBAL.GTID_EXECUTED
2017-07-03T00:47:50.372473+08:00    87364 Query SHOW MASTER STATUS
2017-07-03T00:47:50.372557+08:00    87364 Query UNLOCK TABLES
......
2017-07-03T00:47:50.381256+08:00    87366 Query insert into t_luoxiaobo2(test,datet_time) values(11383,now())
......
2017-07-03T00:47:50.381577+08:00    87365 Query insert into t_luoxiaobo2(test,datet_time) values(11380,now())
2017-07-03T00:47:50.381817+08:00    87360 Init DB   luoxiaobo
2017-07-03T00:47:50.381886+08:00    87360 Query insert into t_luoxiaobo2(test,datet_time) values(11382,now())
......
2017-07-03T00:47:50.391873+08:00    87364 Query show fields from `t_luoxiaobo2`
2017-07-03T00:47:50.392116+08:00    87364 Query show fields from `t_luoxiaobo2`
2017-07-03T00:47:50.392339+08:00    87364 Query SELECT /*!40001 SQL_NO_CACHE */ * FROM `t_luoxiaobo2`

现在,我们将这个备份文件用于B库上搭建备库,并启动复制,可以发现有如下复制报错:

root@localhost : (none) 12:59:12> show slave statusG;
*************************** 1. row ***************************
               Slave_IO_State: Waiting for master to send event
                  Master_Host: 192.168.2.111
                  Master_User: qfsys
                  Master_Port: 3306
                Connect_Retry: 60
              Master_Log_File: mysql-bin.000005
          Read_Master_Log_Pos: 79521301
               Relay_Log_File: mysql-relay-bin.000002
                Relay_Log_Pos: 320
        Relay_Master_Log_File: mysql-bin.000005
             Slave_IO_Running: Yes
            Slave_SQL_Running: No
......
               Last_SQL_Errno: 1062
               Last_SQL_Error: Could not execute Write_rows event on table luoxiaobo.t_luoxiaobo2; Duplicate entry '6465261' for key 'PRIMARY', Error_code: 1062;
 handler error HA_ERR_FOUND_DUPP_KEY; the event's master log mysql-bin.000005, end_log_pos 76658175
......
ERROR: 
No query specified

从上面的结果中可以看到,主键冲突了,也就是说备份的表t_luoxiaobo2中的数据与备份文件中获取的binlog pos点并不一致,咱们现在在B库中,查询一下这个表中大于等于这个冲突主键的数据,从下面的结果中可以看到,备份文件中如果严格按照一致性要求,备份文件中的数据必须和binlog pos点一致,但是现在,备份文件中的数据却比获取的binlog pos点多了5行数据:

root@localhost : (none) 12:59:24> use luoxiaobo
Database changed
root@localhost : luoxiaobo 12:59:44> select id from t_luoxiaobo2 where id>=6465261;
+---------+
| id      |
+---------+
| 6465261 |
| 6465263 |
| 6465265 |
| 6465267 |
| 6465269 |
+---------+
5 rows in set (0.01 sec)

现在,咱们去掉–single-transaction选项,重新执行本小节以上步骤,重新搭建从库,看看是否还有问题(这里限于篇幅,步骤省略,只贴出最后结果):

root@localhost : (none) 01:09:12> change master to master_host='192.168.2.111',master_user='qfsys',master_password='letsg0',master_log_file='mysql-bin.000005',
master_log_pos=83601517;
Query OK, 0 rows affected, 2 warnings (0.02 sec)
Note (Code 1759): Sending passwords in plain text without SSL/TLS is extremely insecure.
Note (Code 1760): Storing MySQL user name or password information in the master info repository is not secure and is therefore not recommended. Please consider 
using the USER and PASSWORD connection options for START SLAVE; see the 'START SLAVE Syntax' in the MySQL Manual for more information.
root@localhost : (none) 01:09:23> start slave;
Query OK, 0 rows affected (0.01 sec)
root@localhost : (none) 01:09:25> show slave statusG;
*************************** 1. row ***************************
               Slave_IO_State: Waiting for master to send event
                  Master_Host: 192.168.2.111
                  Master_User: qfsys
                  Master_Port: 3306
                Connect_Retry: 60
              Master_Log_File: mysql-bin.000005
          Read_Master_Log_Pos: 84697699
               Relay_Log_File: mysql-relay-bin.000002
                Relay_Log_Pos: 1096502
        Relay_Master_Log_File: mysql-bin.000005
             Slave_IO_Running: Yes
            Slave_SQL_Running: Yes
......
          Exec_Master_Log_Pos: 84697699
......

从上面的show slave status输出信息中我们可以看到,去掉了–single-transaction选项之后的备份,用于搭建备库就正常了。另外,我们重新在A库上查看查询日志也可以发现,只搜索到flush语句而没有搜索到unlock tables、set session transaction.. 、start transaction.. 语句,说明备份过程没有开启一致性快照事务,没有修改隔离级别,是全程加全局读锁的,mysqldump备份进程结束退出之后mysql server自动回收锁资源:

[root@localhost ~]# grep -iE 'flush|unlock tables|transaction' /home/mysql/data/mysqldata1/mydata/localhost.log 
2017-07-03T01:07:08.195470+08:00    102945 Query    FLUSH /*!40101 LOCAL */ TABLES
2017-07-03T01:07:08.206607+08:00    102945 Query    FLUSH TABLES WITH READ LOCK

也许你会说,我们数据库环境很规范,没有myisam表,不会有这个问题,OK,赞一个。

1.3.2 坑二

使用–single-transaction + –master-data时,InnoDB表执行online ddl,备份文件用于搭建备库(注意,本小节中的数据库实例与前一小节不同)。

这次我们操作InnoDB表,在A库上先把t_luoxiaobo表的数据也弄到几百万行。

qogir_env@localhost : luoxiaobo 10:03:35> insert into t_luoxiaobo(test,datet_time) select test,now() from t_luoxiaobo;
Query OK, 1048576 rows affected (9.83 sec)
Records: 1048576  Duplicates: 0  Warnings: 0
qogir_env@localhost : luoxiaobo 10:03:46> select count(*) from t_luoxiaobo;
+----------+
| count(*) |
+----------+
|  2097152 |
+----------+
1 row in set (0.39 sec)

A库在ssh会话2中,使用如下脚本持续对表t_luoxiaobo进行DDL操作(该表为InnoDB表),限于篇幅,请点击此处获取。

A库在ssh会话3中,清空查询日志:

[root@localhost ~]# echo > /home/mysql/data/mysqldata1/mydata/localhost.log 

现在,A库在ssh会话3中,使用mysqldump备份整个实例:

[root@localhost ~]# mysqldump -h 192.168.2.111 -uadmin -pletsg0 --single-transaction --master-data=2 --triggers --routines --events -A > 
backup_`date +%F_%H_%M_%S`.sql 
mysqldump: [Warning] Using a password on the command line interface can be insecure.
[root@5f1772e3-0c7a-4537-97f9-9b57cf6a04c2 ~]# ls -lh backup_2017-07-03_12_46_49.sql 
-rw-r--r-- 1 root root 74M Jul  3 12:46 backup_2017-07-03_12_46_49.sql

A库在ssh会话2中,停止DDL添加脚本。

A库在ssh会话2中,查看备份文件中的binlog pos:

[root@5f1772e3-0c7a-4537-97f9-9b57cf6a04c2 ~]# head -100 backup_*.sql |grep -i 'change master to'
-- CHANGE MASTER TO MASTER_LOG_FILE='mysql-bin.000257', MASTER_LOG_POS=62109599;

现在,我们将这个备份文件用于在B库中搭建备库,并启动复制,从下面的结果中可以看到,复制状态正常:

qogir_env@localhost : (none) 01:32:00> show slave statusG;
......
              Master_Log_File: mysql-bin.000257
          Read_Master_Log_Pos: 62110423
               Relay_Log_File: [email protected]
                Relay_Log_Pos: 1144
        Relay_Master_Log_File: mysql-bin.000257
             Slave_IO_Running: Yes
            Slave_SQL_Running: Yes
......
          Exec_Master_Log_Pos: 62110423
......
        Seconds_Behind_Master: 0
......
           Retrieved_Gtid_Set: 799ef59c-4126-11e7-83ce-00163e407cfb:53831090-53831093
            Executed_Gtid_Set: 799ef59c-4126-11e7-83ce-00163e407cfb:1-53831093,
f9b1a9b6-46b7-11e7-9e8b-00163e4fde29:1
                Auto_Position: 0
......

现在我们回到A库上,对表t_luoxiaobo插入一些测试数据:

qogir_env@localhost : luoxiaobo 12:43:30> insert into t_luoxiaobo(test,datet_time) values('test_replication',now());
Query OK, 1 row affected (0.00 sec)
qogir_env@localhost : luoxiaobo 01:36:31> select * from t_luoxiaobo where test='test_replication';
+---------+------------------+---------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+
| id      | test             | datet_time          | test1 | test2 | test3 | test4 | test5 | test6 | test8 | test7 | test9 |
+---------+------------------+---------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+
| 7470943 | test_replication | 2017-07-03 13:36:31 | NULL  | NULL  | NULL  | NULL  | NULL  | NULL  | NULL  | NULL  | NULL  |
+---------+------------------+---------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+
1 row in set (0.96 sec)

在B库上查询复制状态和表t_luoxiaobo中的数据:

qogir_env@localhost : luoxiaobo 01:32:21> show slave statusG;
......
              Master_Log_File: mysql-bin.000257
          Read_Master_Log_Pos: 62110862
               Relay_Log_File: [email protected]
                Relay_Log_Pos: 1583
        Relay_Master_Log_File: mysql-bin.000257
             Slave_IO_Running: Yes
            Slave_SQL_Running: Yes
......
          Exec_Master_Log_Pos: 62110862
......
        Seconds_Behind_Master: 0
......
           Retrieved_Gtid_Set: 799ef59c-4126-11e7-83ce-00163e407cfb:53831090-53831094
            Executed_Gtid_Set: 799ef59c-4126-11e7-83ce-00163e407cfb:1-53831094,
f9b1a9b6-46b7-11e7-9e8b-00163e4fde29:1
......
1 row in set (0.00 sec)
qogir_env@localhost : luoxiaobo 01:38:23> select * from t_luoxiaobo where test='test_replication';
+---------+------------------+---------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+
| id      | test             | datet_time          | test1 | test2 | test3 | test4 | test5 | test6 | test8 | test7 | test9 |
+---------+------------------+---------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+
| 7470943 | test_replication | 2017-07-03 13:36:31 | NULL  | NULL  | NULL  | NULL  | NULL  | NULL  | NULL  | NULL  | NULL  |
+---------+------------------+---------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+
1 row in set (0.05 sec)

到这里,看起来一切正常,对不对?开心吗?先等等,请保持DBA一贯严谨的优良传统,咱们在主库上使用pt-table-checksum工具检查一下:

# 删除了一部分无用信息,只保留了我们之前造数的两张表
[root@5f1772e3-0c7a-4537-97f9-9b57cf6a04c2 ~]# pt-table-checksum --nocheck-replication-filters  --no-check-binlog-format --replicate=xiaoboluo.checksums   
h=localhost,u=admin,p=letsg0,P=3306
            TS ERRORS  DIFFS     ROWS  CHUNKS SKIPPED    TIME TABLE
07-03T13:57:48      0     16  2097153      18       0   7.543 luoxiaobo.t_luoxiaobo
07-03T13:57:57      0      0  2097152      18       0   9.281 luoxiaobo.t_luoxiaobo2
......

从上面的信息中可以看到,表luoxiaobo.t_luoxiaobo的检测DIFFS 列为16,代表主从有数据差异,神马情况?别急,咱们先来分别在AB库查询下这张表的数据行数,从下面的结果可以看到,该表主从数据差异2097152行!!

# A库
qogir_env@localhost : (none) 01:57:03> use luoxiaobo
Database changed
qogir_env@localhost : luoxiaobo 02:09:40> select count(*) from t_luoxiaobo;
+----------+
| count(*) |
+----------+
|  2097153 |
+----------+
1 row in set (0.41 sec)
B库
qogir_env@localhost : (none) 01:55:28> use luoxiaobo
Database changed
qogir_env@localhost : luoxiaobo 02:10:30> select count(*) from t_luoxiaobo;
+----------+
| count(*) |
+----------+
|        1 |
+----------+
1 row in set (0.00 sec)

发生什么了?也许你会说,平时使用mysqldump不都是这样的吗?没毛病啊。

回想一下,从咱们上篇《mysqldump与innobackupex备份过程知多少(二)》中提到的“WITH CONSISTENT SNAPSHOT语句的作用”时的演示过程可以知道,DDL的负载是刻意加上去的,还记得之前演示mysqldump使用savepoint的作用的时候,使用start transaction with consistent snapshot语句显式开启一个事务之后,该事务执行select之前,该表被其他会话执行了DDL之后无法查询数据,我们知道mysqldump备份数据的时候,就是在start transaction with consistent snapshot语句开启的一个一致性快照事务下使用select语句查询数据进行备份的。

为了证实这个问题,下面我们打开查询日志查看一下在start transaction with consistent snapshot语句和select … 之间是否有DDL语句,如下:

.......
2017-07-03T12:46:57.082727+08:00    1649664 Query   SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ
2017-07-03T12:46:57.082889+08:00    1649664 Query   START TRANSACTION /*!40100 WITH CONSISTENT SNAPSHOT */
......
2017-07-03T12:46:57.085821+08:00    1649664 Query   SELECT @@GLOBAL.GTID_EXECUTED
2017-07-03T12:46:57.085954+08:00    1649664 Query   SHOW MASTER STATUS
......
' # 这里可以看到,在START TRANSACTION /*!40100 WITH CONSISTENT SNAPSHOT */语句之后,select 备份表t_luoxiaobo表之前,有一个DDL语句插入进来'
2017-07-03T12:46:57.089833+08:00    1649667 Query   alter table t_luoxiaobo add column test8 varchar(10)
2017-07-03T12:46:57.095153+08:00    1649664 Query   UNLOCK TABLES
......
2017-07-03T12:46:57.098199+08:00    1649664 Init DB luoxiaobo
2017-07-03T12:46:57.098273+08:00    1649664 Query   SHOW CREATE DATABASE IF NOT EXISTS `luoxiaobo`
2017-07-03T12:46:57.098360+08:00    1649664 Query   SAVEPOINT sp
2017-07-03T12:46:57.098435+08:00    1649664 Query   show tables
2017-07-03T12:46:57.098645+08:00    1649664 Query   show table status like 't_luoxiaobo'
2017-07-03T12:46:57.098843+08:00    1649664 Query   SET SQL_QUOTE_SHOW_CREATE=1
2017-07-03T12:46:57.098927+08:00    1649664 Query   SET SESSION character_set_results = 'binary'
2017-07-03T12:46:57.099013+08:00    1649664 Query   show create table `t_luoxiaobo`
2017-07-03T12:46:57.105056+08:00    1649664 Query   SET SESSION character_set_results = 'utf8'
2017-07-03T12:46:57.105165+08:00    1649664 Query   show fields from `t_luoxiaobo`
2017-07-03T12:46:57.105538+08:00    1649664 Query   show fields from `t_luoxiaobo`
'# 这里原本应该是一句:SELECT /*!40001 SQL_NO_CACHE */ * FROM `t_luoxiaobo`,但是却没有,我们可以推理一下,因为select的时候报了表定'
'# 义已经发生变化的错误,所以这句select并没有被记录到查询日志中来'
2017-07-03T12:46:57.105857+08:00    1649664 Query   SET SESSION character_set_results = 'binary'
2017-07-03T12:46:57.105929+08:00    1649664 Query   use `luoxiaobo`
2017-07-03T12:46:57.106021+08:00    1649664 Query   select @@collation_database
2017-07-03T12:46:57.106116+08:00    1649664 Query   SHOW TRIGGERS LIKE 't_luoxiaobo'
2017-07-03T12:46:57.106394+08:00    1649664 Query   SET SESSION character_set_results = 'utf8'
2017-07-03T12:46:57.106466+08:00    1649664 Query   ROLLBACK TO SAVEPOINT sp
2017-07-03T12:46:57.106586+08:00    1649664 Query   show table status like 't_luoxiaobo2'
......
2017-07-03T12:46:57.107063+08:00    1649664 Query   show create table `t_luoxiaobo2`
2017-07-03T12:46:57.107151+08:00    1649664 Query   SET SESSION character_set_results = 'utf8'
2017-07-03T12:46:57.107233+08:00    1649664 Query   show fields from `t_luoxiaobo2`
2017-07-03T12:46:57.107511+08:00    1649664 Query   show fields from `t_luoxiaobo2`
2017-07-03T12:46:57.107807+08:00    1649664 Query   SELECT /*!40001 SQL_NO_CACHE */ * FROM `t_luoxiaobo2`
......

现在,我们打开备份文件,找到表t_luoxiaob的备份语句位置,可以看到并没有生成INSERT语句:

DROP TABLE IF EXISTS `t_luoxiaobo`;
/*!40101 SET @saved_cs_client     = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `t_luoxiaobo` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `test` varchar(50) COLLATE utf8_bin DEFAULT NULL,
  `datet_time` datetime DEFAULT NULL,
  `test1` varchar(10) COLLATE utf8_bin DEFAULT NULL,
  `test2` varchar(10) COLLATE utf8_bin DEFAULT NULL,
  `test3` varchar(10) COLLATE utf8_bin DEFAULT NULL,
  `test4` varchar(10) COLLATE utf8_bin DEFAULT NULL,
  `test5` varchar(10) COLLATE utf8_bin DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=7470943 DEFAULT CHARSET=utf8 COLLATE=utf8_bin;
/*!40101 SET character_set_client = @saved_cs_client */;
'# 正常情况下,在这个位置,应该出现LOCK TABLES `t_luoxiaobo` WRITE; + /*!40000 ALTER TABLE `t_luoxiaobo2` DISABLE KEYS */; + INSERT INTO语句的,然而,现在这里却是空的'
--
-- Table structure for table `t_luoxiaobo2`
--
DROP TABLE IF EXISTS `t_luoxiaobo2`;
......
LOCK TABLES `t_luoxiaobo2` WRITE;
/*!40000 ALTER TABLE `t_luoxiaobo2` DISABLE KEYS */;
INSERT INTO `t_luoxiaobo2` VALUES (1,'1','2017-07-03 09:22:16'),(4,'2','2017-07-03 09:22:19'),(7,'3','2017-07-03 09:22:21'),
......

到这里,是不是突然心弦一紧呢?so..如果你决定继续使用mysqldump,那么以后搭建好备库之后,一定要记得校验一下主备数据一致性!!

1.3.3 有办法改善这这些问题吗?

在寻找解决办法之前,咱们先来看看mysqldump的备份选项–single-transaction和–master-data[=value]的作用和使用限制。

–single-transaction

此选项将事务隔离模式设置为REPEATABLE READ,并在备份数据之前向server发送START TRANSACTION SQL语句以显示开启一个事务快照。仅适用于InnoDB这样的事务表,由于是在事务快照内进行备份,这样可以使得备份的数据与获取事务快照时的数据是一致的,而且不会阻塞任何应用程序对server的访问。

在进行单事务备份时,为确保有效的备份文件(正确的表内容和二进制日志位置),不能有其他连接应使用语句:ALTER TABLE,CREATE TABLE,DROP TABLE,RENAME TABLE,TRUNCATE等DDL语句。这会导致一致状态被破坏,可能导致mysqldump执行SELECT检索表数据时查询到不正确的内容或备份失败 。

注意:该选项仅适用于事务引擎表,对于MyISAM或MEMORY表由于不支持事务,所以备份过程中这些引擎表的数据仍可能发生更改。

–master-data[=value]

使用此选项备份时会在备份文件中生成change master to语句,使用的binlog pos是使用的备份server自己的binlog pos,可使用备份文件用于将另一台服务器(恢复这个备份文件的服务器)设置为备份server的从库。

与–dump-slave选项类似,如果选项值为2,则CHANGE MASTER TO语句将作为SQL注释写入备份文件,因此仅供参考;当备份文件被重新加载时,这个注释不起作用。如果选项值为1,则该语句不会注释,并在重新加载备份文件时会生效(被执行)。如果未指定选项值,则默认值为1。

指定此选项的用户需要RELOAD权限,并且server必须启用二进制日志,因为这个位置是使用show master status获取的(如果没有开启log_bin参数,则show master status输出信息为空),而不是使用show slave status获取的。

–master-data选项自动关闭–lock-tables选项。同时还会打开–lock-all-tables,除非指定了–single-transaction选项,在指定了–single-transaction选项之后,只有在备份开始时间内才加全局读取锁。

so,–single-transaction选项中明确说明了如果使用了该选项,那么在备份期间如果发生DDL,则可能导致备份数据一致性被破坏,select检索不到正确的内容。另外,该选项仅仅只适用于事务引擎表,不适用于非事务引擎。作为DBA,很多时候是非常无奈的,虽然有各种规范,但是保不齐就是有漏网之鱼,这个时候,生活还得继续,工作还得做好, 那么,有什么办法可以缓解这个问题吗?有的:

  • 就如同上文中演示步骤中那样,去掉–single-transaction选项进行备份,此时单独使用–master-data选项时会自动开启–lock-all-tables,备份过程中整个实例全程锁表,不会发生备份数据与获取的binlog pos点不一致的问题,这样,用该备份来搭建备库时就不会出现数据冲突。但是问题显而易见,备份期间数据库不可用,如果采用这种方法,至少需要在业务低峰期进行备份
  • 使用innobackupex备份工具

mysqldump与innobackupex备份过程知多少(二)

1.2.3 使用WITH CONSISTENT SNAPSHOT子句的作用

START TRANSACTION语句使用WITH CONSISTENT SNAPSHOT子句时,会为事务启动一致性读(该子句仅适用于InnoDB)。其行为与执行START TRANSACTION语句之后+一个SELECT语句效果相同(会获取一个事务号,在read view中占个坑,但是不会请求任何锁)。WITH CONSISTENT SNAPSHOT子句不会自动修改当前的事务隔离级别,由于WITH CONSISTENT SNAPSHOT子句要求必须RR隔离级别下才会自动启用,因此只有当前隔离级别为RR时才会启用一致性快照,非RR隔离级别下,会忽略WITH CONSISTENT SNAPSHOT子句。从MySQL 5.7.2起,当WITH CONSISTENT SNAPSHOT子句被忽略时,会产生一个警告(类似上一篇mysqldump与innobackupex备份过程你知多少(一)提到的警告信息)。

为了使得更清晰地了解mysqldump在备份过程中使用WITH CONSISTENT SNAPSHOT子句的作用,下面咱们来演示一下带与不带WITH CONSISTENT SNAPSHOT子句会发生什么?

  • 开启两个会话,操作同一张表

未分类

未分类

未分类

未分类

未分类

未分类

未分类

从上面的表格对比结果中可以看到:

  • WITH CONSISTENT SNAPSHOT子句的作用就相当于START TRANSACTION+ SELECT语句,目地是为了在开启事务的那一刻往mvcc的read view中立即加入这个事务,就好像read view在事务一开始就被固定了一样,使得后续其他事务的DML不会影响到该事务的查询结果,这就是所说的一致性读

  • 如果不使用WITH CONSISTENT SNAPSHOT子句,在使用START TRANSACTION语句显式开启一个事务之后,在执行SELECT语句之前,这段时间内如果有别的事务发起了DML操作,就会导致该事务查询该表的时候读取的数据与事务开始时间点不一致。

1.2.4 使用savepoint来设置回滚点的作用

大家都知道,设置SAVEPOINT是为了回滚在设置这个点时候发生变更的数据,但是mysqldump备份只是使用select语句做查询,为什么要使用savepoint呢?需要回滚什么呢?请看下文分析:

  • SAVEPOINT ‘identifier’语句,为事务设置一个命名的事务保存点(回滚点),该字符串为事务保存点的标识符。

  • ROLLBACK TO SAVEPOINT语句的作用是将事务回滚到指定的保存点的位置,而不终止事务。当前事务在回滚点之后的修改的行数据将被撤销(注:InnoDB不会释放这些发生修改且被撤销行的行锁,注意是修改,不是新插入,这些发生修改的数据行行锁被存储在内存中),对于设置了保存点之后,新插入的行数据也会被撤销(注:这些锁信息被存储在行数据中的事务ID上,这些行锁不会单独存储在内存中,在这种情况下,这些新插入的行数据在被回滚之后,对应的行锁将被释放)。另外,回滚到某个保存点之后,比这个保存点在时间上更晚设置的保存点将被删除。

  • ROLLBACK TO SAVEPOINT语句还有一个作用,可以释放在设置保存点之后事务持有的MDL锁,这点便是mysqldump需要使用保存点的关键点。

为了更清晰地了解mysqldump在备份过程中使用SAVEPOINT sp + ROLLBACK TO SAVEPOINT sp语句的作用,下面使用两个会话演示一下使用与不使用保存点会发生什么?

未分类

未分类

未分类

未分类

未分类

未分类

从上面的对比结果中可以得知:

  • mysqldump使用savepoint的作用就是,当一个显式开启的事务回滚到保存点时,除了回滚数据变更之外,还会释放保存点之后select语句获取的MDL锁,使得其他会话的DDL语句可以正常执行。对于mysqldump来说,select 语句执行完成之后就代表着该表的数据已经备份完成,无需再继续持有MDL锁,使用savepoint就实现了在select 执行完成之后释放MDL锁的目的(注:在事务内,执行select *语句虽然不会有数据行锁,但是会持有表的MDL锁)。

  • with consistent snapshot子句对应mysqldump实现一致性备份来说至关重要,不仅仅是数据的一致性,使用该子句时,表定义也保持事务开启的那一刻,所以,从上面的对比结果中可以看到,使用了with consistent snapshot子句开启一个一致性快照事务之后,如果一旦表结构定义发生改变,事务将无法重复查询表。

  • 从上面的演示过程中,我们也可以看到,使用 with consistent snapshot子句显式开启一个事务之后,如果该事务没有对任何表做任何操作时,此时是没有获得任何锁的,所以,如果在该事务对某表执行操作之前其他事务对该表执行了DDL操作之后,将导致该事务无法再对表执行查询,会报表结构发生变化的错误;当然,如果显式开启事务后立即对某表执行查询,那么其他会话的DDL是会发生阻塞的;当在该事务使用savepoint实现方式释放表的MDL锁之后,其他会话允许执行DDL,但是执行了DDL语句之后,该事务就无法再对该表执行查询。当然,如果不使用 with consistent snapshot子句,则其他会话执行的DDL对表定义的变更不会影响到该事务重复对表执行查询。

mysqldump与innobackupex备份过程知多少(一)

想必搞数据库的都知道:

  • mysqldump优点:mysqldump的优点就是逻辑备份,把数据生成SQL形式保存,在单库,单表数据迁移,备份恢复等场景方便,SQL形式的备份文件通用,也方便在不同数据库之间移植。对于InnoDB表可以在线备份。

  • mysqldump缺点:mysqldump是单线程,数据量大的时候,备份时间长,甚至有可能在备份过程中非事务表长期锁表对业务造成影响(SQL形式的备份恢复时间也比较长)。mysqldump备份时会查询所有的数据,这可能会把内存中的热点数据刷掉

  • innobackupex优点:物理备份可以绕过MySQL Server层,加上本身就是文件系统级别的备份,备份速度块,恢复速度快,可以在线备份,支持并发备份,支持加密传输,支持备份限速

  • innobackupex缺点:要提取部分库表数据比较麻烦,不能按照基于时间点来恢复数据,并且不能远程备份,只能本地备份,增量备份的恢复也比较麻烦。如果使用innobackupex的全备+binlog增量备份就可以解决基于时间点恢复的问题。

要查看备份过程中这俩备份工具都对数据库做了什么操作,想必大家都知道:可以打开general_log来查。那么问题来了,general_log输出的信息都代表什么?如果不这样做会怎样?这两个备份工具会不会有什么平时被忽略的坑?请看下文分析,也许……你会发现原来之前对这俩备份工具好像也不是那么了解!

环境信息

  • 服务器配置:

CPU:4 vcpus
内存:4G
磁盘:250G SAS
网卡:Speed: 1000Mb/s

  • 操作系统:CentOS release 6.5 (Final)

  • 数据库版本:MySQL 5.7.17

  • xtrabackup版本:2.4.4

  • 主从IP(文中一些演示步骤需要用到主备复制架构):

主库:192.168.2.111(以下称为A库)

从库:192.168.2.121(以下称为B库)

  • 数据库关键配置参数

主库:双一,log_slave_updates,log-bin,binlog_rows_query_log_events=ON,server-id=3306111,gtid_mode=ON,enforce_gtid_consistency=ON,auto_increment_increment=2,auto_increment_offset=1

备库:双一,log_slave_updates,log-bin,binlog_rows_query_log_events=ON,server-id=3306121,gtid_mode=ON,enforce_gtid_consistency=ON,auto_increment_increment=2,auto_increment_offset=2

  • 测试库表创建(这里在同一个库下创建两个表,一个表为InnoDB引擎,一个为MyISAM引擎)
root@localhost : (none) 04:21:27> create database luoxiaobo;
Query OK, 1 row affected (0.01 sec)


root@localhost : (none) 04:21:45> use luoxiaobo
Database changed
root@localhost : luoxiaobo 04:21:55> create table t_luoxiaobo(id int unsigned not null primary key auto_increment,test varchar(50),datet_time datetime)
 engine=innodb;
Query OK, 0 rows affected (0.05 sec)


root@localhost : luoxiaobo 04:23:00> insert into t_luoxiaobo(test,datet_time) values('1',now());
Query OK, 1 row affected (0.00 sec)


root@localhost : luoxiaobo 04:23:32> insert into t_luoxiaobo(test,datet_time) values('2',now());
Query OK, 1 row affected (0.01 sec)


root@localhost : luoxiaobo 04:23:36> insert into t_luoxiaobo(test,datet_time) values('3',now());
Query OK, 1 row affected (0.00 sec)


root@localhost : luoxiaobo 04:23:38> insert into t_luoxiaobo(test,datet_time) values('4',now());
Query OK, 1 row affected (0.00 sec)


root@localhost : luoxiaobo 04:23:41> select * from t_luoxiaobo;
+----+------+---------------------+
| id | test | datet_time          |
+----+------+---------------------+
|  1 | 1    | 2017-07-01 16:23:32 |
|  3 | 2    | 2017-07-01 16:23:36 |
|  5 | 3    | 2017-07-01 16:23:38 |
|  7 | 4    | 2017-07-01 16:23:41 |
+----+------+---------------------+
4 rows in set (0.00 sec)


root@localhost : luoxiaobo 04:24:51> create table t_luoxiaobo2(id int unsigned not null primary key auto_increment,test varchar(50),datet_time datetime)
 engine=myisam;
Query OK, 0 rows affected (0.04 sec)


root@localhost : luoxiaobo 05:38:19> insert into t_luoxiaobo2(test,datet_time) values('1',now());
Query OK, 1 row affected (0.01 sec)


root@localhost : luoxiaobo 05:38:29> insert into t_luoxiaobo2(test,datet_time) values('2',now());
Query OK, 1 row affected (0.00 sec)


root@localhost : luoxiaobo 05:38:32> insert into t_luoxiaobo2(test,datet_time) values('3',now());
Query OK, 1 row affected (0.01 sec)


root@localhost : luoxiaobo 05:38:35> insert into t_luoxiaobo2(test,datet_time) values('4',now());
Query OK, 1 row affected (0.00 sec)

root@localhost : luoxiaobo 05:38:37> select * from t_luoxiaobo2;
+----+------+---------------------+
| id | test | datet_time          |
+----+------+---------------------+
|  1 | 1    | 2017-07-01 17:38:29 |
|  3 | 2    | 2017-07-01 17:38:32 |
|  5 | 3    | 2017-07-01 17:38:35 |
|  7 | 4    | 2017-07-01 17:38:37 |
+----+------+---------------------+
4 rows in set (0.00 sec)

1. 先看mysqldump

1.1 mysqldump备份过程解读

通常,使用mysqldump备份期间,为了使得数据库中加锁时间尽量短,会使用–single-transaction选项来开启一个一致性快照事务,为了使得备份期间能够获得一个与数据一致的binlog pos点,会使用–master-data选项,现在登录A库主机,使用这俩选项执行备份演示。

先在数据库中打开general_log:

root@localhost : luoxiaobo 04:23:50> show variables like 'general_log';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| general_log   | OFF   |
+---------------+-------+
1 row in set (0.00 sec)


root@localhost : luoxiaobo 04:24:41> set global general_log=1;
Query OK, 0 rows affected (0.03 sec)

root@localhost : luoxiaobo 04:24:49> show variables like 'general_log';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| general_log   | ON    |
+---------------+-------+
1 row in set (0.01 sec)

使用mysqldump备份(使用strace捕获执行过程中的调用栈),这里紧以备份测试库luoxiaobo为例进行演示:

[root@localhost ~]# strace mysqldump -h 192.168.2.111 -uadmin -pletsg0 --single-transaction --master-data=2 --triggers --routines --events luoxiaobo > 
backup_`date +%F_%H_%M_%S`.sql 2> strace_mysqldump.txt 

备份完成之后,查看general_log中的内容(去掉了一些无用信息):

* 留意unlock tables语句的位置,是在show master status语句获取了binlog pos之后立即执行
[root@localhost ~]# cat /home/mysql/data/mysqldata1/mydata/localhost.log 


......
' #修改session级别的sql_mode为空,避免可能有些sql_mode值对备份产生影响'

2017-07-01T17:42:17.779564+08:00        6 Query /*!40100 SET @@SQL_MODE='' */  2017-07-01T17:42:17.779695+08:00        6 Query /*!40103 SET TIME_ZONE='+00:00' */
' #强制刷新表缓存到磁盘并关闭表(但已经加表锁的表会阻塞该语句)'

2017-07-01T17:42:17.779889+08:00        6 Query FLUSH /*!40101 LOCAL */ TABLES 
' # 对整个实例加全局读锁,如果存在表锁将阻塞加全局读锁语句'

2017-07-01T17:42:17.780047+08:00        6 Query FLUSH TABLES WITH READ LOCK
' #在session级别修改隔离级别为RR'

2017-07-01T17:42:17.780201+08:00        6 Query SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ
'# 开启一个一致性快照事务,必须在隔离级别RR下才能开启一个快照事务'

2017-07-01T17:42:17.780326+08:00        6 Query START TRANSACTION /*!40100 WITH CONSISTENT SNAPSHOT */ 
'#查看是否开启GTID'

2017-07-01T17:42:17.780452+08:00        6 Query SHOW VARIABLES LIKE 'gtid_mode' 
'#如果开启GTID则查看当前的事务GTID集合'

2017-07-01T17:42:17.781867+08:00        6 Query SELECT @@GLOBAL.GTID_EXECUTED 
'#查看当前数据的binlog pos'

2017-07-01T17:42:17.781999+08:00        6 Query SHOW MASTER STATUS  
'#释放全局读锁,留意解锁的位置,下文会专门提到这个'

2017-07-01T17:42:17.782113+08:00        6 Query UNLOCK TABLES 
......
2017-07-01T17:42:17.786315+08:00        6 Init DB   luoxiaobo
'#在一个数据库开始备份之前,设置一个保存点(回滚点)'

2017-07-01T17:42:17.786428+08:00        6 Query SAVEPOINT sp  
'#查看库下有哪些表'

2017-07-01T17:42:17.786539+08:00        6 Query show tables  
' #查看这个表的状态'

2017-07-01T17:42:17.786710+08:00        6 Query show table status like 't_luoxiaobo' 
'# 给每个表的每个字段加个反引号'

2017-07-01T17:42:17.786908+08:00        6 Query SET SQL_QUOTE_SHOW_CREATE=1 
'#表结构的备份都是binary格式,所以要先改这个'

2017-07-01T17:42:17.787023+08:00        6 Query SET SESSION character_set_results = 'binary' 
' #查看这个表的定义语句'

2017-07-01T17:42:17.787137+08:00        6 Query show create table `t_luoxiaobo` 
'# 修改session的数据结果返回字符集,备份数据需要使用数据原本的字符集,这里是utf8'

2017-07-01T17:42:17.787329+08:00        6 Query SET SESSION character_set_results = 'utf8' 
' #查看这个表的字段信息'

2017-07-01T17:42:17.787450+08:00        6 Query show fields from `t_luoxiaobo` 
2017-07-01T17:42:17.787715+08:00        6 Query show fields from `t_luoxiaobo`
' #查询表中的数据,结合show fields from `t_luoxiaobo`的字段信息生成insert into语句'

2017-07-01T17:42:17.787967+08:00        6 Query SELECT /*!40001 SQL_NO_CACHE */ * FROM `t_luoxiaobo` 
2017-07-01T17:42:17.788285+08:00        6 Query SET SESSION character_set_results = 'binary' 
2017-07-01T17:42:17.788411+08:00        6 Query use `luoxiaobo`
2017-07-01T17:42:17.788535+08:00        6 Query select @@collation_database
'#查看是否有这个表的触发器'

2017-07-01T17:42:17.788668+08:00        6 Query SHOW TRIGGERS LIKE 't_luoxiaobo'  
2017-07-01T17:42:17.788926+08:00        6 Query SET SESSION character_set_results = 'utf8'
' #t_luoxiaobob表备份结束,回滚到保存点sp,以释放select *...语句产生的MDL锁,如果不回滚到sp,后续整个备份过程中无法对该表执行DDL操作'

2017-07-01T17:42:17.789043+08:00        6 Query ROLLBACK TO SAVEPOINT sp  
2017-07-01T17:42:17.789191+08:00        6 Query show table status like 't_luoxiaobo2'
2017-07-01T17:42:17.789399+08:00        6 Query SET SQL_QUOTE_SHOW_CREATE=1
2017-07-01T17:42:17.789510+08:00        6 Query SET SESSION character_set_results = 'binary'
2017-07-01T17:42:17.789625+08:00        6 Query show create table `t_luoxiaobo2`
2017-07-01T17:42:17.789753+08:00        6 Query SET SESSION character_set_results = 'utf8'
2017-07-01T17:42:17.789871+08:00        6 Query show fields from `t_luoxiaobo2`
2017-07-01T17:42:17.790123+08:00        6 Query show fields from `t_luoxiaobo2`
'#备份表t_luoxiaobo2'

2017-07-01T17:42:17.790486+08:00        6 Query SELECT /*!40001 SQL_NO_CACHE */ * FROM `t_luoxiaobo2`  
2017-07-01T17:42:17.790689+08:00        6 Query SET SESSION character_set_results = 'binary'
2017-07-01T17:42:17.790806+08:00        6 Query use `luoxiaobo`
2017-07-01T17:42:17.790923+08:00        6 Query select @@collation_database
2017-07-01T17:42:17.791053+08:00        6 Query SHOW TRIGGERS LIKE 't_luoxiaobo2'
2017-07-01T17:42:17.791378+08:00        6 Query SET SESSION character_set_results = 'utf8'
'#备份t_luoxiaobo2表过程与t__luoxiaobo表完全一样'

2017-07-01T17:42:17.791497+08:00        6 Query ROLLBACK TO SAVEPOINT sp  
'#整个luoxiaobo库备份完成之后,释放该保存点'

2017-07-01T17:42:17.791606+08:00        6 Query RELEASE SAVEPOINT sp  
' #查看是否有相关的events'

2017-07-01T17:42:17.791717+08:00        6 Query show events 
2017-07-01T17:42:17.792065+08:00        6 Query use `luoxiaobo`
2017-07-01T17:42:17.792323+08:00        6 Query select @@collation_database
2017-07-01T17:42:17.792489+08:00        6 Query SET SESSION character_set_results = 'binary'
'#查看luoxiaobo库是否有存储函数'

2017-07-01T17:42:17.792617+08:00        6 Query SHOW FUNCTION STATUS WHERE Db = 'luoxiaobo' 
' #查看luoxiaobo库是否有存储过程'

2017-07-01T17:42:17.793967+08:00        6 Query SHOW PROCEDURE STATUS WHERE Db = 'luoxiaobo'
2017-07-01T17:42:17.794952+08:00        6 Query SET SESSION character_set_results = 'utf8'
'#备份结束,退出连接'

2017-07-01T17:42:17.805746+08:00        6 Quit    

查看strace抓取的调用栈信息,限于篇幅,详见为知笔记链接:

http://5d096a11.wiz03.com/share/s/1t2mEh0a-kl_2c2NZ33kSiac3oxBB40tGQNY2L6Z_M2LtLbG

上面的strace信息是不是看起来和general_log中的信息很像啊?因为general_log中记录的就是mysqldump发送过去的SQL语句:

从上面general_log和strace信息对比我们可以知道,strace信息代表了mysqldump进程对数据库进程发送了哪些请求信息,general_log代表了数据库中所有的客户端SQL请求操作记录,这就是大家熟知的mysqldump备份过程中的关键步骤,那么问题来了,mysqldump备份过程中为什么需要这些 步骤?不这么做会怎样?下面对这些步骤逐一使用演示步骤进行详细解释。

1.2 mysqldump备份过程中的关键步骤

1.2.1 FLUSH TABLES和FLUSH TABLES WITH READ LOCK的区别

FLUSH TABLES

强制关闭所有正在使用的表,并刷新查询缓存,从查询缓存中删除所有查询缓存结果,类似RESET QUERY CACHE语句的行为

在MySQL 5.7官方文档描述中,当有表正处于LOCK TABLES … READ语句加锁状态时,不允许使用FLUSH TABLES语句(另外一个会话执行FLUSH TABLES会被阻塞),如果已经使用LOCK TABLES … READ语句对某表加读锁的情况下要对另外的表执行刷新,可以在另外一个会话中使用FLUSH TABLES tbl_name … WITH READ LOCK语句(稍后会讲到)

注意:

  • 如果一个会话中使用LOCK TABLES语句对某表加了表锁,在该表锁未释放前,那么另外一个会话如果执行FLUSH TABLES语句会被阻塞
  • 如果一个会话正在执行DDL语句,那么另外一个会话如果执行FLUSH TABLES 语句会被阻塞
  • 如果一个会话正在执行DML大事务(DML语句正在执行,数据正在发生修改,而不是使用lock in share mode和for update语句来显式加锁),那么另外一个会话如果执行FLUSH TABLES语句会被阻塞

FLUSH TABLES WITH READ LOCK

关闭所有打开的表,并使用全局读锁锁定整个实例下的所有表。此时,你可以方便地使用支持快照的文件系统进行快照备份,备份完成之后,使用UNLOCK TABLES语句释放锁。

FLUSH TABLES WITH READ LOCK语句获取的是一个全局读锁,而不是表锁,因此表现行为不会像LOCK TABLES和UNLOCK TABLES语句,LOCK TABLES和UNLOCK TABLES语句在与事务混搭时,会出现一些相互影响的情况,如下:

  • 如果有表使用了LOCK TABLES语句加锁,那么开启一个事务会造成该表的表锁被释放(注意是任何表的表锁,只要存在表锁都会被释放,另外,必须是同一个会话中操作才会造成这个现象),就类似执行了UNLOCK TABLES语句一样,但使用FLUSH TABLES WITH READ LOCK语句加全局读锁,开启一个事务不会造成全局读锁被释放

  • 如果你开启了一个事务,然后在事务内使用LOCK TABLES语句加锁和FLUSH TABLES WITH READ LOCK语句加全局读锁(注意,是对任何表加表锁,只要使用了LOCK TABLES),会造成该事务隐式提交

  • 如果你开启了一个事务,然后在事务内使用UNLOCK TABLES语句,无效

  • 官方文档中还有一句:”如果有表使用LOCK TABLES语句加表锁,在使用UNLOCK TABLES语句解锁时会造成该表的所有事务隐式提交”,个人认为这是理论上的说法,或者说本人能力有限,暂未想到可能会造成这种情况的原因,因为实际上使用LOCK TABLES语句语句时,开启一个事务会造成自动解锁(前面已经提到过),而如果在事务内使用LOCK TABLES语句会造成事务隐式提交(前面已经提到过),所以实际上不可能出现在事务内使用UNLOCK TABLES语句解锁LOCK TABLES语句的情况,而如果是使用FLUSH TABLES WITH READ LOCK语句,如果执行该语句之前存在LOCK TABLES加的表锁,则FLUSH TABLES WITH READ LOCK语句发生阻塞,如果是已经执行FLUSH TABLES WITH READ LOCK语句,LOCK TABLES语句发生阻塞,不会再有任何的表锁和互斥锁能够被获取到(新的非select和show的请求都会被阻塞)。所以不可能出现UNLOCK TABLES语句解锁时造成隐式提交

注:

  • FLUSH TABLES WITH READ LOCK语句不会阻塞日志表的写入,例如:查询日志,慢查询日志等

  • FLUSH TABLES WITH READ LOCK语句与XA协议不兼容

  • 如果一个会话中使用LOCK TABLES语句对某表加了表锁,在该表锁未释放前,那么另外一个会话如果执行FLUSH TABLES WITH READ LOCK语句会被阻塞,而如果数据库中lock_wait_timeout参数设置时间太短,mysqldump将会因为执行FLUSH TABLES WITH READ LOCK语句获取全局读锁超时而导致备份失败退出

  • 如果一个会话正在执行DDL语句,那么另外一个会话如果执行FLUSH TABLES WITH READ LOCK语句会被阻塞,如果数据库中lock_wait_timeout参数设置时间太短,mysqldump将会因为执行FLUSH TABLES WITH READ LOCK语句获取全局读锁超时而导致备份失败退出

  • 如果一个会话正在执行DML大事务(DML语句正在执行,数据正在发生修改,而不是使用lock in share mode和for update语句来显式加锁),那么另外一个会话如果执行FLUSH TABLES WITH READ LOCK语句会被阻塞,如果数据库中lock_wait_timeout参数设置时间太短,mysqldump将会因为执行FLUSH TABLES WITH READ LOCK语句获取全局读锁超时而导致备份失败退出

FLUSH TABLES tbl_name [,tbl_name] … WITH READ LOCK

刷新表并获取指定表的读锁。该语句首先获取表的独占MDL锁,所以需要等待该表的所有事务提交完成。然后刷新该表的表缓存,重新打开表,获取表读锁(类似LOCK TABLES … READ),并将MDL锁从独占级别降级为共享。在该语句获取表读锁、降级MDL锁之后,其他会话可以读取该表,但不能修改表数据及其表结构。

执行该语句需要RELOAD和LOCK TABLES权限。

该语句仅适用于基表(持久表),不适用于临时表,会自动忽略,另外在对视图使用该语句使会报错。

与LOCK TABLES语句类似,在使用该语句对某表加锁之后,再同一个会话中开启一个事务时,会被自动解锁

MySQL5.7官方文档描述说:这种新的变体语法能够使得只针对某一个表加读锁的同时还能够同时刷新这个表,这解决了某表使用LOCK TABLES … READ语句加读锁时,需要刷新表不能使用FLUSH TABLES语句的问题,此时可以使用FLUSH TABLES tbl_name [,tbl_name] … WITH READ LOCK语句代替,但是,官方描述不太清晰,实测在同一个会话中使用LOCK TABLES … READ语句加读锁时,不允许执行该语句(无论操作表是否是同一张表),会报错:ERROR 1192 (HY000): Can’t execute the given command because you have active locked tables or an active transaction,但是如果在不同的会话中,那么,如果表不相同,允许执行,表相同,则FLUSH TABLES tbl_name [,tbl_name] … WITH READ LOCK语句发生等待

该语句同一个会话重复执行时,无论是否同一个表,都会报错:

ERROR 1192 (HY000): Can't execute the given command because you have active locked tables or an active transactio

如果是不同会话不同表则允许执行,但是表相同则发生等待

1.2.2 修改隔离级别的作用

为什么要执行SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ语句呢?因为后续需要使用START TRANSACTION /!40100 WITH CONSISTENT SNAPSHOT语句开启一个一致性事务快照,根据事务一致性读要求,一致性事务快照只支持RR隔离级别,在其他隔离级别下执行语句START TRANSACTION /!40100 WITH CONSISTENT SNAPSHOT会报如下警告信息:

'# RU、RC、串行隔离级别报一样的警告,告诉你WITH CONSISTENT SNAPSHOT子句被忽略,该子句只支持RR隔离级别'
root@localhost : (none) 02:54:15> show variables like '%isolation%';

+---------------+----------------+
| Variable_name | Value          |
+---------------+----------------+
| tx_isolation  | READ-COMMITTED |
+---------------+----------------+
1 row in set (0.00 sec)


root@localhost : (none) 02:54:35> START TRANSACTION WITH CONSISTENT SNAPSHOT;
Query OK, 0 rows affected, 1 warning (0.00 sec)


Warning (Code 138): InnoDB: WITH CONSISTENT SNAPSHOT was ignored because this phrase can only be used with REPEATABLE READ isolation level.

从inotify机制谈FileObserver实现原理

有些情况下,我们难免需要监控一些文件的变化情况,这该如何实现呢?自然而然的我们会想要利用一个线程,每个一段时间便去看看文件的情况,这种方式本质上就是基于时间调度的轮训.虽然能够实现我们的需求,但是这种方式只适合文件经常变化的情况,其他情况下都非常低效,并且可能丢掉某些类型的变化,也就是说,这种方式无法实现实时的文件监控.

inotify简介

那还有其他的方式么?熟悉linux的童鞋应该记得从linux kernel 2.6.13开始引入inotify机制,用于通知用户相关文件变化情况:任何一个文件发生某种变化,都会产生一个相应的文件事件.

我们不仅好奇,文件的哪些事件能够被监控,也就是说inotify支持监控文件的哪些变化呢?继续往下看.

可监控事件类型

目前inotify能够监控的以下文件变化事件:

未分类

API说明

不难发现,inotify对文件监控的支持是非常全面的,足以满足我们绝大部门的需求.接下来,我们对innotify的api做个简单的说明:

未分类

事件通知

在可监控事件中,我们已经了解inotify支持的文件事件.现在来看看这些当这些文件事件产生时,其发出通知的结构.在inotify中,事件通知结构用结构体inotify_event表示:

struct inotify_event
{
  int wd;               //监控目标的watch描述符
  uint32_t mask;        //事件掩码
  uint32_t cookie;      //事件同步cookie
  uint32_t len;         //name字符串的长度
  char name __flexarr;  //被监视目标的路径名
  };

这里需要记住一点:name字段并不是什么时候都有的:只有要监控的目标是一个目录,且产生的事件与目录内部的文件或子目录相关,且与目录本身无关时才会提供相应的name字段.
cookie用于关联被观察对象的IN_MOVED_FROM事件和IN_MOVED_TO 事件.

使用流程

了解以上之后,那么该怎么用呢?要实现对文件或者目录的监控需要经过以下几个步骤:

1. 创建inotify实例

在应用程序中,首先需要创建inotify实例:

int fd=inotify_init();

该方法创建了一个inotify实例,并返回一个文件描述符以便能够通过这个描述符访问到inotify实例.

2. 添加监控

在获得inotify实例产生的文件描述符之后,我们就可以为其添加watch.另外,我们也可以使用mask(事件掩码)来设置我们想监控的事件类型.当然可以我们也可以使用IN_ALL_EVENTS监控所有事件:

int wd=inotify_add_watch(fd,path,mask)

补充:
此处的fd即inotify_init()方法返回的文件描述符.每个文件描述符都有一个排序的事件序列.path则是需要监控的文件或者目录的路径.mask则是事件掩码,它表示应用程序对哪些事件感兴趣.

文件系统产生的事件由Watch对象来管理,该方法将返回的wd就是Watch对象的句柄.

3. 等待事件与循环处理

在为inotify实例添加watch之后,接下来就是等待事件了.为了能不断的处理事件,我们将其放在循环体当中.
在循环中,通过read()方法可以一次获得多个事件.在没有事件产生时,read()被阻塞,一旦有事件产生,那么我们就可以读取事件到的我们设置的事件数组中,然后对事件数组进行处理,其简单代码如下:

//事件数组,自定义设置,这里我们设置为128
char event_buf[128];

while(true){
     int num_bytes=read(fd,event_buf,len);
     //处理事件
     handleEvent(event_buf);
     //....省略....
    }

补充

event_buf是一个事件数组,用于接受文件变化所产生的事件(inotify_event).len则指定了要读的长度.通常来说,len大于事件数组的大小,很多时候,我们也会直接取事件数组的大小来作为len.

4.停止监控

当需要停止监控的时候,需要为文件描述符删除watch:

int r=inotify_rm_watch(fd,wd);

此处的fd也是在创建inotify时返回的文件描述符,wd则是上面提到watch对象的句柄.

到现在,我们已经对inotity有了初步的理解,感兴趣的童鞋可以自行研究.我们的重点还是Android中FileObserver的实现.接下来,我们真正的开始了解FileObserver的实现.

FileObserver实现原理

我们知道Android 1.5时对应的linux内核已经是2.6.26,因此完全可以在Android上利用inotify机制来实现对文件的监控.Google很显然意识到了这一点,并且帮我们在inotify的基础上进行封装—FileObserver,以实现监听文件访问,创建,修改,删除等操作

接下来,来看一下FileObserver如何借助inotify实现文件监控的.

监控线程初始化

在FileObserver中存在一个静态内部类ObserverThread,该线程类是实现了文件监控的过程:

public abstract class FileObserver {
    //可监控的事件
    public static final int ALL_EVENTS = ACCESS | MODIFY | ATTRIB | CLOSE_WRITE
            | CLOSE_NOWRITE | OPEN | MOVED_FROM | MOVED_TO | DELETE | CREATE
            | DELETE_SELF | MOVE_SELF;

    private static class ObserverThread extends Thread {
        //....省略多行代码....
    }

    private static ObserverThread s_observerThread;

    static {
        s_observerThread = new ObserverThread();
        s_observerThread.start();
    }

       //....省略多行代码....

}

不难发现发现FileObserver通过静态代码块的方式构造了s_observerThread对象,我们来看一下其构造过程:

public ObserverThread() {
       super("FileObserver");
       m_fd = init();
   }

这里又调用natvie方法init().既然这样,我们就在深入一下,看看init()方法的实现(现在,是不是发现我们自己编译源码的好处了?)该方法的实现在/frameworks/base/core/jni/android_util_FileObserver.cpp

static jint android_os_fileobserver_init(JNIEnv* env, jobject object)
{
#if defined(__linux__)
    return (jint)inotify_init();
#else
    return -1;
#endif
}

其实现非常简单,就是调用inotify中的inotify_init()来创建一个inotify实例。回到FileObserver中来看s_ObserverThread的启动:

public void run() {
            observe(m_fd);
        }

这里同样是调用natvie方法observe(int fd):

static void android_os_fileobserver_observe(JNIEnv* env, jobject object, jint fd)
{
#if defined(__linux__)

2    //设置事件数组
    char event_buf[512];
    struct inotify_event* event;

    //循环处理读到的事件
    while (1)
    {
        int event_pos = 0;
        //读取事件
        int num_bytes = read(fd, event_buf, sizeof(event_buf));

        if (num_bytes < (int)sizeof(*event))
        {
            if (errno == EINTR)
                continue;

            ALOGE("***** ERROR! android_os_fileobserver_observe() got a short event!");
            return;
        }

        //处理事件数组
        while (num_bytes >= (int)sizeof(*event))
        {
            int event_size;
            event = (struct inotify_event *)(event_buf + event_pos);

            jstring path = NULL;

            if (event->len > 0)
            {
                path = env->NewStringUTF(event->name);
            }

           //调用ObserverThread中的onEvent方法 
            env->CallVoidMethod(object, method_onEvent, event->wd, event->mask, path);
            if (env->ExceptionCheck()) {
                env->ExceptionDescribe();
                env->ExceptionClear();
            }
            if (path != NULL)
            {
                env->DeleteLocalRef(path);
            }

            event_size = sizeof(*event) + event->len;
            num_bytes -= event_size;
            event_pos += event_size;
        }
    }

#endif
}

不难看出,此处的循环主要就是从inotity中取出事件,然后回调ObserverThread中的onEvent()方法.现在,回到ObserverThread中的onEvent()方法中:

public void onEvent(int wfd, int mask, String path) {
            // look up our observer, fixing up the map if necessary...
            FileObserver observer = null;

            synchronized (m_observers) {
                //根据wfd找出FileObserver对象
                WeakReference weak = m_observers.get(wfd);
                if (weak != null) {  // can happen with lots of events from a dead wfd
                    observer = (FileObserver) weak.get();
                    if (observer == null) {//observer已经被回收时,从m_observers中删除该对象
                        m_observers.remove(wfd);
                    }
                }
            }

            // ...then call out to the observer without the sync lock held
            if (observer != null) {
                try {
                    //回调给FileObserver中的onEvent方法进行处理
                    observer.onEvent(mask, path);
                } catch (Throwable throwable) {
                    Log.wtf(LOG_TAG, "Unhandled exception in FileObserver " + observer, throwable);
                }
            }
        }

FileObserver中的onEvent()为抽象方法,也就是要求你继承FileObserver,并实现该方法,在其中做相关的操作.
到现在为止我们已经明白了ObserverThread如何被启动,以及如何获取inotify中的事件,并回调给上层进行处理.

启动监控

上面提到m_observers表,该表维护着已经注册的FileObserver对象.接下来,我们就就来看看FileObserver中的startWatching()方法,该方法注册FileObserver对象,也是启动监控的过程:

public void startWatching() {
        if (m_descriptor < 0) {
            m_descriptor = s_observerThread.startWatching(m_path, m_mask, this);
        }
    }

具体的注册操作委托给s_observerThread中的startWatching():

public int startWatching(String path, int mask, FileObserver observer) {
            //调用native方法startWatching,并得到一个watch对象的句柄
            int wfd = startWatching(m_fd, path, mask);

            Integer i = new Integer(wfd);
            if (wfd >= 0) {
                synchronized (m_observers) {
                    //将watch对象句柄和当前FileObserver关联
                    m_observers.put(i, new WeakReference(observer));
                }
            }

            return i;
        }

该方法中同样调用了native方法,其具体实现是:

static jint android_os_fileobserver_startWatching(JNIEnv* env, jobject object, jint fd, jstring pathString, jint mask)
{
    int res = -1;
#if defined(__linux__)
    if (fd >= 0)
    {
        const char* path = env->GetStringUTFChars(pathString, NULL);

        res = inotify_add_watch(fd, path, mask);

        env->ReleaseStringUTFChars(pathString, path);
    }
#endif
    return res;
}

不难看出,这里通过inotify的inotify_add_watch()为上面生成的inotify对象添加watch对象,并将watch对象的句柄返回给ObserverThread.

停止监控

到现在我们已经了解了如何注册watch句柄到FileObserver对象.有了注册的过程,当然少不了反注册的过程.同样,FileObserver为我们提供了stopWatching()来实现反注册,即停止监控的过程:

public void stopWatching() {
       if (m_descriptor >= 0) {//已经注册过的才能反注册
           s_observerThread.stopWatching(m_descriptor);
           m_descriptor = -1;
       }
   }

具体的实现也是交给了s_observerThread的stopWatching()方法:

public void stopWatching(int descriptor) {
            stopWatching(m_fd, descriptor);
        }

接着委托给了natvie方法:

static void android_os_fileobserver_stopWatching(JNIEnv* env, jobject object, jint fd, jint wfd)
{
#if defined(__linux__)
    inotify_rm_watch((int)fd, (uint32_t)wfd);
#endif
}

这里的实现非常简单,就是调用inotify_rm_watch方法来解除inotify实例和watch实例的关系.

到现在为止我们已经弄明白了FileObserver的实现原理,为了方便理解,我们用一张简单的图来描述整个过程:

未分类

使用说明

想必你已经了解了FileObserver的实现原理,接下里我们来看看如何使用.要想实现文件监控,我们只需要继承FileObserver类,并在onEvent()处理相关事件即可,简单的用代码演示一下:

public class SDCardObserver extends FileObserver {
    public SDCardObserver(String path) {
        super(path);
    }

    @Override
    public void onEvent(int i, String s) {
        switch (i) {
            case FileObserver.ALL_EVENTS:
                //全部事件
                break;
            case FileObserver.CREATE:
                //文件被创建
                break;
            case FileObserver.DELETE:
                //文件被删除
                break;
            case FileObserver.MODIFY:
                //文件被修改
                break;
        }
    }
}

注意事项

这里我们需要注意两个问题:

  • 谨慎的选择你要处理的事件,否则可能造成死循环.

  • 当不再需要监听时,请记得停止监控

  • 需要注意FileObserver对象被垃圾回收的情况,从上面的原理中我们知道,该对象被回收后将不会再触发事件.

配置rsync Inotify进行文件实时同步

一、简介

rsync用于网络间数据备份 具备高安全性,能实现增量备份,监控的文件必须扫描 文件量大时扫描花费大量时间 所以使用inotify的异步文件系统监控

调用内核监控 检测到文件的修改 同时rsync同步文件

rpmfind.NET搜索 rsync 找到官网地址为 http://rsync.samba.org/ 可以下载源码包进行安装

二、安装过程

1、模拟环境

  • 源服务器192.168.58.142 目标服务器(可以有多台) 192.168.58.143

  • 目标服务器需要监听服务 等待源服务器(客户端)推送文件

2、演示rsync安装过程

目标服务器rsync安装和配置

yum install rsync xinetd

xinetd即extended internet daemon,xinetd是新一代的网络守护进程服务程序,可以用于管理其他的网络服务

  • 关闭selinux(不关闭有可能抛出rsync: mkstemp “/.a.txt.SqAQVm” (in test) failed: Permission denied (13))

  • 临时关闭 setenforce 0 或者编辑 etc/selinux/config 修改 SELINUX=disabled 永久关闭

编辑/etc/xinted.d/rsync

service rsync  
{  
        disable = no  #将禁用改成no  
        flags           = IPv6  
        socket_type     = stream  
        wait            = no  
        user            = root  
        server          = /usr/bin/rsync  
        server_args     = --daemon  
        log_on_failure  += USERID  
}   

通过官方文档 查看rsync的配置文件格式(http://everythinglinux.org/rsync/)

[root@ha etc]# vi rsyncd.conf  
log file = /var/log/rsyncd.log  #日志文件  
pid file = /var/run/rsyncd.pid  #进程文件  
lock file = /var/run/rsync.lock #同步锁文件  

[simple_path_name]   
   path = /data   #表示要同步的目录  
   comment = My Very Own Rsync Server #表示注释  
   uid = root   #同步有权限的用户名  
   gid = root   #同步有权限的用户组  
   port = 873   #对外监听的端口  
   read only = no  #是否文件目录是只读的  
   list = yes   # 是否显示服务端文件列表  
   auth users = mysync  需要同步的文件需要用到的内部账号 可以在下面参数文件中定义  
   secrets file = /etc/rsyncd.scrt 同步的内部通信的账号和密码文件  

创建 用户密码文件 /etc/rsyncd.scrt 格式为:用户名:密码 可以定义多个用户

mysync:123456  

给该两个文件设置600权限

chmod 600 rsyncd.conf

chmod 600 /etc/rsyncd.scrt 

启动rsync

service xinetd start |  stop |restart  

查看运行状态

[root@ha etc]# service xinetd status  
xinetd (pid  7283) is running...  

查看默认 873端口是否开放

[root@ha etc]# netstat -aon | grep 873  
    tcp        0      0 :::873                      :::*                        LISTEN      off (0.00/0/0)  

尝试在任意机器 使用 telnet 192.168.58.142 873 可以查看配置的日志文件

[root@ha run]# more /var/log/rsyncd.log  
    2017/08/03 12:13:30 [7406] connect from ha1 (192.168.58.143)  
    2017/08/03 12:14:11 [7505] connect from ha1 (192.168.58.143)  

源服务器(192.168.58.143)rsync安装和配置

yum install rsync xinetd

xinetd即extended internet daemon,xinetd是新一代的网络守护进程服务程序,可以用于管理其他的网络服务

编辑/etc/xinted.d/rsync

service rsync  
{  
        disable = no  #将禁用改成no  
        flags           = IPv6  
        socket_type     = stream  
        wait            = no  
        user            = root  
        server          = /usr/bin/rsync  
        server_args     = --daemon  
        log_on_failure  += USERID  
}  
  • 关闭selinux(不关闭有可能抛出rsync: mkstemp “/.a.txt.SqAQVm” (in test) failed: Permission denied (13))

  • 临时关闭 setenforce 0 或者编辑 etc/selinux/config 修改 SELINUX=disabled 永久关闭

编辑/etc/xinted.d/rsync

使用命令测试同步文件-v表示增量备份 -a表示传输文件 /data/ 表示当前机器需要同步到目标服务器的文件目录 :test 表示目标服务器 rsyncd.conf定义的[模块名称]

rsync -avH --port=873 --progress  /data/   [email protected]::test  要求你输入目标服务器用户名密码配置文件中的密码 输入123456即可

多次执行只有一次同步 修改文件后再次尝试

3、演示inotify安装过程

  • rpmfind.net搜索 inotify-tool 搜索到官网 https://github.com/rvoicilas/inotify-tools/wiki

  • 下载 源代码 wget https://cloud.github.com/downloads/rvoicilas/inotify-tools/inotify-tools-3.14.tar.gz

  • inotify只需要在源服务器监听文件的修改状态 然后调用rsync即可 或者使用scp都行

  • 解压后 ./configure –prefix=/usr/local/inotify & make & make install 安装 (安装过rsync 一般gcc都已经安装 如果没有 yum -y install gcc)

  • 安装完成后 /usr/local/inotify/bin下的可执行文件 必须设置到环境变量中

  • vi ~/.bash_profile 添加 或者添加到/etc/rc.local文件中

PATH=$PATH:/usr/local/inotify/bin  
export PATH  

source .bash_profile 执行

查看inotify的三个内核参数是否成功配置

[root@ha1 bin]# ll /proc/sys/fs/inotify    
total 0  
-rw-r--r-- 1 root root 0 Aug  3 14:24 max_queued_events  
-rw-r--r-- 1 root root 0 Aug  3 14:24 max_user_instances  
-rw-r--r-- 1 root root 0 Aug  3 14:24 max_user_watches  

该三个参数的作用为

max_user_instances:用户创建inotify实例最大值  
max_queued_events:inotify产生的事件队列最大长度,如果值太小,会出现错误,导致监控文件不准确  
max_user_watches:监控同步的文件包含的最大目录数,  
可以用:find /data -type d|wc -l 统计,必须保证参数值大于统计结果(/home/rain为同步文件目录)。  

查看查看完整的参数名称

[root@ha1 bin]# sysctl -a | grep max_user_instances  
fs.inotify.max_user_instances = 128  #可以看出参数名 带了前缀 fs.inotify.  

可以通过sysctl -w 参数名=参数值修改 或者编辑 /etc/sysctl.conf 添加参数名=参数值键值对
查看官方教程 https://github.com/rvoicilas/inotify-tools/wiki#info 通过查看/usr/local/inotify/bin目录 发现就两个命令

inotifywait 用于等待一个事件被触发(增删改移动等)  
inotifywatch 监听某个目录后者文件的所有事件 一有事件就触发  

4、inotifywatch 演示

(-e表示监听事件(create创建 access表示读取 delete删除 modify修改) 可以通过inotifywatch –help查看 -t表示监听事件 -r 表示监听目录)

inotifywatch -v -e access -e modify -t 60 -r /data 

60s内尝试读取或者修改文件 过了60s后统计记录就会出现在一个列表中

[root@ha1 bin]# inotifywatch -v -e access -e modify -t 60 -r /data  
Establishing watches...  
Setting up watch(es) on /data  
OK, /data is now being watched.  
Total of 1 watches.  
Finished establishing watches, now collecting statistics.  
Will listen for events for 60 seconds.  
total  access  modify  filename  
4      1       3       /data/  

5、inotifywait演示

该命令 等待一个事件触发 触发后返回状态 此时根据状态调用rsync同步

测试该命令 创建一个脚本文件

vi ~/test.sh
#!/bin/sh  

EVENT=$(inotifywait --format '%e' ~/data)  
[ $? != 0 ] && exit  # 返回值0表示成功 不为0表示出现错误 就退出  
[ "$EVENT" = "MODIFY" ] && echo 'file modified!' 根据变量EVENT判断是哪个事件被触发了   
[ "$EVENT" = "DELETE" ] && echo 'file deleted!'  
chmod +x ~/test.sh
~/test.sh 

发现出现了阻塞 等待一个事件

[root@ha1 bin]# ./test.sh  
Setting up watches.  
Watches established.  

另外一个客户端上执行 删除/data下一个文件

[root@ha1 bin]# ./test.sh  
Setting up watches.  
Watches established.  
file delete  #输出了脚本中 输出的内容  

当然可以通过设置输出输出文件名等信息

  • –format表示时间输出格式 %T表示时间 时间格式–timefmt表示 %d表示天 %m月 %y年 %H 24小时制小时 %M分钟

  • -e表示事件 可以多个 %w表示目录 %f表示文件

[root@ha1 bin]# inotifywait -mr --timefmt '%d/%m/%y %H:%M' --format '%T %w %f' -e create /data  
Setting up watches.  Beware: since -r was given, this may take a while!  
Watches established.  
03/08/17 15:16 /data/ a.txt  
03/08/17 15:17 /data/ b.txt  

也可以通过while循环 使用管道命令获取到对应的文件目录

inotifywait -mr --timefmt '%d/%m/%y %H:%M' --format '%T %w %f' -e create /data |  
while read date time dir file  
do  
 echo $file  
done  

6、inotifywait整合rsync备份文件从源服务

有了上面 inotifywait的基础后 只需要在循环中获取到file 调用同步即可

使用 以下脚本测试 所有的时间 (通过inotifywait –help查看)

inotifywait -mr --timefmt '%d/%m/%y %H:%M' --format '%T %w %f %e' -e create,modify,delete,move /data |  
while read date time dir file event  
do  
 echo "$dir $file $event"  
done  

尝试去修改文件创建目录等操作 控制台输出如下 代码中根据不同的类型进行不同的处理

/data/ cc CREATE,ISDIR  创建目录  
/data/cc/ a.txt CREATE  创建文件  
/data/cc/ a.txt MOVED_FROM 从哪里移动  
/data/cc/ b.txt MOVED_TO   移动到这里  
/data/cc/ b.txt DELETE  删除文件  
/data/ cc DELETE,ISDIR 删除目录  
/data/ bb MOVED_FROM,ISDIR 从目录移动  
/data/ cc MOVED_TO,ISDIR   移动到新目录  

完整代码如下(如果有一个文件被修改就同步该文件就行了 但是实现起来较为复杂 这里直接更新整个目录)还是rsync的环境

srcDir=/data  
echo 123456 > /pass.wd  
chmod 600 /pass.wd  
inotifywait -mr --timefmt '%d/%m/%y %H:%M' --format '%T %w %f %e' -e create,modify,delete,move $srcDir |  
while read date time dir file event  
do  
 echo "$dir $file $event begin syncing"  
 rsync -avH --port=873 --progress --password-file=/pass.wd  $srcDir [email protected]::test   
done  

目标服务器 192.168.58.143 新建/my.sh 填写上边内容

nohup /my.sh & 运行 测试在/data目录修改数据 是否能同步到 58.142对应的/data目录。