java中我们常用的锁有synchronized或者Lock,由于这些锁是线程锁,所以对同一个JVM进程内的多个线程有效。因为锁的本质 是内存中存放一个标记,记录获取锁的线程是谁,这个标记对每个线程都可见。但是目前的形式下,很多时候是多服务集群的,就会是多个jvm,各个jvm之间是不共享的,也就无法再这种情况下使用以上两种线程锁了。就使用到分布式锁了。
常用的有三种解决方案:1.基于数据库实现 2.基于zookeeper的临时序列化节点实现 3.redis实现。其中数据库实现的锁如果在高并发的情况不合适,redis是现在常用的服务。是一个高性能的key-value数据库。很适合。现在很多应用也是用的redis实现分布式锁。
分布式锁需要满足:多进程可见,互斥、可重入。
首先生成redis链接信息:
@Configuration
public class RedisConfigurer {
@Value("${spring.redis.host}")
String host;
@Value("${spring.redis.port}")
int port;
@Value("${spring.redis.password}")
String password;
@Value("${spring.redis.database}")
int database;
@Bean("jedisPool")
public JedisPool getJedisPool(){
JedisPool jedisPool = new JedisPool(new GenericObjectPoolConfig(), host, port, Protocol.DEFAULT_TIMEOUT, password, database, null);
return jedisPool;
}
}
第一种redis分布式锁:
所有的进程同时使用一个redis锁,每个进程在获取到锁之后设置锁的超时时间。其他的进程在锁未过期时是无法获取到做的。
// 获取锁之后的超时时间(防止死锁)单位seconds
private final static int timeOut = 100;
private static JedisPool jedisPool = SpringContextUtils.getBean(JedisPool.class);
private static Set<String> lockNameMap = new HashSet<>();
/**
* 第一种锁,多个进程使用同样的锁名称, 获取到锁之后设置超时时间
* @param lockName 锁名称
* @param value 锁值
* @return
*/
public static boolean tryLock(String lockName, String value){
Jedis jedis = jedisPool.getResource();
boolean flag = false;
//获取锁
try {
long result = jedis.setnx(lockName, value);
if (result == 1) {
//获取成功,设置锁的超时时间
long i = jedis.expire(lockName, timeOut);
lockNameMap.add(lockName);
System.out.println(String.format("获取到锁 %s --- %s, 超时设置返回:%s", lockName, value, i));
flag = true;
} else {
System.out.println(String.format("未获取到锁 %s --- %s, 返回:%s", lockName, value, result));
}
} finally {
close(jedis);
}
return flag;
}
/**
* 释放锁
* @param lockName 所名称
*/
public static void unLock(String... lockName){
Jedis jedis = jedisPool.getResource();
try {
//删除锁
long result = jedis.del(lockName);
if (result != 0) {
//删除成功
lockNameMap.removeAll(Lists.newArrayList(lockName));
System.out.println(String.format("删除锁成功:%s, 返回:%s", Arrays.toString(lockName), result));
} else {
System.out.println(String.format("删除锁失败:%s, 返回:%s", Arrays.toString(lockName), result));
}
}finally {
close(jedis);
}
}
private static void close(Jedis jedis){
if(jedis != null){
jedis.close();
}
}
第一种锁的缺陷:
缺陷一: 当一个服务获取到锁之后,还没有设置超时时间, 突然服务宕机,就会导致所有的服务都拿不到锁
解决:必须原子性的同时操作 获取锁和设置锁超时间 操作。redis提供了 nx 与 ex连用的命令--set
缺陷二:假设有A、B、C三个服务, A获取到锁之后设置了超时时间为 1min, 但是由于某些原因实际执行了 5mim 才去释放锁。在 1min 之后锁被释放, B获取了锁, 设置超时时间为 5mim。这时候 A执行完了,释放了B的锁。 C在这个时候获取锁也可以成功, 如果B 还执行完了C还没执行完了,B 就会释放了C 的锁,这就会导致后续一系列的问题。
解决:每一个服务在加锁时都带上自己的 线程id 标识,释放锁时,只有 线程id 标识一样时才会释放锁。
第二种redis分布式锁:
在加锁的同时设置超时时间,每一个进程在加锁的时带上自己的唯一标识。解锁时只有唯一标识一致的数据才能解锁。
// 获取锁之后的超时时间(防止死锁)单位seconds
private final static int timeOut = 100;
private static JedisPool jedisPool = SpringContextUtils.getBean(JedisPool.class);
/**
* 第二种锁,多个进程使用同样的锁名称并且锁中包含自己的线程id, 获取到锁之后设置超时时间
* @param lockName 锁名称
* @param clientId 线程的唯一标识
* @return
*/
public static boolean trySecondLock(String lockName, String clientId){
Jedis jedis = jedisPool.getResource();
String result = jedis.set(lockName, clientId, "NX", "PX", timeOut);
if("OK".equals(result)){
System.out.println(String.format("获取到锁 %s --- %s, 返回:%s", lockName, clientId, result));
return true;
}else {
System.out.println(String.format("没获取到锁 %s --- %s, 返回:%s", lockName, clientId, result));
}
return false;
}
/**
* 释放锁
* @param lockName 所名称
* @param clientId 锁所属标识
*/
public static void unSecondLock(String lockName, String clientId){
Jedis jedis = jedisPool.getResource();
try {
//判断clientId是否一致
String redisClientId = jedis.get(lockName);
if(clientId.equals(redisClientId)){
long result = jedis.del(lockName);
if (result != 0) {
//删除成功
lockNameMap.removeAll(Lists.newArrayList(lockName));
System.out.println(String.format("删除锁成功:%s, clientId= %s, 返回:%s", lockName, clientId, result));
} else {
System.out.println(String.format("删除锁失败:%s, clientId= %s, 返回:%s", lockName, clientId, result));
}
} else {
System.out.println(String.format("删除锁失败:%s, clientId= %s, redis锁的标识:%s", lockName, clientId, redisClientId));
}
}finally {
close(jedis);
}
}
private static void close(Jedis jedis){
if(jedis != null){
jedis.close();
}
}
第三种锁:重入锁
重入锁也叫做递归锁,指的是在同一线程内,外层函数获得锁之后,内层递归函数仍然可以获取到该锁。可重入锁可以避免因同一线程中多次获取锁而导致死锁发生。像synchronized就是一个重入锁,它是通过moniter函数记录当前线程信息来实现的。
获取锁:首先判断锁是否存在,如果不存在则加锁,设置锁的次数是1和超时时间。如果是存在,则判断锁是否是自己的,是自己的就所次数加1和设置超时时间,否则不处理。
释放锁:释放锁不能直接删除了,因为锁是可重入的,如果锁进入了多次,在内层直接删除锁, 导致外部的业务在没有锁的情况下执行,会有安全问题。因此必须获取锁时累计重入的次数,释放时则减去重入次数,如果减到0,则可以删除锁。
重入锁在redis中是按照一个类似map的形式,key值是线程的唯一标识,value值是重入的错次数,也就是同一个线程加了多少次锁。
实现:
重入锁需要用到lua脚本来实现:
加锁脚本lock.lua
local key = KEYS[1]; -- 第1个参数,锁的key
local threadId = ARGV[1]; -- 第2个参数,线程唯一标识
local releaseTime = ARGV[2]; -- 第3个参数,锁的自动释放时间
if(redis.call('exists', key) == 0) then -- 判断锁是否已存在
redis.call('hset', key, threadId, '1'); -- 不存在, 则获取锁
redis.call('expire', key, releaseTime); -- 设置有效期
return '1'; --返回string结果
end;
if(redis.call('hexists', key, threadId) == 1) then --根据threadId判断锁是否是自己加的
local count = redis.call('hincrby', key, threadId, '1'); --是,则重入次数加1
redis.call('expire', key, releaseTime); --设置超时时间
return tostring(count); -- 返回string结果
end;
return '0'; --到这里表示已经加了锁,但是锁不是自己加的。
释放锁脚本unlock.lua
local key = KEYS[1]; --第一个参数,锁的key
local threadId = ARGV[1]; -- 第2个参数,线程唯一标识
if (redis.call('hexists', key, threadId) == 0) then -- 判断当前锁是否还是被自己持有
return '-1'; --不是自己的锁,返回
end;
local count = redis.call('hincrby', key, threadId, -1); --到这里表示锁是自己的,重入次数减1表示,释放一次锁
if (count == 0) then --判断锁的重入次数是否为0
redis.call('DEL', key); --为0表示已经全部释放了锁,所以需要删掉锁值。
end;
return tostring(count); --返回剩余需要释放的次数
代码实现:
private void thirdLock(){
String lockName = "REDIS:TEST:LOCK:lock";
String clientId = "1";
String clientId2 = "2";
//获取锁测试
DefaultRedisScript<String> LOCK_SCRIPT;
DefaultRedisScript<String> UNLOCK_SCRIPT;
// 加载释放锁的脚本
LOCK_SCRIPT = new DefaultRedisScript<>();
LOCK_SCRIPT.setScriptSource(new ResourceScriptSource(new ClassPathResource("lock.lua")));
LOCK_SCRIPT.setResultType(String.class);
// 加载释放锁的脚本
UNLOCK_SCRIPT = new DefaultRedisScript<>();
UNLOCK_SCRIPT.setScriptSource(new ResourceScriptSource(new ClassPathResource("unlock.lua")));
UNLOCK_SCRIPT.setResultType(String.class);
for(int i = 0; i < 5; i++) {
JedisLockUtil.tryThirdLock(stringRedisTemplate, LOCK_SCRIPT, lockName, clientId);
}
for(int i = 0; i < 5; i++) {
JedisLockUtil.unlock(stringRedisTemplate, UNLOCK_SCRIPT, lockName, clientId);
}
}
/**
* 第三种锁,重入锁
*/
public static void tryThirdLock(StringRedisTemplate redisTemplate, DefaultRedisScript<String> LOCK_SCRIPT, String lockName, String clientId){
try {
// 执行脚本
String result = redisTemplate.execute(LOCK_SCRIPT, Collections.singletonList(lockName), clientId, "100");
// 判断结果
if(!"0".equals(result)) {
System.out.println(String.format("获取到锁 %s,clientId = %s, 返回:%s", lockName, clientId, result));
}else{
System.out.println(String.format("未获取到锁 %s, clientId = %s, 返回:%s", lockName, clientId, result));
}
} catch (Exception e){
e.printStackTrace();
}
}
/**
* 释放锁
* @param lockName 锁名称
* @param clientId 解锁标识
*/
public static void unlock(StringRedisTemplate redisTemplate, DefaultRedisScript<String> UNLOCK_SCRIPT, String lockName,String clientId) {
try {
// 执行脚本
String result = redisTemplate.execute(UNLOCK_SCRIPT, Collections.singletonList(lockName), clientId);
if(!"-1".equals(result)) {
System.out.println(String.format("释放锁 %s, clientId = %s, 返回:%s", lockName, clientId, result));
} else {
System.out.println(String.format("释放锁失败 %s, clientId = %s, 返回:%s", lockName, clientId, result));
}
} catch (Exception e) {
e.printStackTrace();
}
}
测试结果如图: