最近无意中看了几篇讨论分布式锁的博文,有基于数据库的,有基于Redis的,也有基于zookeeper的。于是自己也花了点时间去研究。下面记录一下自己的研究成果。
综合了各种设计和自己的分析,设计了一种基于Redis的分布式锁。该锁主要利用了Redis的三个API,分别是setnxgetsetget

  • setnx的功能是

    将 key 的值设为 value ,当且仅当 key 不存在。
    若给定的 key 已经存在,则 SETNX 不做任何动作

  • getset的功能是

    将给定 key 的值设为 value ,并返回 key 的旧值(old value)。
    当 key 存在但不是字符串类型时,返回一个错误。

锁的机制如下:

  1. 利用setnx(key, currentSysTime + timeOut),如果返回1,说明获取锁成功,否则进行下一步
  2. oldLockExpireTime = get(key),如果oldLockExpireTime < currentSysTime,说明锁已经过期,那么其他的线程就可以尝试获取该锁
  3. 记新的锁的过期时间为newLockExpireTime = currentSysTime + timeOut
  4. 利用getset(key, newLockExpireTime)获取原锁的过期时间记为currentLockExpireTime并将锁的新的过期时间设为newLockExpireTime
  5. 如果currentLockExpireTimeoldLockExpireTime相等,则说明步骤2到4之间,没有其他线程抢占了该锁,那么当前线程获取锁成功,否则回到步骤1
  6. 线程获取到锁后进行业务处理,完成后将当前系统时间和锁过期时间比较,如果小于锁过期时间,那么则利用del(key)删除锁

重点需要说明的是,这个设计利用getset来保证在一些场景下不会有多个线程同时获取到锁,比如步骤2多个线程都发现锁已经过期,于是去获取该锁,在获取锁的过程中即步骤2-4,利用getset返回的原锁的过期时间和步骤2中的oldLockExpireTime比较是否相等,来判断当前线程正在获取的锁是否仍然是步骤2get(key)的那把锁,这样就能够避免多个线程同时获取到锁的错误的情况。整个设计较好的利用了Redis的单线程串行执行的特性。

流程图如下
lock

按照这个设计思路,简单实现了demo性质的锁,并模拟一个商品秒杀场景进行验证。
demo中利用jedis来与Redis-server进行交互。

先看锁的实现类DistributedRedisLock类,代码的逻辑完全按照上述逻辑编写,分别有getLockreleaseLock方法

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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
package me.lijf;

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.exceptions.JedisException;

/**
* Created by lijf on 2017/6/5.
*/

public class DistributedRedisLock {

private final JedisPool jedisPool;

public DistributedRedisLock(JedisPool jedisPool) {
this.jedisPool = jedisPool;
}

public LockInfo getLock(String key, long timeOut) {
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
while (true) {
String lockExpireTime = String.valueOf(System.currentTimeMillis() + timeOut);
if (jedis.setnx(key, lockExpireTime) == 1) {
LockInfo lockInfo = new LockInfo();
lockInfo.setExpireTime(jedis.get(key));
lockInfo.setIsGetLock(true);
return lockInfo;
} else {
String oldLockExpireTime = jedis.get(key);
if (oldLockExpireTime != null && Long.parseLong(oldLockExpireTime) < System.currentTimeMillis()) {
String currentLockExpireTime = jedis.getSet(key, String.valueOf(System.currentTimeMillis() + timeOut));
if (currentLockExpireTime != null && oldLockExpireTime.equals(currentLockExpireTime)) {
LockInfo lockInfo = new LockInfo();
lockInfo.setExpireTime(String.valueOf(newLockExpireTime));
lockInfo.setIsGetLock(true);
return lockInfo;
}
}
}

Thread.sleep(10);
}
} catch (InterruptedException e) {
e.printStackTrace();
} catch (JedisException e) {
e.printStackTrace();
} finally {
jedis.close();
}

return new LockInfo();
}

public void releaseLock(String key, String time) {
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
if (System.currentTimeMillis() < Long.parseLong(time)) {
jedis.del(key);
}
} catch (JedisException e) {
e.printStackTrace();
} finally {
jedis.close();
}
}
}

下面是锁的获取结果封装类LockInfo

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
package me.lijf;

/**
* Created by lijf on 2017/6/5.
*/

public class LockInfo {

private String expireTime; // 锁过期时间

private Boolean isGetLock; // 是否获取到锁

public LockInfo() {
this.isGetLock = false;
}

public String getExpireTime() {
return expireTime;
}

public void setExpireTime(String expireTime) {
this.expireTime = expireTime;
}


public Boolean getIsGetLock() {
return isGetLock;
}

public void setIsGetLock(Boolean isGetLock) {
this.isGetLock = isGetLock;
}
}

模拟秒杀场景的SeckillService,商品数量-1代表库存减少1件

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
package me.lijf;

import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

/**
* Created by lijf on 2017/6/5.
*/

public class SeckillService {

private static JedisPool jedisPool;

static {
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
jedisPoolConfig.setMaxIdle(10);
jedisPoolConfig.setMaxTotal(200);
jedisPoolConfig.setMaxWaitMillis(1000 * 100);
jedisPoolConfig.setTestOnBorrow(true);
jedisPool = new JedisPool(jedisPoolConfig, "127.0.0.1", 6379, 3000);
}

DistributedRedisLock lock = new DistributedRedisLock(jedisPool);

int productNum = 10;

String productName = "iPhone 7";

public void seckill() {
LockInfo lockInfo = lock.getLock(productName, 10);
String expireTime = lockInfo.getExpireTime();
if (lockInfo.getIsGetLock() && productNum > 0) {
System.out.println(Thread.currentThread().getName() + " successfully buy an iPhone 7");
System.out.println("products remain " + --productNum);
lock.releaseLock(productName, expireTime);
} else if (productNum == 0) {
System.out.println(Thread.currentThread().getName());
System.out.println("sold out");
lock.releaseLock(productName, expireTime);
}
}
}

测试类LockTest

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
package me.lijf;

/**
* Created by lijf on 2017/6/5.
*/


class TestThread extends Thread {

private SeckillService seckillService;

public TestThread(SeckillService seckillService) {
this.seckillService = seckillService;
}

@Override
public void run() {
seckillService.seckill();
}
}

public class LockTest {

public static void main(String[] args) {
SeckillService seckillService = new SeckillService();

for (int i = 0; i < 100; ++i) {
TestThread testThread = new TestThread(seckillService);
testThread.start();
}
}
}

全部代码如上。可以看到,代码中模拟了一个100个用户秒杀10iPhone 7的业务场景。如果我们的锁能正确工作,库存的减少应该是有序的,直至商品库存为0
来看下测试的部分结果

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
Thread-81 successfully buy an iPhone 7
products remain 9
Thread-48 successfully buy an iPhone 7
products remain 8
Thread-44 successfully buy an iPhone 7
products remain 7
Thread-46 successfully buy an iPhone 7
products remain 6
Thread-75 successfully buy an iPhone 7
products remain 5
Thread-57 successfully buy an iPhone 7
products remain 4
Thread-56 successfully buy an iPhone 7
products remain 3
Thread-47 successfully buy an iPhone 7
products remain 2
Thread-61 successfully buy an iPhone 7
products remain 1
Thread-51 successfully buy an iPhone 7
products remain 0
Thread-27
sold out
Thread-84
sold out
Thread-52
sold out
Thread-41
sold out
Thread-25
sold out
Thread-34
sold out

由此可见,我们的锁起到了作用。经过多组测试,该锁都能够正常工作。但是timeOut不能设置的过小,因为非业务代码本身也需要执行时间。

总结一下,这个demo还是比较粗糙的,本文也仅仅是介绍了一种思路。但我认为这种基于Redis的分布式锁,能够应付一般的业务场景了。