前言

为什么Redis可以做分布式锁

  • 首先还是先看下分布式锁的概念,锁是在一个资源有多个访问者同时操作的情况为了保证数据正确性而出现的(比如食堂打饭场景,如果任由学生自己来打菜打饭就会乱,所以要打菜大妈来维护秩序大家排好队呀),还比如我们Java语言的synchronized关键字和lock也是锁的实现

  • synchronized关键字是单体环境下是有效的,现在我们项目都流行使用分布式服务作为服务架构,所以这种情况下synchronized这种锁就不满足条件了

  • 针对上面这种情况,可以使用第三方中间件技术来管理锁,所以就出现了如下分布式锁的实现,下面的中间件对分布式锁的实现提供了很好的支持,当然你也可以使用mysql或者其他文件存储系统来维护锁的状态,当然加锁呀释放锁这些逻辑就需要自己研发了

    • Redis分布式锁
    • Zookeeper分布式锁

Redis分布式锁实现原理

加锁

  • 最简单的方法是使用setnx + expire命令,key是锁的唯一标识,按业务来决定命名,这种方式有个问题就是不能保证原子性,所以可以配合LUA脚本将两个命令绑定在一起

  • 上面这种方式是简单的方式,还有一种更简单的方式:Redis 2.6.12以上版本为set指令增加了可选参数,伪代码如下:set(key,1,30,NX),下面的实例代码就是通过这种方式来实现加锁,将两个操作合并为一个命令了

解锁

  • 下面的实例也是用了Lua脚本来实现解锁,首先判断解锁的这个人是不是本人,如果是的话就执行del,如果不是的话就不能解锁了
1
2
3
4
5
6
7
8
StringBuilder sb = new StringBuilder();
sb.append("if redis.call(\"get\",KEYS[1]) == ARGV[1] ");
sb.append("then ");
sb.append(" return redis.call(\"del\",KEYS[1]) ");
sb.append("else ");
sb.append(" return 0 ");
sb.append("end ");
UNLOCK_LUA = sb.toString();

实例

  • 接口类
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 interface RedisLockService {
/**
* 加锁
* 过期时间为5分钟
* @param lockKey 锁
* @param requestId 请求标识
* @return
*/
boolean lock(String lockKey, String requestId);


/**
* 加锁
*
* @param lockKey 锁
* @param requestId 请求标识
* @param expireTime 锁的有效时间(s)
* @return
*/
boolean lock(String lockKey, String requestId, long expireTime);


/**
* 解锁
* @param lockKey 锁
* @param requestId 请求标识
* @return
*/
boolean unlock(String lockKey, String requestId);
}
  • 实现类
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
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
@Slf4j
@Service
public class RedisLockServiceImpl implements RedisLockService {

@Autowired
private StringRedisTemplate redisTemplate;

/**
* 将key 的值设为value ,当且仅当key 不存在,等效于 SETNX。
*/
private static final String NX = "NX";

/**
* seconds — 以秒为单位设置 key 的过期时间,等效于EXPIRE key seconds
*/
private static final String EX = "EX";

/**
* 调用set后的返回值
*/
private static final String OK = "OK";

/**
* 解锁的lua脚本
*/
private static final String UNLOCK_LUA;

static {
StringBuilder sb = new StringBuilder();
sb.append("if redis.call(\"get\",KEYS[1]) == ARGV[1] ");
sb.append("then ");
sb.append(" return redis.call(\"del\",KEYS[1]) ");
sb.append("else ");
sb.append(" return 0 ");
sb.append("end ");
UNLOCK_LUA = sb.toString();
}

@Override
public boolean lock(String lockKey, String requestId) {
return lock(lockKey, requestId, RedisConstant.LOCK_TIME_FIVE_MINUTES);
}


@Override
public boolean lock(String lockKey, String requestId, long expireTime) {
Assert.isTrue(StringUtils.isNotEmpty(lockKey), "key不能为空");
Assert.isTrue(StringUtils.isNotEmpty(requestId), "请求标识不能为空");
String result = set(lockKey, requestId, expireTime);
return OK.equalsIgnoreCase(result);
}

@Override
public boolean unlock(String lockKey, String requestId) {
return redisTemplate.execute(new RedisCallback<Boolean>() {
@Override
public Boolean doInRedis(RedisConnection connection) throws DataAccessException {
Object nativeConnection = connection.getNativeConnection();
Long result = 0L;

List<String> keys = new ArrayList<>();
keys.add(lockKey);
List<String> values = new ArrayList<>();
values.add(requestId);

// 集群模式
if (nativeConnection instanceof JedisCluster) {
result = (Long) ((JedisCluster) nativeConnection).eval(UNLOCK_LUA, keys, values);
}

// 单机模式
if (nativeConnection instanceof Jedis) {
result = (Long) ((Jedis) nativeConnection).eval(UNLOCK_LUA, keys, values);
}

if (result == 0) {
log.info("Redis分布式锁,解锁{}失败!解锁时间:{}", lockKey, System.currentTimeMillis());
}
return result == 1;
}
});
}

/**
* 重写redisTemplate的set方法
* <p>
* 命令 SET resource-name anystring NX EX max-lock-time 是一种在 Redis 中实现锁的简单方法。
* <p>
* 客户端执行以上的命令:
* <p>
* 如果服务器返回 OK ,那么这个客户端获得锁。
* 如果服务器返回 NIL ,那么客户端获取锁失败,可以在稍后再重试。
*
* @param key 锁的Key
* @param value 锁里面的值
* @param seconds 过去时间(秒)
* @return
*/
private String set(final String key, final String value, final long seconds) {
Assert.isTrue(StringUtils.isNotEmpty(key), "key不能为空");
return redisTemplate.execute(new RedisCallback<String>() {
@Override
public String doInRedis(RedisConnection connection) throws DataAccessException {
Object nativeConnection = connection.getNativeConnection();
String result = null;
// 集群模式
if (nativeConnection instanceof JedisCluster) {
result = ((JedisCluster) nativeConnection).set(key, value, NX, EX, seconds);
}
// 单机模式
if (nativeConnection instanceof Jedis) {
result = ((Jedis) nativeConnection).set(key, value, NX, EX, seconds);
}

log.info("获取锁{}的时间:{}", key, System.currentTimeMillis());

return result;
}
});
}
}