xiaobaoqiu Blog

Think More, Code Less

Linux进制转换

经常涉及到进制的转换,比如线上处理问题时候经常需要转换线程id到16进制.

1.进制转换

2.1.shell运算

Shell 运算把一个数字从给定的进制转换位十进制.如果数字以运算展开式的形式提供,那么假定它带有十进制符号,除非 它前面带有 0(这种情况假定是八进制)或 0x(这种情况假定是十六进制)

1
2
3
4
xiaobaoqiu@xiaobaoqiu:~/myshell$ echo $((010))
8
xiaobaoqiu@xiaobaoqiu:~/myshell$ echo $((0x10))
16

也可以指定 2 到 64 之间的任意进制,超过64进制则不支持.格式如下:

1
$((BASE#NUMBER))

使用举例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
xiaobaoqiu@xiaobaoqiu:~/myshell$ echo $((2#10))
2
xiaobaoqiu@xiaobaoqiu:~/myshell$ echo $((5#10))
5
xiaobaoqiu@xiaobaoqiu:~/myshell$ echo $((8#10))
8
xiaobaoqiu@xiaobaoqiu:~/myshell$ echo $((10#10))
10
xiaobaoqiu@xiaobaoqiu:~/myshell$ echo $((16#10))
16
xiaobaoqiu@xiaobaoqiu:~/myshell$ echo $((64#10))
64
xiaobaoqiu@xiaobaoqiu:~/myshell$ echo $((100#10))
bash: 100#10: 无效的算数进制 (错误符号是 "100#10")

2.2.bc

bc是一种任意精度运算语言,大多数 UNIX/Linux 安装程序都提供.因为它允许您指定输出进制,所以当您需要以十进制以外的进制输出时,这是一种很好的技术.

bc 的特殊变量 ibase 和 obase 分别包含用于输入和输出的进制的值.缺省情况下,都被设置为 10.要执行进制转换,需要改变其中的一个或两个值,然后提供一个数字.

1
2
3
4
5
6
xiaobaoqiu@xiaobaoqiu:~/myshell$ echo 'obase=16; 10' | bc
A
xiaobaoqiu@xiaobaoqiu:~/myshell$ echo 'obase=16; ibase=10; 10' | bc
A
xiaobaoqiu@xiaobaoqiu:~/myshell$ echo 'obase=10; ibase=16; 10' | bc
16

2.3.printf

格式化参数和C语言的格式一致:

1
2
3
4
xiaobaoqiu@xiaobaoqiu:~/myshell$ printf "%X\n" 100
64
xiaobaoqiu@xiaobaoqiu:~/myshell$ printf "%d\n" 0x10
16

2.自定义shell

自己写一个shell,处理多个输入,任意进制到任意进制的转换.通过这个脚本,学习了一下shell处理option的方式.

原理很简单,就是利用上面说道的bc命令.shell脚本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#!/bin/bash
while getopts :i:o: opt 
do
        case "$opt" in
        i) #输入的参数的进制
           #echo "Found the -i option,with vale $OPTARG"
           ibase=$OPTARG
           ;;
        o) #输出参数进制
           #echo "Found the -o option,with vale $OPTARG"
           obase=$OPTARG
           ;;
        *) #当有不认识的选项的时候arg为?
           echo "unkonw argument, Usage : "
           echo "$1 -i 10 -o 16 1 2 3 4 5 6 ..."
           exit 1
        esac
done

#跳过opt参数
shift $[$OPTIND - 1]

#参数检验
if [ ! $ibase ]; then
    echo "Usage : $0 -i 10 -o 16 1 2 3 4 5 6 ..."
    exit 1
fi

if [ ! $obase ]; then
    echo "Usage : $0 -i 10 -o 16 1 2 3 4 5 6 ..."
    exit 1
fi

echo "输入进制:$ibase"
echo "输出进制:$obase"

#执行进制转换
for i in $@
do
    echo "obase=$obase; ibase=$ibase; $i" |bc
done

1.bc

2.bc

简单使用如下:

1
2
3
4
xiaobaoqiu@xiaobaoqiu:~/myshell$ ./trans -i 10 -o 16 100
输入进制:10
输出进制:16
64

RateLimiter

昨天CodeReview的时候看到同时使用RateLimiter这个类用作QPS访问限制.学习一下这个类.

RateLimiter是Guava的concurrent包下的一个用于限制访问频率的类.

1.限流

每个API接口都是有访问上限的,当访问频率或者并发量超过其承受范围时候,我们就必须考虑限流来保证接口的可用性或者降级可用性.即接口也需要安装上保险丝,以防止非预期的请求对系统压力过大而引起的系统瘫痪.

通常的策略就是拒绝多余的访问,或者让多余的访问排队等待服务,或者引流.

如果要准确的控制QPS,简单的做法是维护一个单位时间内的Counter,如判断单位时间已经过去,则将Counter重置零.此做法被认为没有很好的处理单位时间的边界,比如在前一秒的最后一毫秒里和下一秒的第一毫秒都触发了最大的请求数,将目光移动一下,就看到在两毫秒内发生了两倍的QPS.

2.限流算法

常用的更平滑的限流算法有两种:漏桶算法和令牌桶算法.

很多传统的服务提供商如华为中兴都有类似的专利,参考: http://www.google.com/patents/CN1536815A?cl=zh

2.1 漏桶算法

漏桶(Leaky Bucket)算法思路很简单,水(请求)先进入到漏桶里,漏桶以一定的速度出水(接口有响应速率),当水流入速度过大会直接溢出(访问频率超过接口响应速率),然后就拒绝请求,可以看出漏桶算法能强行限制数据的传输速率.示意图如下:

可见这里有两个变量,一个是桶的大小,支持流量突发增多时可以存多少的水(burst),另一个是水桶漏洞的大小(rate),伪代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
double rate;               // leak rate in calls/s
double burst;              // bucket size in calls

long refreshTime;          // time for last water refresh
double water;              // water count at refreshTime

refreshWater() {
    long  now = getTimeOfDay();
    
    //水随着时间流逝,不断流走,最多就流干到0.
    water = max(0, water- (now - refreshTime)*rate); 
    refreshTime = now;
}

bool permissionGranted() {
    refreshWater();
    if (water < burst) { // 水桶还没满,继续加1
        water ++;
        return true;
    } else {
        return false;
    }
}

因为漏桶的漏出速率是固定的参数,所以,即使网络中不存在资源冲突(没有发生拥塞),漏桶算法也不能使流突发(burst)到端口速率.因此,漏桶算法对于存在突发特性的流量来说缺乏效率.

2.2 令牌桶算法

令牌桶算法(Token Bucket)和 Leaky Bucket 效果一样但方向相反的算法,更加容易理解.随着时间流逝,系统会按恒定1/QPS时间间隔(如果QPS=100,则间隔是10ms)往桶里加入Token(想象和漏洞漏水相反,有个水龙头在不断的加水),如果桶已经满了就不再加了.新请求来临时,会各自拿走一个Token,如果没有Token可拿了就阻塞或者拒绝服务.

令牌桶的另外一个好处是可以方便的改变速度. 一旦需要提高速率,则按需提高放入桶中的令牌的速率. 一般会定时(比如100毫秒)往桶中增加一定数量的令牌, 有些变种算法则实时的计算应该增加的令牌的数量.

3.RateLimiter简介

Google开源工具包Guava提供了限流工具类RateLimiter,该类基于令牌桶算法(Token Bucket)来完成限流,非常易于使用.RateLimiter经常用于限制对一些物理资源或者逻辑资源的访问速率.它支持两种获取permits接口,一种是如果拿不到立刻返回false,一种会阻塞等待一段时间看能不能拿到.

RateLimiter和Java中的信号量(java.util.concurrent.Semaphore)类似,Semaphore通常用于限制并发量.

源码注释中的一个例子,比如我们有很多任务需要执行,但是我们不希望每秒超过两个任务执行,那么我们就可以使用RateLimiter:

1
2
3
4
5
6
7
final RateLimiter rateLimiter = RateLimiter.create(2.0);
void submitTasks(List<Runnable> tasks, Executor executor) {
    for (Runnable task : tasks) {
        rateLimiter.acquire(); // may wait
        executor.execute(task);
    }
}

另外一个例子,假如我们会产生一个数据流,然后我们想以每秒5kb的速度发送出去.我们可以每获取一个令牌(permit)就发送一个byte的数据,这样我们就可以通过一个每秒5000个令牌的RateLimiter来实现:

1
2
3
4
5
final RateLimiter rateLimiter = RateLimiter.create(5000.0);
void submitPacket(byte[] packet) {
    rateLimiter.acquire(packet.length);
    networkService.send(packet);
}

另外,我们也可以使用非阻塞的形式达到降级运行的目的,即使用非阻塞的tryAcquire()方法:

1
2
3
4
5
if(limiter.tryAcquire()) { //未请求到limiter则立即返回false
    doSomething();
}else{
    doSomethingElse();
}

4.RateLimiter主要接口

RateLimiter其实是一个abstract类,但是它提供了几个static方法用于创建RateLimiter:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 创建一个稳定输出令牌的RateLimiter,保证了平均每秒不超过permitsPerSecond个请求
* 当请求到来的速度超过了permitsPerSecond,保证每秒只处理permitsPerSecond个请求
* 当这个RateLimiter使用不足(即请求到来速度小于permitsPerSecond),会囤积最多permitsPerSecond个请求
*/
public static RateLimiter create(double permitsPerSecond);

/**
* 创建一个稳定输出令牌的RateLimiter,保证了平均每秒不超过permitsPerSecond个请求
* 还包含一个热身期(warmup period),热身期内,RateLimiter会平滑的将其释放令牌的速率加大,直到起达到最大速率
* 同样,如果RateLimiter在热身期没有足够的请求(unused),则起速率会逐渐降低到冷却状态
* 
* 设计这个的意图是为了满足那种资源提供方需要热身时间,而不是每次访问都能提供稳定速率的服务的情况(比如带缓存服务,需要定期刷新缓存的)
* 参数warmupPeriod和unit决定了其从冷却状态到达最大速率的时间
*/
public static RateLimiter create(double permitsPerSecond, long warmupPeriod, TimeUnit unit);

提供了两个获取令牌的方法,不带参数表示获取一个令牌.如果没有令牌则一直等待,返回等待的时间(单位为秒),没有被限流则直接返回0.0:

1
2
3
public double acquire();

public double acquire(int permits);

尝试获取令牌,分为待超时时间和不带超时时间两种:

1
2
3
4
5
6
public boolean tryAcquire();
//尝试获取一个令牌,立即返回
public boolean tryAcquire(int permits);
public boolean tryAcquire(long timeout, TimeUnit unit);
//尝试获取permits个令牌,带超时时间
public boolean tryAcquire(int permits, long timeout, TimeUnit unit);

5.RateLimiter设计

考虑一下RateLimiter是如何设计的,并且为什么要这样设计.

RateLimiter的主要功能就是提供一个稳定的速率,实现方式就是通过限制请求流入的速度,比如计算请求等待合适的时间阈值.

实现QPS速率的最简单的方式就是记住上一次请求的最后授权时间,然后保证1/QPS秒内不允许请求进入.比如QPS=5,如果我们保证最后一个被授权请求之后的200ms的时间内没有请求被授权,那么我们就达到了预期的速率.如果一个请求现在过来但是最后一个被授权请求是在100ms之前,那么我们就要求当前这个请求等待100ms.按照这个思路,请求15个新令牌(许可证)就需要3秒.

有一点很重要:上面这个设计思路的RateLimiter记忆非常的浅,它的脑容量非常的小,只记得上一次被授权的请求的时间.如果RateLimiter的一个被授权请求q之前很长一段时间没有被使用会怎么样?这个RateLimiter会立马忘记过去这一段时间的利用不足,而只记得刚刚的请求q.

过去一段时间的利用不足意味着有过剩的资源是可以利用的.这种情况下,RateLimiter应该加把劲(speed up for a while)将这些过剩的资源利用起来.比如在向网络中发生数据的场景(限流),过去一段时间的利用不足可能意味着网卡缓冲区是空的,这种场景下,我们是可以加速发送来将这些过程的资源利用起来.

另一方面,过去一段时间的利用不足可能意味着处理请求的服务器对即将到来的请求是准备不足的(less ready for future requests),比如因为很长一段时间没有请求当前服务器的cache是陈旧的,进而导致即将到来的请求会触发一个昂贵的操作(比如重新刷新全量的缓存).

为了处理这种情况,RateLimiter中增加了一个维度的信息,就是过去一段时间的利用不足(past underutilization),代码中使用storedPermits变量表示.当没有利用不足这个变量为0,最大能达到maxStoredPermits(maxStoredPermits表示完全没有利用).因此,请求的令牌可能从两个地方来:

1.过去剩余的令牌(stored permits, 可能没有)
2.现有的令牌(fresh permits,当前这段时间还没用完的令牌)

我们将通过一个例子来解释它是如何工作的:

对一个每秒产生一个令牌的RateLimiter,每有一个没有使用令牌的一秒,我们就将storedPermits加1,如果RateLimiter在10秒都没有使用,则storedPermits变成10.0.这个时候,一个请求到来并请求三个令牌(acquire(3)),我们将从storedPermits中的令牌为其服务,storedPermits变为7.0.这个请求之后立马又有一个请求到来并请求10个令牌,我们将从storedPermits剩余的7个令牌给这个请求,剩下还需要三个令牌,我们将从RateLimiter新产生的令牌中获取.我们已经知道,RateLimiter每秒新产生1个令牌,就是说上面这个请求还需要的3个请求就要求其等待3秒.

想象一个RateLimiter每秒产生一个令牌,现在完全没有使用(处于初始状态),限制一个昂贵的请求acquire(100)过来.如果我们选择让这个请求等待100秒再允许其执行,这显然很荒谬.我们为什么什么也不做而只是傻傻的等待100秒,一个更好的做法是允许这个请求立即执行(和acquire(1)没有区别),然后将随后到来的请求推迟到正确的时间点.这种策略,我们允许这个昂贵的任务立即执行,并将随后到来的请求推迟100秒.这种策略就是让任务的执行和等待同时进行.

一个重要的结论:RateLimiter不会记最后一个请求,而是即下一个请求允许执行的时间.这也可以很直白的告诉我们到达下一个调度时间点的时间间隔.然后定一个一段时间未使用的Ratelimiter也很简单:下一个调度时间点已经过去,这个时间点和现在时间的差就是Ratelimiter多久没有被使用,我们会将这一段时间翻译成storedPermits.所有,如果每秒钟产生一个令牌(rate==1),并且正好每秒来一个请求,那么storedPermits就不会增长.

6.RateLimiter主要源码

RateLimiter定义了两个create函数用于构建不同形式的RateLimiter:

1.public static RateLimiter create(double permitsPerSecond)
用于创建SmoothBursty类型的RateLimiter
2.public static RateLimiter create(double permitsPerSecond, long warmupPeriod, TimeUnit unit)
用于创建

源码下面以acquire为例子,分析一下RateLimiter如何实现限流:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public double acquire() {
    return acquire(1);
}
public double acquire(int permits) {
    long microsToWait = reserve(permits);
    stopwatch.sleepMicrosUninterruptibly(microsToWait);
    return 1.0 * microsToWait / SECONDS.toMicros(1L);
}
final long reserve(int permits) {
    checkPermits(permits);
    synchronized (mutex()) {    //应对并发情况需要同步
      return reserveAndGetWaitLength(permits, stopwatch.readMicros());
    }
}
final long reserveAndGetWaitLength(int permits, long nowMicros) {
    long momentAvailable = reserveEarliestAvailable(permits, nowMicros);
    return max(momentAvailable - nowMicros, 0);
}

下面方法来自RateLimiter的具体实现类SmoothRateLimiter:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
final long reserveEarliestAvailable(int requiredPermits, long nowMicros) {
    resync(nowMicros);  //补充令牌
    long returnValue = nextFreeTicketMicros;
    //这次请求消耗的令牌数目
    double storedPermitsToSpend = min(requiredPermits, this.storedPermits);
    double freshPermits = requiredPermits - storedPermitsToSpend;

    long waitMicros = storedPermitsToWaitTime(this.storedPermits, storedPermitsToSpend)
        + (long) (freshPermits * stableIntervalMicros);

    this.nextFreeTicketMicros = nextFreeTicketMicros + waitMicros;
    this.storedPermits -= storedPermitsToSpend;
    return returnValue;
}
private void resync(long nowMicros) {
    // if nextFreeTicket is in the past, resync to now
    if (nowMicros > nextFreeTicketMicros) {
        storedPermits = min(maxPermits,
        storedPermits + (nowMicros - nextFreeTicketMicros) / stableIntervalMicros);
        nextFreeTicketMicros = nowMicros;
    }
}

另外,对于storedPermits的使用,RateLimiter存在两种策略,二者区别主要体现在使用storedPermits时候需要等待的时间。这个逻辑由storedPermitsToWaitTime函数实现:

1
2
3
4
5
6
7
8
9
/**
 * Translates a specified portion of our currently stored permits which we want to
 * spend/acquire, into a throttling time. Conceptually, this evaluates the integral
 * of the underlying function we use, for the range of
 * [(storedPermits - permitsToTake), storedPermits].
 *
 * <p>This always holds: {@code 0 <= permitsToTake <= storedPermits}
 */
abstract long storedPermitsToWaitTime(double storedPermits, double permitsToTake);

存在两种策略就是为了应对我们上面讲到的,存在资源使用不足大致分为两种情况: (1).资源确实使用不足,这些剩余的资源我们私海可以使用的; (2).提供资源的服务过去还没准备好,比如服务刚启动等;

为此,RateLimiter实际上由两种实现策略,其实现分别见SmoothBursty和SmoothWarmingUp。二者主要的区别就是storedPermitsToWaitTime实现以及maxPermits数量的计算。

6.1 SmoothBursty

SmoothBursty使用storedPermits不需要额外等待时间。并且默认maxBurstSeconds未1,因此maxPermits为permitsPerSecond,即最多可以存储1秒的剩余令牌,比如QPS=5,则maxPermits=5.

下面这个RateLimiter的入口就是用来创建SmoothBursty类型的RateLimiter,

1
public static RateLimiter create(double permitsPerSecond)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/**
     * This implements a "bursty" RateLimiter, where storedPermits are translated to
     * zero throttling. The maximum number of permits that can be saved (when the RateLimiter is
     * unused) is defined in terms of time, in this sense: if a RateLimiter is 2qps, and this
     * time is specified as 10 seconds, we can save up to 2 * 10 = 20 permits.
     */
    static final class SmoothBursty extends SmoothRateLimiter {
        /** The work (permits) of how many seconds can be saved up if this RateLimiter is unused? */
        final double maxBurstSeconds;

        SmoothBursty(SleepingStopwatch stopwatch, double maxBurstSeconds) {
            super(stopwatch);
            this.maxBurstSeconds = maxBurstSeconds;
        }

        void doSetRate(double permitsPerSecond, double stableIntervalMicros) {
            double oldMaxPermits = this.maxPermits;
            maxPermits = maxBurstSeconds * permitsPerSecond;
            System.out.println("maxPermits=" + maxPermits);
            if (oldMaxPermits == Double.POSITIVE_INFINITY) {
                // if we don't special-case this, we would get storedPermits == NaN, below
                storedPermits = maxPermits;
            } else {
                storedPermits = (oldMaxPermits == 0.0)
                        ? 0.0 // initial state
                        : storedPermits * maxPermits / oldMaxPermits;
            }
        }

        long storedPermitsToWaitTime(double storedPermits, double permitsToTake) {
            return 0L;
        }
    }

一个简单的使用示意图及解释,下面私海一个QPS=4的SmoothBursty:

(1).t=0,这时候storedPermits=0,请求1个令牌,等待时间=0;
(2).t=1,这时候storedPermits=3,请求3个令牌,等待时间=0;
(3).t=2,这时候storedPermits=4,请求10个令牌,等待时间=0,超前使用了2个令牌;
(4).t=3,这时候storedPermits=0,请求1个令牌,等待时间=0.5;

代码的输出:

1
2
3
4
5
6
7
8
9
10
11
maxPermits=4.0, storedPermits=7.2E-4, stableIntervalMicros=250000.0, nextFreeTicketMicros=1472
acquire(1), sleepSecond=0.0

maxPermits=4.0, storedPermits=3.012212, stableIntervalMicros=250000.0, nextFreeTicketMicros=1004345
acquire(3), sleepSecond=0.0

maxPermits=4.0, storedPermits=4.0, stableIntervalMicros=250000.0, nextFreeTicketMicros=2004668
acquire(10), sleepSecond=0.0

maxPermits=4.0, storedPermits=0.0, stableIntervalMicros=250000.0, nextFreeTicketMicros=3504668
acquire(1), sleepSecond=0.499591

6.2 SmoothWarmingUp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
static final class SmoothWarmingUp extends SmoothRateLimiter {
        private final long warmupPeriodMicros;
        /**
         * The slope of the line from the stable interval (when permits == 0), to the cold interval
         * (when permits == maxPermits)
         */
        private double slope;
        private double halfPermits;

        SmoothWarmingUp(SleepingStopwatch stopwatch, long warmupPeriod, TimeUnit timeUnit) {
            super(stopwatch);
            this.warmupPeriodMicros = timeUnit.toMicros(warmupPeriod);
        }

        @Override
        void doSetRate(double permitsPerSecond, double stableIntervalMicros) {
            double oldMaxPermits = maxPermits;
            maxPermits = warmupPeriodMicros / stableIntervalMicros;
            halfPermits = maxPermits / 2.0;
            // Stable interval is x, cold is 3x, so on average it's 2x. Double the time -> halve the rate
            double coldIntervalMicros = stableIntervalMicros * 3.0;
            slope = (coldIntervalMicros - stableIntervalMicros) / halfPermits;
            if (oldMaxPermits == Double.POSITIVE_INFINITY) {
                // if we don't special-case this, we would get storedPermits == NaN, below
                storedPermits = 0.0;
            } else {
                storedPermits = (oldMaxPermits == 0.0)
                        ? maxPermits // initial state is cold
                        : storedPermits * maxPermits / oldMaxPermits;
            }
        }

        @Override
        long storedPermitsToWaitTime(double storedPermits, double permitsToTake) {
            double availablePermitsAboveHalf = storedPermits - halfPermits;
            long micros = 0;
            // measuring the integral on the right part of the function (the climbing line)
            if (availablePermitsAboveHalf > 0.0) {
                double permitsAboveHalfToTake = min(availablePermitsAboveHalf, permitsToTake);
                micros = (long) (permitsAboveHalfToTake * (permitsToTime(availablePermitsAboveHalf)
                        + permitsToTime(availablePermitsAboveHalf - permitsAboveHalfToTake)) / 2.0);
                permitsToTake -= permitsAboveHalfToTake;
            }
            // measuring the integral on the left part of the function (the horizontal line)
            micros += (stableIntervalMicros * permitsToTake);
            return micros;
        }

        private double permitsToTime(double permits) {
            return stableIntervalMicros + permits * slope;
        }
    }

maxPermits等于热身(warmup)期间能产生的令牌数,比如QPS=4,warmup为2秒,则maxPermits=8.halfPermits为maxPermits的一半.

参考注释中的神图:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 *          ^ throttling
 *          |
 * 3*stable +                  /
 * interval |                 /.
 *  (cold)  |                / .
 *          |               /  .   <-- "warmup period" is the area of the trapezoid between
 * 2*stable +              /   .       halfPermits and maxPermits
 * interval |             /    .
 *          |            /     .
 *          |           /      .
 *   stable +----------/  WARM . }
 * interval |          .   UP  . } <-- this rectangle (from 0 to maxPermits, and
 *          |          . PERIOD. }     height == stableInterval) defines the cooldown period,
 *          |          .       . }     and we want cooldownPeriod == warmupPeriod
 *          |---------------------------------> storedPermits
 *              (halfPermits) (maxPermits)
 *

下面是我们QPS=4,warmup为2秒时候对应的图。

maxPermits=8,halfPermits=4,和SmoothBursty相同的请求序列:

(1).t=0,这时候storedPermits=8,请求1个令牌,使用1个storedPermits消耗时间=1×(0.75+0.625)/2=0.6875秒;
(2).t=1,这时候storedPermits=8,请求3个令牌,使用3个storedPermits消耗时间=3×(0.75+0.375)/2=1.6875秒(注意已经超过1秒了,意味着下次产生新Permit时间为2.6875);
(3).t=2,这时候storedPermits=5,请求10个令牌,使用5个storedPermits消耗时间=1×(0.375+0.25)/2+4*0.25=1.3125秒,再加上额外请求的5个新产生的Permit需要消耗=5*0.25=1.25秒,即总共需要耗时2.5625秒,则下一次产生新的Permit时间为2.6875+2.5625=5.25,注意当前请求私海2.6875才返回的,之前一直阻塞;
(4).t=3,因为前一个请求阻塞到2.6875,实际这个请求3.6875才到达RateLimiter,请求1个令牌,storedPermits=0,下一次产生新Permit时间为5.25,因此总共需要等待5.25-3.6875=1.5625秒;

实际执行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
warmupPeriodMicros=2000000
stableIntervalMicros=250000.0, maxPermits=8.0, halfPermits=4.0, coldIntervalMicros=750000.0, slope=125000.0, storedPermits=8.0

maxPermits=8.0, storedPermits=8.0, stableIntervalMicros=250000.0, nextFreeTicketMicros=1524
acquire(1), sleepSecond=0.0

maxPermits=8.0, storedPermits=8.0, stableIntervalMicros=250000.0, nextFreeTicketMicros=1001946
acquire(3), sleepSecond=0.0

maxPermits=8.0, storedPermits=5.0, stableIntervalMicros=250000.0, nextFreeTicketMicros=2689446
acquire(10), sleepSecond=0.687186

maxPermits=8.0, storedPermits=0.0, stableIntervalMicros=250000.0, nextFreeTicketMicros=5251946
acquire(1), sleepSecond=1.559174

7.其他限流器

ASP.NET版本的一个比较成熟限流器:WebApiThrottle 参考:http://www.cnblogs.com/mushroom/archive/2015/07/21/4659200.html

参考: https://github.com/springside/springside4/wiki/Rate-Limiter

https://en.wikipedia.org/wiki/Token_bucket

https://en.wikipedia.org/wiki/Leaky_bucket

Linux文件编码

在多系统直接传输文件经常碰到文件编码的问题.

1.查看编码

查看文件的编码有很多种方式

1.1 file

file命令输出了文件的格式和编码.

1
2
xiaobaoqiu@xiaobaoqiu:~/octopress$ file _config.yml
_config.yml: UTF-8 Unicode text

1.2 vi

vi打开文件之后,输入:set fileencoding就可以获得文件编码:

1
fileencoding=utf-8

2.转换编码

2.1 vi

使用vi也可以变更文件编码

1
:set fileencoding=utf8

2.2 iconv

使用iconv命令,iconv几个主要参数(man):

1
2
3
4
5
6
7
8
9
10
11
12
输入/输出格式规范:
-f, --from-code=名称 原始文本编码
-t, --to-code=名称 输出编码

信息:
-l, --list 列举所有已知的字符集

输出控制:
-c 从输出中忽略无效的字符
-o, --output=FILE 输出到文件
-s, --silent 关闭警告
--verbose 打印进度信息

使用例子,文件infile从GB18030编码转换至UTF-8编码并写入到文件outfile中:

1
2
3
4
5
xiaobaoqiu@xiaobaoqiu:~/temp$ cat infile 
���
xiaobaoqiu@xiaobaoqiu:~/temp$ iconv -f GB18030 -t utf-8 infile -o outfile
xiaobaoqiu@xiaobaoqiu:~/temp$ cat outfile 
测试

也可以从管道输入,比如直接访问www.google.com.hk,不出意外会是乱码(使用的big5中文编码),可以转换成utf8

1
xiaobaoqiu@xiaobaoqiu:~/temp$ curl -l 'www.google.com.hk' | iconv -f big5 -t gbk

iconv基于GPL公开源代码,是GNU项目的一部分.目前,libiconv已经包含在C运行时刻库libc.so中。因此,Linux平台上使用iconv库函数的程序,需要包含<iconv.h>文件.

Linux Swap

最近一台机器出现swap报警,从监控图上也看到swap的使用一直存在.

服务器上只有一个Tomcat应用和一些定时任务.

1.什么是swap

inux操作系统将物理内存分为多个小的内存块,称之为页(pages). 当应用请求的物理内存不够分配时,操作系统会将一段时间之内不用的内存页交换至swap分区,从而为应用释放内存空间。

Swap对于系统过来说非常重要:

1.首先,当主内存不够用时,操作系统可以swap out一部分内存页,迅速为当前急需内存的应用或者进程分配内存;
2.其次,某些内存页只在应用初始化阶段用到,之后可能就不再使用了,操作系统可以将这些内存页swap out,从而为应用或者磁盘cache腾出更多的内存空间;

2.swap会带来哪些问题

我们知道,计算机磁盘I/O通常是系统的瓶颈所在。主内存的读写速度是纳秒级别,而磁盘读写速度是毫秒级别,两者相差3、4个数量级。然而,即使是当前广泛使用的SSD,读写速度相比主内存或者CPU cache也相差2、3个数量级。系统发生swap交换越多,那么系统自然也越慢。

特别对于web服务器来说,都是面对用户的交互式应用,因此响应速度尤其重要。如果系统经常因为swap交换而变得响应迟钝,那么用户体验效果可想而知。

总结成一句话:swap分区要有,在关键时刻不至于让你的应用因为内存不够用而被操作系统OOM KILLER干掉;但是不到关键时刻不要进行swap交换,因为这些操作会影响系统的响应速度。

关于swap的swap in和swap out可以从vmstat命令查看(si表示swap in, so表示swap out).参考: http://xiaobaoqiu.github.io/blog/2015/01/26/vmstatgong-ju/

3.查看swap占用

free命令, 参考: http://xiaobaoqiu.github.io/blog/2014/09/04/linux-memory-usage/

4.找到swap占用元凶

Linux系统中有一个文件smaps文件,记录了当前进程所对应的内存映像信息,路径为/proc/$pid/smaps.以本机的一个线程为例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
xiaobaoqiu@xiaobaoqiu:~/octopress$ sudo cat /proc/3555/smaps | head -16
00400000-00401000 r-xp 00000000 08:13 3444936                            /usr/local/jdk1.7.0_72/bin/java
Size:                  4 kB
Rss:                   4 kB
Pss:                   4 kB
Shared_Clean:          0 kB
Shared_Dirty:          0 kB
Private_Clean:         4 kB
Private_Dirty:         0 kB
Referenced:            4 kB
Anonymous:             0 kB
AnonHugePages:         0 kB
Swap:                  0 kB
KernelPageSize:        4 kB
MMUPageSize:           4 kB
Locked:                0 kB
VmFlags: rd ex mr mw me dw

其中Swap表示这个线程占有Swap的情况.

查看swap的占有情况的脚本,按照占用swap的占用多少从高到底排序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#!/bin/bash

function getswap {
SUM=0
OVERALL=0
for DIR in `find /proc/ -maxdepth 1 -type d | egrep "^/proc/[0-9]"` ; do
PID=`echo $DIR | cut -d / -f 3`
PROGNAME=`ps -p $PID -o comm --no-headers`
for SWAP in `grep Swap $DIR/smaps 2>/dev/null| awk '{ print $2 }'`
do
let SUM=$SUM+$SWAP
done
echo "PID=$PID    Swap used(KB): $SUM    ($PROGNAME )"
let OVERALL=$OVERALL+$SUM
SUM=0

done
echo "Overall swap used: $OVERALL"
}

getswap|sort -k4nr

然后运行:

1
sudo ~/check_swap.sh

5.清除被占用的swap

在我们明确知道哪些进程吃swap以后,接下来的问题就是我们如何释放这些swap,释放swap的意思就是把交换到swap中的数据swap in到物理内存页中。

1.重启吃swap的服务,比如重启一下我们的java进程;
2.swapoff + swapon
这个方法的好处是,不用重启服务,但是需要确保现在有足够的物理内存可以容下从swap中释放出来的数据。下面给出了swapoff和swapon的具体做法,注意看swapoff后和swapon后,free的输出有什么异同;

sudo /sbin/swapoff -a
sudo /sbin/swapon -a

swapoff后,free的输出里,swap分区的大小变为0,占用变为0,也就是说swap分区中的数据已经释放到物理内存中,同时swap分区被禁用。swapon后,free的输出里,swap分区的容量又恢复了
,也就是说swap分区重新被启用了。当然我们可以把这两个命令写到一起:

sudo /sbin/swapoff -a && sudo /sbin/swapon -a

Mysql InnoDB锁和死锁

在使用Mysql的业务中,经常会碰到各种Mysql的死锁.一直以来,都对Mysql的死锁不甚了解,这次我们的发布中也出现了一次死锁,趁这次机会,好好学习一下Mysql的死锁.我们的死锁的讨论是在InnoDB引擎基础上的.

1.MySQL索引

1.1 聚簇索引(Clustered Indexes)

InnoDB存储引擎的数据组织方式,是聚簇索引表:完整的记录,存储在主键索引中,通过主键索引,就可以获取记录所有的列.

每个InnoDB的表有一个特殊的索引称之为聚簇索引,每行的数据就是存储在聚簇索引中.通常,聚簇索引和主键同义.

当你在你的表上面定义一个主键时,InnoDB将其作为聚簇索引.建议为你的表都创建一个主键.如果没有唯一并且非空的一列或者多列(用来做你的主键),那么可以创建一个自动填充的自增列(比如ID)

如果你的表没有定义主键,MySQL会将第一个所有列都非空的UNIQUE索引作为聚簇索引.

如果你的表不存在这样的UNIQUE索引(见上),InnoDB内部会自动隐式生成一个包含行ID的列并在其上面建立聚簇索引.这一列按行ID排序.行ID是一个6-byte的严格单调自增的字段.因此,按照行在物理上是按照插入顺序排序的.

聚簇所有是如何加速查询的呢?通过聚簇所有访问一行非常快,这是因为在聚簇索引上搜索会直接定位到包含你需要的行的数据所在的页上(page).

参考: http://dev.mysql.com/doc/refman/5.6/en/innodb-index-types.html

1.2 二级索引(Secondary Indexes)

除了聚簇索引其他索引都是二级索引.在InnoDB中每个二级索引记录都包含了这一行的主键列和当前这个二级索引包含的列.InnoDB使用二级索引中包含的主键取索引这一行对应的聚簇索引,进而找到这一行完整的数据.

如果主键很长,则二级索引会占有更多的空间,因此建议使用短的列做主键.

2.MySQL锁

Innodb具备表锁和行锁,其中表锁是MySQL提供的,跟存储引擎无关;行锁是Innodb存储引擎实现.

2.1 共享锁和排他锁

1.共享锁(S)
允许一个事务去读一行,阻止其他事务获得相同数据集的排他锁.
2.排他锁(X)
允许获得排他锁的事务更新数据,阻止其他事务取得相同数据集的共享读锁和排他写锁.

另外,为了允许行锁和表锁共存,实现多粒度锁机制,InnoDB还有两种内部使用的意向锁(Intention Locks),这两种意向锁都是表锁.

1.意向共享锁(IS)
事务打算给数据行加行共享锁,事务在给一个数据行加共享锁前必须先取得该表的IS锁.
2.意向排他锁(IX)
事务打算给数据行加行排他锁,事务在给一个数据行加排他锁前必须先取得该表的IX锁.

上面这四种锁的兼容性Conflict表示冲突不能共存,Compatible表示可以共存:

   |     X     |      IX     |      S     |  IS
X  | Conflict  |  Conflict   | Conflict   | Conflict
IX | Conflict  |  Compatible | Conflict   | Compatible
S  | Conflict  |  Conflict   | Compatible | Compatible
IS | Conflict  |  Compatible | Compatible | Compatible

2.2 什么时候会加锁

1.共享锁(S)
    SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE
2.排他锁(X)
    SELECT * FROM table_name WHERE ... FOR UPDATE

参考: http://dev.mysql.com/doc/refman/5.6/en/innodb-lock-modes.html

2.3 当前请求锁

使用show engine innodb status命令查看当前请求锁的信息.

1
mysql> show engine innodb status \G;

可以从information_schema.INNODB_LOCKS表中查看锁的信息. 比如事物A:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
mysql> select * from t;
+----+------+
| id | num  |
+----+------+
| 10 |    0 |
|  1 |    1 |
|  3 |    4 |
|  2 |    7 |
| 11 |    8 |
|  4 |    9 |
+----+------+
6 rows in set (0.00 sec)

mysql> select * from t where num < 4 lock in share mode;
+----+------+
| id | num  |
+----+------+
| 10 |    0 |
|  1 |    1 |
+----+------+
2 rows in set (0.00 sec)

然后事物B去做插入就可能被阻塞:

1
mysql> insert into t(num) values(4);

这是很可以查询锁信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
mysql> select * from information_schema.INNODB_LOCKS \G;
*************************** 1. row ***************************
    lock_id: 5E05:0:138228:3
lock_trx_id: 5E05
  lock_mode: X,GAP
  lock_type: RECORD
 lock_table: `test`.`t`
 lock_index: `n`
 lock_space: 0
  lock_page: 138228
   lock_rec: 3
  lock_data: 7, 2
*************************** 2. row ***************************
    lock_id: 5E02:0:138228:3
lock_trx_id: 5E02
  lock_mode: S
  lock_type: RECORD
 lock_table: `test`.`t`
 lock_index: `n`
 lock_space: 0
  lock_page: 138228
   lock_rec: 3
  lock_data: 7, 2

2.2 锁的算法(Record Lock,Gap Lock,Next-Key Lock)

InnoDB有三种类型的行锁:record locks,gap locks和next-key locks:索引锁是在单个索引记录上的锁;区间锁是两个索引记录之间的锁,或者第一个索引之前的锁,或者最后一个索引之后的锁;Next-Key锁是索引锁和该索引之前的gap锁的结合.

1.索引锁(Record Lock)
索引锁总是锁定索引(可能多条),即使表上面没有索引(这种情况,InnoDB会隐式的用自增id创建一个聚簇索引).一级索引只对一级索引加锁,二级索引对二级索引和对应的一级索引加锁.注意记录锁锁的是索引记录,不是具体的数据记录.

2.区间锁(Gap Lock)
锁定索引记录间隙的锁,确保索引记录的间隙不变,间隙锁是针对事务隔离等级是可重复读(Repeatable Read)或以上级别而言的.

间隙锁一般是针对非唯一索引而言的

3.Next-Key Lock
默认情况,InnoDB使用REPEATABLE READ事物隔离级别,并且innodb_locks_unsafe_for_binlog这个系统设置无效.这时InnoDB使用next-key锁来做搜索(searches)和索引扫描(index scans),以此来防止幻读(参考:http://dev.mysql.com/doc/refman/5.6/en/innodb-next-key-locking.html).

2.3 区间锁

区间锁的一个简单例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
CREATE TABLE `t` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增主键',
  `num` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `n` (`num`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8

insert into t(`num`) values(1, 7, 4, 9);

mysql> select * from t;
+----+------+
| id | num  |
+----+------+
|  1 |    1 |
|  3 |    4 |
|  2 |    7 |
|  4 |    9 |
+----+------+
4 rows in set (0.01 sec)

表中现在有4条记录,其中普通索引(二级索引n)生成了5个Gap:

1
(负无穷, 1), (1, 4), (4, 7), (7, 9), (9, 正无穷)

现在Session A以共享锁获取num=4的数据,Session B想要插入数据,就有可能造成锁等待导致超时从而重启事务,因为Session A以共享锁获取num=4的数据,会产生gap锁将区间(1, 4)和区间(4, 7)锁住,因此这两个区间的插入会失败:

间隙锁在InnoDB的作用就是防止其它事务的插入操作,以此来达到防止幻读的发生。另外,在上面的例子中,我们选择的是一个普通(非唯一)索引字段来测试的,这不是随便选的,因为如果InnoDB扫描的是一个主键、或是一个唯一索引的话,那InnoDB只会采用行锁方式来加锁,而不会使用Next-Key Lock的方式,也就是说不会对索引之间的间隙加锁.

要禁止间隙锁的话,可以把隔离级别降为读已提交,或者开启参数innodb_locks_unsafe_for_binlog.

参考: http://www.cnblogs.com/sliverdang/p/3163455.html

http://ouyanggod.iteye.com/blog/2166215

http://dev.mysql.com/doc/refman/5.6/en/innodb-record-level-locks.html

3.snapshot read和current read

MySQL的两种read方式:

1.快照读(snapshot read或者consistent read)
快照读,读取的是记录的可见版本(有可能是历史版本),不用加锁;

通常,简单的select操作,属于快照读,不加锁,比如:
```
select * from table where ?
```
2.当前读(current read或者lock read)
当前读,读取的是记录的最新版本,并且,当前读返回的记录,都会加上锁,保证其他事务不会再并发修改这条记录.

特殊的读操作,插入/更新/删除操作,属于当前读,需要加锁.比如:
```
select * from table where ? lock in share mode
select * from table where ? for update
insert into table values (…)
update table set ? where ?
delete from table where ?
```
所有以上的语句,都属于当前读,读取记录的最新版本.并且,读取之后,还需要保证其他并发事务不能修改当前记录,对读取记录加锁.其中,除了第一条语句,对读取记录加S锁 (共享锁)外,其他的操作,都加的是X锁(排它锁).

总之一句话:有加锁的查询都认为是当前读.

快照读大大的提高了数据读取的并发.快照读的一个简单示意图,快照数据就是当前数据之前的版本数据,可能有多个版本的快照数据,每个快照数据中包含了版本信息(如时间戳等):

为什么将插入/更新/删除操作,都归为当前读?可以看看下面这个更新操作,在数据库中的执行流程:

从图中,可以看到,一个Update操作的具体流程.当Update SQL被发给MySQL后,MySQL Server会根据where条件,读取第一条满足条件的记录,然后InnoDB引擎会将第一条记录返回,并加锁(current read).待MySQL Server收到这条加锁的记录之后,会再发起一个Update请求,更新这条记录.一条记录操作完成,再读取下一条记录,直至没有满足条件的记录为止.因此,Update操作内部,就包含了一个当前读.同理,Delete操作也一样.Insert操作会稍微有些不同,简单来说,就是Insert操作可能会触发Unique Key的冲突检查,也会进行一个当前读.

注:根据上图的交互,针对一条当前读的SQL语句,InnoDB与MySQL Server的交互,是一条一条进行的,因此,加锁也是一条一条进行的.先对一条满足条件的记录加锁,返回给MySQL Server,做一些DML操作;然后在读取下一条加锁,直至读取完毕.

3.1不同隔离界别下的snapshot read

在Read Committed级别下,快照读总是读取被锁定行的最新的快照数据.而在Repeatable Read和Serializable级别,快照读读取的是事物开始时候的行数据版本.

下面是一个简单的例子,一个很简单的表,插入一条数据:

1
2
3
CREATE TABLE `parent` (   `id` int(10) NOT NULL,   PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;

insert into parent (id) values(1);

我们起两个事务,一个读取(Session A),一个更新(Session B),用来验证不同事务隔离级别下快照读的差异:

1.Session A中首先开始事物,查询id=1的数据,这时候,无论在Read Committed还是Repeatable Read级别,结果都是1;
2.Session B然后开始事物,并执行update操作,没有commit;
3.这时候Session A再查询id=1的数据,显然Read Committed还是Repeatable Read级别,结果都是1;(在Read Uncommited灰度到未提交的脏数据).
4.Session B提交事物;
5.这时候Session A再查询id=1的数据,就发现差异:Read Committed级别下读取到被修改的数据,而Repeatable Read读取的还是老数据.因为Read Committed只读取最新的快照数据,而Repeatable Read是参考当前事物开始时间来读取快照数据.

首先是Repeatable Read的结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
mysql> SELECT @@session.tx_isolation;
+------------------------+
| @@session.tx_isolation |
+------------------------+
| REPEATABLE-READ        |
+------------------------+

mysql> select * from parent where id = 1;
+----+
| id |
+----+
|  1 |
+----+

下面是Read Committed的结果(Session B一旦提交,Session A未commit的情况下就能读到Session B提交的数据.):

1
2
3
4
5
6
7
8
9
10
11
12
13
mysql> SELECT @@session.tx_isolation;
+------------------------+
| @@session.tx_isolation |
+------------------------+
| READ-COMMITTED         |
+------------------------+

mysql> select * from parent where id = 1;
+----+
| id |
+----+
|  1 |
+----+

4.InnoDB MVCC

InnoDB是一种多版本存储引擎,它必须保存各行老版本信息,这个信息存在一个称之为回滚段(rollback segment)的数据结构中.

在Mysql内部,InnoDB为每行数据额外增加三个字段:

1.一个6-byte的名为DB_TRX_ID字段,用来表示最后一个插入(insert)或者更新(update)这行记录的事物的标记.注意,删除(delete)也被当成一种更新,只是标记这一行的一个额外的bit位来表征这个数据被删除.
2.一个7-byte的名为DB_ROLL_PTR字段,称之为回滚指针(roll pointer).这个指针指向写在rollback segment中的undo log记录.如果这一行被更新了,undo log就包含了能够将这一行完全恢复到修改之前的信息.
3.一个6-byte的DB_ROW_ID字段,用来存行id(row ID),行id是插入数据的时候自动严格递增生成的.如果InnoDB自动产生了一个聚簇索引(clustered index),这个索引就包含行id.否在行id就不会在任何索引中出现.

InnoDB使用存储在rollback segment中的信息(undo log)去实现事物回滚时候的undo操作.另外,InnoDB也是使用这个信息构建快照读(consistent read或者snapshot read)时候的行数据.

rollback segment中的Undo logs分为插入(insert)和更新(update)的undo logs.

经常提交你的事物,包括那些只是consistent reads的事物.否则(长时间不提事物)会导致InnoDB不能及时废弃update undo logs中的数据,进而导致rollback segment中数据太大挤占你的表空间(tablespace).

rollback segment中undo log记录的物理大小(physical size)通常小于对应的插入或者更新的行数.你可以使用这个信息取计算你的rollback segment需要的空间.

在InnoDB多版本方案中,当你删除一行记录,实际上行不会立即被物理删除.只有当这个删除对应的update undo log被废弃的时候这行记录才会真正被物理删除.此删除操作被称为清除(purge)是通过Purge后台进程实现的,这个过程非常的快,通常其顺序和SQL语句执行删除的顺序一致.Purge进程定期扫描InnoDB的undo,按照先读老undo,再读新undo的顺序,读取每条undo record.

参考: http://hedengcheng.com/?p=148 https://dev.mysql.com/doc/refman/5.0/en/innodb-multi-versioning.html

5.隔离级别(Isolation Level)

5.1 InnoDB的4种隔离级别

MySQL InnoDB定义的4种隔离级别:

1.Read Uncommited
2.Read Committed (RC)
3.Repeatable Read (RR)
4.Serializable

Read Uncommited安全级别比较低,因此很少使用.Serializable隔离级别读写冲突,因此并发度急剧下降,在MySQL/InnoDB下不建议使用. Repeatable Read是InnoDB默认的事物级别.Oracle和MS SQL的默认级别是Read Committed.

5.2脏读,不可重复读,幻读

在事务并行下出现的几个问题:

1.脏读
可能读取到其他会话中未提交事务修改的数据,在Read Uncommited级别下可能出现.
2.不可重复读
同一个事物中前后两次读取的内容不一致,在Read Uncommited和Read Committed会出现.
3.幻读
如果另一个事务同时提交了新数据(本事务查询时候感知不到这个变更),本事务再更新时,就会惊奇的发现了这些新数据(比如触发违反了uniq key等),就好像之前读到的数据是鬼影一样的幻觉.这种情况就是上述说的,快照读和当前读一起存在的情况,会出现幻读的场景.必须使用当前读,才能避免幻读.比如:select ...lock in share mode和select ...for update.

各个事物界别下可能出现的问题:

隔离级别 脏读(Dirty Read) 不可重复读(NonRepeatable Read) 幻读(Phantom Read)
Read Uncommited 可能 可能 可能
Read Committed 不可能 可能 可能
Repeatable Read 不可能 不可能 可能
Serializable 不可能 不可能 不可能

幻读的一个示例,Session A在insert之前先select查看数据是否存在,结果告知可以插入,这时候Session B变更数据并提交.Session A再插入会因为主键冲突失败:

1
2
3
4
5
6
7
8
9
10
11
12
13
mysql> SELECT @@session.tx_isolation;
+------------------------+
| @@session.tx_isolation |
+------------------------+
| READ-COMMITTED         |
+------------------------+

mysql> select * from parent where id = 1;
+----+
| id |
+----+
|  1 |
+----+

那么,InnoDB指出的可以避免幻读是怎么回事呢?

http://dev.mysql.com/doc/refman/5.6/en/innodb-record-level-locks.html

By default, InnoDB operates in REPEATABLE READ transaction isolation level and with the innodb_locks_unsafe_for_binlog system variable disabled. In this case, InnoDB uses next-key locks for searches and index scans, which prevents phantom rows (see Section 13.6.8.5, “Avoiding the Phantom Problem Using Next-Key Locking”).

简单翻译就是,当隔离级别是可重复读,且禁用innodb_locks_unsafe_for_binlog的情况下,在搜索和扫描index的时候使用的next-key locks可以避免幻读.

关键点在于,是InnoDB默认对一个普通的查询也会加next-key locks,还是说需要应用自己来加锁呢?如果单看这一句,可能会以为InnoDB对普通的查询也加了锁,如果是,那和序列化(SERIALIZABLE)的区别又在哪里呢?

MySQL manual里还有一段:

http://dev.mysql.com/doc/refman/5.6/en/innodb-next-key-locking.html
Avoiding the Phantom Problem Using Next-Key Locking

To prevent phantoms, InnoDB uses an algorithm called next-key locking that combines index-row locking with gap locking.

You can use next-key locking to implement a uniqueness check in your application: If you read your data in share mode and do not see a duplicate for a row you are going to insert, then you can safely insert your row and know that the next-key lock set on the successor of your row during the read prevents anyone meanwhile inserting a duplicate for your row. Thus, the next-key locking enables you to “lock” the nonexistence of something in your table.

根据这一段,我们可以理解为,InnoDB提供了next-key locks,但需要应用程序自己去加锁,才能防止幻读.manual里提供一个例子:

SELECT * FROM child WHERE id > 100 FOR UPDATE;

这样,InnoDB会给id大于100的行(假如child表里有一行id为102),以及100-102,102+的gap都加上锁.可以使用show innodb status来查看是否给表加上了锁.

结论就是:MySQL InnoDB的REPEATABLE READ并不保证避免幻读,需要应用使用加锁读来保证.而这个加锁度使用到的机制就是next-key locks.

5.3 修改隔离级别

InnoDB默认是可重复读的(REPEATABLE READ).可以在命令行用–transaction-isolation选项,或在选项文件里,为所有连接设置默认隔离级别.

在my.inf文件的[mysqld]节里类似如下设置该选项:

1
transaction-isolation = {READ-UNCOMMITTED | READ-COMMITTED | REPEATABLE-READ | SERIALIZABLE}

用户可以用SET TRANSACTION语句改变单个会话或者所有新进连接的隔离级别:

1
SET [SESSION | GLOBAL] TRANSACTION ISOLATION LEVEL {READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE}

6.死锁

1.死锁发生的条件
互斥条件:一个资源每次只能被一个进程使用;
请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放;
不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺;
循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

系统检测到死锁后,会自动回滚其中事务较小的一个(记得好像根据undo日志的大小来决定).

对于DB而言,导致死锁意味着发生了循环等待,在InnoDB中由于行锁的引入,比较容易发生死锁,下面总结一些发生死锁的情况(不全): 1. 同一索引上,两个session相反的顺序加锁多行记录; 2. Primary key和Secondary index,通过primary key找到记录,更新Secondary index字段与通过Secondary index更新记录; 3. UPDATE/DELETE通过不同的二级索引更新多条记录,可能造成在Primary key上不同的加锁顺序,可以参考之前一篇博客:http://www.gpfeng.com/?p=406

6.1死锁实例

6.2查看死锁信息

6.3解决死锁

参考:

http://hedengcheng.com/?p=771

http://dev.mysql.com/doc/refman/5.6/en/innodb-record-level-locks.html

http://novoland.github.io/%E6%95%B0%E6%8D%AE%E5%BA%93/2014/07/26/MySQL%E6%80%BB%E7%BB%93.html

http://wiki.corp.qunar.com/pages/viewpage.action?pageId=76820049 http://wiki.corp.qunar.com/pages/viewpage.action?pageId=50520113

Mysql锁基础知识 http://wiki.corp.qunar.com/pages/viewpage.action?pageId=64807317