文章目录
Redis - 分布式锁实现以及相关问题解决方案
1.分布式锁是什么?
1.1 分布式锁设计目的
1.2 分布式锁设计要求
1.3 分布式锁设计思路
2.分布式锁实现
3.分布式锁实现过程中可能出现的问题以及解决方案
3.1 服务宕机造成死锁
3.1.1 Lua脚本命令连用
3.1.2 RedisConnection命令连用
3.1.3 升级高版本Redis
3.2 业务时间大于锁超时时间
3.2.1 解锁错位问题
3.2.2 业务并发执行问题
3.2.3 可重入锁
4.总结
Redis - 分布式锁实现以及相关问题解决方案
1.分布式锁是什么?
?分布式锁是控制分布式系统或不同系统之间共同访问共享资源的一种锁实现。如果不同的系统或同一个系统的不同主机之间共享了某个资源时,往往通过互斥来防止彼此之间的干扰。实现分布式锁的方式有很多,可以通过各种中间件来进行分布式锁的设计,包括Redis、Zookeeper等,这里我们主要介绍Redis如何实现分布式锁以及在整个过程中出现的问题和优化解决方案。
1.1 分布式锁设计目的
?可以保证在分布式部署的应用集群中,同一个方法的同一操作只能被一台机器上的一个线程执行。分布式锁至少包含以下三点:
具有互斥性。任意时刻只有一个服务持有锁。
不会死锁。即使持有锁的服务异常崩溃没有主动解锁后续也能够保证其他服务可以拿到锁。
加锁和解锁都需要是同一个服务。
1.2 分布式锁设计要求
分布式锁要是一把可重入锁(同时需避免死锁)。
分布式锁有高可用的获取锁和释放锁功能。
分布式锁获取锁和释放锁的性能要好
1.3 分布式锁设计思路
使用SETNX命令获取锁(Key存在则返回0,不存在并且设置成功返回1)。
若返回0则不进行业务操作,若返回1则设置锁Value为当前服务器IP + 业务标识,用于锁释放和锁延期时判断。同时使用EXPIRE命令给锁设置一个合理的过期时间,避免当前服务宕机锁永久存在造成死锁,并且设计需要保证可重入性。
执行业务,业务执行完成判断当前锁Value是否为当前服务器IP + 业务标识,若相同则通过DEL或者EXPIRE设置为0释放当前锁。
2.分布式锁实现
?我们在实现分布式锁的过程中大致思路就是上图的整个流程,这里我们主要记住几个要点:
锁一定要设置失效时间,否则服务宕机锁就会永久性存在,整个业务体系死锁。
业务执行完必须解锁,可将加锁和业务代码放置try/catch中,解锁流程放置finally中。
?若要用jar包方式后台启动服务,可用 nohup java -jar jar包名称 &命令。这里我们来看一下我们加解锁的主要代码。
ClusterLockJob.java
package com.springboot.schedule;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.util.Enumeration;
import java.util.concurrent.TimeUnit;
/**
* @author hzk
* @date 2019/7/2
*/
@Component
public class ClusterLockJob {
@Autowired
private RedisTemplate redisTemplate;
@Value("${server.port}")
private String port;
public static final String LOCK_PRE = "lock_prefix_";
@Scheduled(cron = "0/5 * * * * *")
public void lock(){
String lockName = LOCK_PRE + "ClusterLockJob";
String currentValue = https://www.it610.com/article/getHostIp() +":" + port;
Boolean ifAbsent = false;
try {
//设置锁
//Boolean ifAbsent = redisTemplate.opsForValue().setIfAbsent(lockName, currentValue,3600, TimeUnit.SECONDS);
ifAbsent = redisTemplate.opsForValue().setIfAbsent(lockName, currentValue);
if(ifAbsent){
//获取锁成功,设置失效时间
redisTemplate.expire(lockName,3600,TimeUnit.SECONDS);
System.out.println("Lock success,execute business,current time:" + System.currentTimeMillis());
Thread.sleep(3000);
}else{
//获取锁失败
String value = https://www.it610.com/article/(String) redisTemplate.opsForValue().get(lockName);
System.out.println("Lock fail,current lock belong to:" + value);
}
}catch (Exception e){
System.out.println("ClusterLockJob exception:" + e);
}finally {
if(ifAbsent){
//若分布式锁Value与本机Value一致,则当前机器获得锁,进行解锁
redisTemplate.delete(lockName);
}
}
}
/**
* 获取本机内网IP地址方法
* @return
*/
private static String getHostIp(){
try{
Enumeration allNetInterfaces = NetworkInterface.getNetworkInterfaces();
while (allNetInterfaces.hasMoreElements()){
NetworkInterface netInterface = (NetworkInterface) allNetInterfaces.nextElement();
Enumeration addresses = netInterface.getInetAddresses();
while (addresses.hasMoreElements()){
InetAddress ip = (InetAddress) addresses.nextElement();
if (ip != null
&& ip instanceof Inet4Address
&& !ip.isLoopbackAddress() //loopback地址即本机地址,IPv4的loopback范围是127.0.0.0 ~ 127.255.255.255
&& ip.getHostAddress().indexOf(":")==-1){
return ip.getHostAddress();
}
}
}
}catch(Exception e){
e.printStackTrace();
}
return null;
}
}
?这里我们给锁Key设定了一个和业务相关的唯一标示,用于当前业务分布式锁的相关操作,首先我们通过setIfAbsent也就是SETNX命令去加锁,若成功我们给锁加上失效时间并执行业务结束后解锁,否则重试或者结束等待下一次任务周期。这里我们不将服务打包多个部署在服务器上,直接本地修改端口启动三个项目。看下结果是否和我们预想一致。
port:8080
Lock fail,current lock belong to:192.168.126.1:8081
Lock fail,current lock belong to:192.168.126.1:8082
Lock fail,current lock belong to:192.168.126.1:8082
Lock fail,current lock belong to:192.168.126.1:8081
Lock success,execute business,current time:1562122895372
Lock fail,current lock belong to:192.168.126.1:8082
Lock success,execute business,current time:1562122905350
Lock success,execute business,current time:1562122910334
Lock success,execute business,current time:1562122915340
Lock fail,current lock belong to:192.168.126.1:8082
port:8081
Lock fail,current lock belong to:192.168.126.1:8080
Lock fail,current lock belong to:192.168.126.1:8082
Lock fail,current lock belong to:192.168.126.1:8080
Lock fail,current lock belong to:192.168.126.1:8080
Lock fail,current lock belong to:192.168.126.1:8080
Lock fail,current lock belong to:192.168.126.1:8082
Lock fail,current lock belong to:192.168.126.1:8082
Lock fail,current lock belong to:192.168.126.1:8082
Lock fail,current lock belong to:192.168.126.1:8080
Lock success,execute business,current time:1562122940330
port:8082
Lock fail,current lock belong to:192.168.126.1:8080
Lock fail,current lock belong to:192.168.126.1:8080
Lock success,execute business,current time:1562122920341
Lock success,execute business,current time:1562122925392
Lock success,execute business,current time:1562122930407
Lock fail,current lock belong to:192.168.126.1:8080
Lock fail,current lock belong to:192.168.126.1:8081
Lock success,execute business,current time:1562122945340
Lock fail,current lock belong to:192.168.126.1:8080
Lock success,execute business,current time:1562122955339
?当我们同时开启三个服务,模拟分布式项目,可以看到当我们执行同一段业务代码时,通过分布式锁的实现达到了我们预期的目的,同时只会有一个服务进行业务处理。
3.分布式锁实现过程中可能出现的问题以及解决方案
3.1 服务宕机造成死锁
?上面我们通过我们之前的设计思路,去构建了一个分布式锁的实现,但是在真实的场景中我们需要考虑更多可能出现的一些问题。上面我们实现的思路整体是没有问题的,但是还需要考虑一些特殊情况。
?通过以上两张图我们可以知道,当我们某个服务在成功获取锁之后,在还没有给当前锁设置失效时间之前服务宕机,那么该锁会永久存在,整个业务体系会形成死锁。我们这里模拟这个业务场景,先同时开启三个服务,然后当某个服务设置锁并未设置失效时间前我们把他给停止。
port:8080
Lock fail,current lock belong to:192.168.126.1:8082
Lock fail,current lock belong to:192.168.126.1:8082
Disconnected from the target VM, address: '127.0.0.1:8606', transport: 'socket'
Lock success,execute business,current time:1562124770986
?当8080端口服务获取到锁未设置锁失效时间时我们将其停止。观察另外两个服务获取锁情况。
port:8081
Lock fail,current lock belong to:192.168.126.1:8080
Lock fail,current lock belong to:192.168.126.1:8080
Lock fail,current lock belong to:192.168.126.1:8080
Lock fail,current lock belong to:192.168.126.1:8080
Lock fail,current lock belong to:192.168.126.1:8080
Lock fail,current lock belong to:192.168.126.1:8080
Lock fail,current lock belong to:192.168.126.1:8080
Lock fail,current lock belong to:192.168.126.1:8080
Lock fail,current lock belong to:192.168.126.1:8080
Lock fail,current lock belong to:192.168.126.1:8080
port:8082
Lock fail,current lock belong to:192.168.126.1:8080
Lock fail,current lock belong to:192.168.126.1:8080
Lock fail,current lock belong to:192.168.126.1:8080
Lock fail,current lock belong to:192.168.126.1:8080
Lock fail,current lock belong to:192.168.126.1:8080
Lock fail,current lock belong to:192.168.126.1:8080
Lock fail,current lock belong to:192.168.126.1:8080
Lock fail,current lock belong to:192.168.126.1:8080
Lock fail,current lock belong to:192.168.126.1:8080
Lock fail,current lock belong to:192.168.126.1:8080
?果然另外两个服务是会无止尽获取锁失败,进入一个无限循环拿不到锁的情况,此时就出现了我们所说的服务提供异常造成的死锁问题,这里我们有几种解决办法介绍给大家,主要的解决思路都是使SETNX和SETEX包装成一个整体使其具有原子性来解决。
3.1.1 Lua脚本命令连用
?Redis2.6.0版本起,通过内置的Lua解释器,可以使用EVAL命令对Lua脚本进行求值。关于Lua大家可以自己去了解,使用起来的话很简单。Redis使用单个Lua解释器去运行所有脚本,并且也保证脚本会以原子性的方式执行,即当某个脚本正在运行时不会有其他脚本或Redis命令被执行。这和使用MULTI/EXEC包围的事务类似。在其他客户端看来,脚本的效果要么是不可见的,要么是已完成的。关于EVAL命令使用可以参考Redis 命令参考 ? Script(脚本)。
?通过Redis对Lua脚本保持原子性的支持,我们可以利用此特性去实现SETNX和SETEX并用,包装成一个整体执行。这里我们主要有以下几个步骤:
资源文件目录新建.lua文件并且编写lua脚本
代码中传递参数执行脚本
setnx_ex.lua
local lockKey = KEYS[1]
local lockValue = https://www.it610.com/article/KEYS[2]
local result_nx = redis.call('SETNX',lockKey,lockValue)
if result_nx == 1 then
local result_ex = redis.call('EXPIRE',lockKey,3600)
return result_ex
else
return result_nx
end
LuaClusterLockJob.java
package com.springboot.schedule;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.scripting.support.ResourceScriptSource;
import org.springframework.stereotype.Component;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.concurrent.TimeUnit;
/**
* lua脚本命令连用 保证原子性
* @author hzk
* @date 2019/7/2
*/
@Component
public class LuaClusterLockJob {
@Autowired
private RedisTemplate redisTemplate;
@Value("${server.port}")
private String port;
public static final String LOCK_PRE = "lock_prefix_lua_";
private DefaultRedisScript lockLuaScript;
@Scheduled(cron = "0/5 * * * * *")
public void lock(){
String lockName = LOCK_PRE + "LuaClusterLockJob";
String currentValue = https://www.it610.com/article/getHostIp() +":" + port;
Boolean luaResult = false;
try {
//设置锁
luaResult = luaScript(lockName,currentValue);
if(luaResult){
//获取锁成功,设置失效时间
System.out.println("Lock success,execute business,current time:" + System.currentTimeMillis());
Thread.sleep(3000);
}else{
//获取锁失败
String value = https://www.it610.com/article/(String) redisTemplate.opsForValue().get(lockName);
System.out.println("Lock fail,current lock belong to:" + value);
}
}catch (Exception e){
System.out.println("ClusterLockJob exception:" + e);
}finally {
if(luaResult){
//若分布式锁Value与本机Value一致,则当前机器获得锁,进行解锁
redisTemplate.delete(lockName);
}
}
}
/**
* 获取本机内网IP地址方法
* @return
*/
private static String getHostIp(){
try{
Enumeration allNetInterfaces = NetworkInterface.getNetworkInterfaces();
while (allNetInterfaces.hasMoreElements()){
NetworkInterface netInterface = (NetworkInterface) allNetInterfaces.nextElement();
Enumeration addresses = netInterface.getInetAddresses();
while (addresses.hasMoreElements()){
InetAddress ip = (InetAddress) addresses.nextElement();
if (ip != null
&& ip instanceof Inet4Address
&& !ip.isLoopbackAddress() //loopback地址即本机地址,IPv4的loopback范围是127.0.0.0 ~ 127.255.255.255
&& ip.getHostAddress().indexOf(":")==-1){
return ip.getHostAddress();
}
}
}
}catch(Exception e){
e.printStackTrace();
}
return null;
}
/**
* 执行lua脚本
* @param key
* @param value
* @return
*/
public Boolean luaScript(String key,String value){
lockLuaScript = new DefaultRedisScript();
lockLuaScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("setnx_ex.lua")));
lockLuaScript.setResultType(Boolean.class);
//封装传递脚本参数
ArrayList
推荐阅读