一、简介
通常我们的程序会部署在多个容器上运行(负载均衡),但是我们程序中有时需要加锁(比如多台机器同时运行定时任务,但是我们其实只希望运行一次)。Redis采用的是基于内存的采用的是单进程单线程模型的KV数据库,由C语言编写。我们可以根据Redis单线程特性可以用来实现分布式锁。比较常见的错误示例就是使用jedis.setnx()和jedis.expire()组合实现加锁,该锁通过两步完成不具有原子性,如果两步中间发生异常,就会锁死。又有同学在上面的基础上做改进,使用jedis.setnx()命令实现加锁,其中key是锁,value是锁的过期时间。这样也是有问题的,当服务器之间时间不同步时,锁也会失效(这两种错误情况的详情说明请查看参考文档1)。下面介绍一种比较完美的方式(当然下面这种方式在“redis master节点崩溃,主从切换进行同步”的时候,锁可能会失效,这种情况比较极端可以忽略)。
二、实现
package com.example.demo.common.redis;
import java.util.Collections;
import java.util.UUID;
import org.springframework.data.redis.connection.jedis.JedisConnection;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import com.example.demo.common.util.BeanUtil;
import redis.clients.jedis.Jedis;
/**
* 用redis实现分布式锁
*
* @author Horace
* @date 2018年7月25日 下午8:14:26
*/
public class DistributedLock {
private static final String LOCK_SUCCESS = "OK";
private static final String SET_IF_NOT_EXIST = "NX";
private static final String SET_WITH_EXPIRE_TIME = "PX";
private String lockKey;
/**
* 锁前缀(项目名称,防止重复)
*/
private static final String LOCKPREFIX = "TEST_DEMO";
/**
* 单个业务持有锁的时间最长3s,防止锁死
*/
private long lockExpireTime = 3 * 1000L;
private final String uuid = UUID.randomUUID().toString();
private StringRedisTemplate stringRedisTemplate;
public DistributedLock(String lockKey) {
initRedis();
this.lockKey = LOCKPREFIX + lockKey;
}
/**
* 生成分布式锁
*
* @param lockKey
* 锁key
* @param lockExpireTime
* 锁的过期时间(单位毫秒)
*/
public DistributedLock(String lockKey, long lockExpireTime) {
initRedis();
this.lockKey = LOCKPREFIX + lockKey;
this.lockExpireTime = lockExpireTime;
}
/**
* 因为是一个普通类(通过new创建,没有交给spring容器管理),所以不能直接使用注解的方式
*/
private void initRedis() {
// TODO Auto-generated method stub
if (stringRedisTemplate == null) {
stringRedisTemplate = BeanUtil.getBean(StringRedisTemplate.class);
}
}
/**
* 获取锁
*
* @return
*/
public boolean getLock() {
JedisConnection jc = (JedisConnection) stringRedisTemplate.getConnectionFactory().getConnection();
Jedis jedis = jc.getNativeConnection();
try {
String key = jedis.set(lockKey, uuid, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, lockExpireTime);
return LOCK_SUCCESS.equalsIgnoreCase(key);
}finally {
jedis.close();
}
}
/**
* 释放锁
*/
public void releaseLock() {
if(lockKey != null && "".equals(lockKey)) {
RedisScript<Long> script = new DefaultRedisScript<>(
"if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end",
Long.class);
// 删除本地
stringRedisTemplate.execute(script, Collections.singletonList(lockKey), uuid);
}
}
}
三、总结
上面的实现代码(加锁和释放锁),能比较完美的实现分布式锁,主要是利用了Redis的单线程特性+原子性方法操作。该方法也是Redis官网介绍的实现方法。
参考文档:
1、http://www.sohu.com/a/208019016_355142 Redis分布式锁的正确实现方式( Java 版 );
2、https://redis.io/topics/distlock Distributed locks with Redis
3、https://www.cnblogs.com/s648667069/p/6489557.html 普通类获取Spring容器中的bean