本文讲述,如何使用redis来实现分布式锁。这种实现方式,满足了分布式锁系列–01分布式锁入门介绍一文中,分布式锁约束的前三条:互斥性,安全性,对称性。因为是单机版本,所有无法满足第四条。自己编码来实第四点,是比较麻烦的,后面会介绍如何使用开源的Redisson框架来实现分布式锁。

实现原理

有一个redis服务实例,在分布式系统中,所有需要获取锁的客户端,都需要访问这个redis实例:

如果锁不存在,则写入key-value格式的数据,并设定过期时间,这个value,是为了保证解锁时,不会误解别人的锁,过期时间,是为了保证,万一加锁后客户端挂掉,解锁失败,当过期时间到了,redis会自动删除该锁,防止死锁。

如果锁已经存在,则说明已经有其它客户端持有该锁,可等待其释放(key-value 被主动删除或者因过期而被动删除)再尝试获取锁。

释放锁时,删除该key-value即可,需要确保删除的是自己加的锁,且使用watch机制和事务,来确保删除锁操作的原子性。

本文主要分为以下几个步骤实现:
  • 1.pom.xml引入依赖
  • 2.JedisManager管理JedisPool
  • 3.RedisDistributedLock分布式锁工具类
  • 4.测试代码

    1.pom.xml引入依赖

1
2
3
4
5
6
7
8
9
10
11
<!-- redis -->
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
</dependency>
<!--jedis-->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>

2.JedisManager管理JedisPool

自定义JedisManager类,用于管理jedisPool,配置参数可以根据自己需要做修改,这个连接池的管理和配置,可以自己实现,这里只是一个参考:

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
public class JedisManager {

private static final Logger logger = LoggerFactory.getLogger(ZookeeperClient.class);
private static final String REDIS_HOST = "xx.xx.xx.xx";
private static final Integer REDIS_PORT = 6379;
private static JedisPool jedisPool;
private static JedisPoolConfig config = new JedisPoolConfig();

private static void init(){
config.setMaxTotal(1000);
config.setMaxIdle(10);
config.setMaxWaitMillis(10*1000);
config.setTestOnBorrow(true);//borrow一个jedis实例时,是否提前进行validate操作;如果为true,则得到的jedis实例均是可用的;
config.setTestOnReturn(true);//return一个jedis实例给pool时,是否检查连接可用,设置为true时,返回的对象如果验证失败,将会被销毁,否则返回
}

public static JedisPool getJedisPool(){
if(null == jedisPool){
synchronized (JedisPool.class){
if(null == jedisPool){
init();
jedisPool = new JedisPool(config, REDIS_HOST, REDIS_PORT, 3000,"password");
logger.info("【Redis lock】jedisPool初始化成功......");
return jedisPool;
}
}
}
return jedisPool;
}
}

3.RedisDistributedLock分布式锁工具类

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
69
70
71
72
public class RedisDistributedLock {
private static final Logger logger = LoggerFactory.getLogger(ZookeeperClient.class);
private static final String LOCK_SUCCESS = "OK";
private static final Integer RELEASE_SUCCESS = 1;
private static final String SET_IF_NOT_EXIST = "NX";
private static final String SET_WITH_EXPIRE_TIME = "PX";
private static JedisPool jedisPool = JedisManager.getJedisPool();

/**
* 加锁
*
* @param lockName 锁名,对应被争用的共享资源
* @param randomValue 随机值,需要保持全局唯一,便于释放时校验锁的持有者
* @param expireTime 过期时间,到期后自动释放,防止出现问题时死锁,资源无法释放
* @return
*/
public static boolean acquireLock(String lockName,String randomValue,int expireTime){
Jedis jedis = jedisPool.getResource();
try {
while (true){
String result = jedis
.set(lockName, randomValue, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
if(LOCK_SUCCESS.equals(result)){
logger.info("【Redis lock】success to acquire lock for [ "+lockName+" ],expire time:"+expireTime+"ms");
return true;
}
}
}catch (Exception ex){
ex.printStackTrace();
}finally {
if(null != jedis){
jedis.close();
}
}
logger.info("【Redis lock】failed to acquire lock for [ "+lockName+" ]");
return false;
}

/**
* redis释放锁
* watch和muti命令保证释放时的对等性,防止误解锁
*
* @param lockName 锁名,对应被争用的共享资源
* @param randomValue 随机值,需要保持全局唯一,以检验锁的持有者
* @return 是否释放成功
*/
public static boolean releaseLock(String lockName,String randomValue){
Jedis jedis = jedisPool.getResource();
try{
jedis.watch(lockName);//watch监控
if(randomValue.equals(jedis.get(lockName))){
Transaction multi = jedis.multi();//开启事务
multi.del(lockName);//添加操作到事务
List<Object> exec = multi.exec();//执行事务
if(RELEASE_SUCCESS.equals(exec.size())){
logger.info("【Redis lock】success to release lock for [ "+lockName+" ]");
return true;
}
}
}catch (Exception ex){
logger.info("【Redis lock】failed to release lock for [ "+lockName+" ]");
ex.printStackTrace();
}finally {
if(null != jedis){
jedis.unwatch();
jedis.close();
}
}
return false;
}

}

4.测试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/***
* 测试redis分布式锁
* @param id
* @param stock
*/
@Override
public void updateStockById1(Integer id, Integer stock) {
System.out.println("---------->"+Thread.currentThread().getName());

String randomValue = id+ UUID.randomUUID().toString();//随机值,确保全局唯一
RedisDistributedLock.acquireLock(id.toString(),randomValue,5*1000);//加锁

//业务逻辑
ProductStock product = productStockDao.getById(id);
Integer stock1 = product.getStock();
stock1 = stock1+stock;
productStockDao.updateStockById(id,stock1);

RedisDistributedLock.releaseLock(id.toString(),randomValue);//释放锁
}

测试的方式是,我们有一个产品库存,调用接口时,会去更新库存+1,如果没有加锁,并发情况下,库存会和逾期值差距很大,这里可以用jmter模拟并发来做测试。(也可以启两个服务,做个负载均衡,这样模拟更加真实,后面会把这种测试方式的代码上传到github)
解锁建议放在try catch finally的finally中来释放。