目录
- 分布式锁场景
- Redis分布式锁的简易实现
- Redis分布式锁存在的问题
- Lua脚本实现分布式锁一致性
- RedisConnection实现分布式锁一致性
- 分布式锁优化分析建议
分布式锁场景
-
分布式锁是什么
分布式锁是控制分布式系统或不同系统之间共同访问共享资源的一种锁实现
如果不同的系统或同一个系统的不同主机之间共享了某个资源时,往往通过互斥来防止彼此干扰。 -
分布锁设计目的
可以保证在分布式部署的应用集群中,同一个方法在同一操作只能被一台机器上的一个线程执行。 -
设计要求
- 这把锁要是一把可重入锁(避免死锁)
- 这把锁有高可用的获取锁和释放锁功能
- 这把锁获取锁和释放锁的性能要好…
-
分布锁实现方案分析
- 获取锁的时候,使用 setnx(SETNX key val:当且仅当 key 不存在时,set 一个 key 为 val 的字符串,返回 1;
- 若 key 存在,则什么都不做,返回 【0】加锁,锁的 value 值为当前占有锁服务器内网IP编号拼接任务标识
- 在释放锁的时候进行判断。并使用 expire 命令为锁添 加一个超时时间,超过该时间则自动释放锁。
- 返回1则成功获取锁。还设置一个获取的超时时间, 若超过这个时间则放弃获取锁。setex(key,value,expire)过期以秒为单位
- 释放锁的时候,判断是不是该锁(即Value为当前服务器内网IP编号拼接任务标识),若是该锁,则执行 delete 进行锁释放
Redis分布式锁的简易实现
setnx获取锁成功后设置setex超时时间,在没有任何意外宕机的情况下看起来很完美,下文讲解下突然宕机的情况会产生什么问题
代码演练
本文项目环境基于Springboot与Redis Cache深度整合
1.再主启动类RedisApplication上添加定时器注解 @EnableScheduling
package com.gcxzflgl.redis;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication
@EnableCaching
@EnableScheduling
public class RedisApplication {
public static void main(String[] args) {
SpringApplication.run(RedisApplication.class, args);
}
}
2.再工程结构中新建schedule包,创建定时任务类演练分布式锁初步实现
package com.gcxzflgl.redis.schedule;
/**
* @author gcxzf$
* @version : ClusterLockJob$, v 0.1 2020/6/14$ 9:03$ gcxzf$ Exp$
*/
@Service
public class ClusterLockJob {
private static final Logger logger = LoggerFactory.getLogger(ClusterLockJob.class);
@Autowired
private RedisService redisService;
@Autowired
private RedisTemplate redisTemplate;
private static String LOCK_PREFIX = "prefix_";
@Scheduled(cron = "0/10 * * * * *")
public void lockJob() {
String lock = LOCK_PREFIX + "ClusterLockJob";
boolean nxRet = false;
try{
//redistemplate setnx操作
nxRet = redisTemplate.opsForValue().setIfAbsent(lock,getHostIp());
Object lockValue = redisService.get(lock);
//获取锁失败
if(!nxRet){
String value = (String)redisService.get(lock);
//打印当前占用锁的服务器IP
logger.info("get lock fail,lock belong to:{}",value);
return;
}else{
redisTemplate.opsForValue().set(lock,getHostIp(),3600);
//获取锁成功
logger.info("start lock lockNxExJob success");
Thread.sleep(5000);
}
}catch (Exception e){
logger.error("lock error",e);
}finally {
if(nxRet){
logger.info("release lock success");
redisService.remove(lock);
}
}
}
/**
* 获取本机内网IP地址方法
* @return
*/
private static String getHostIp(){
try{
Enumeration<NetworkInterface> allNetInterfaces = NetworkInterface.getNetworkInterfaces();
while (allNetInterfaces.hasMoreElements()){
NetworkInterface netInterface = (NetworkInterface) allNetInterfaces.nextElement();
Enumeration<InetAddress> 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;
}
public static void main(String[] args) {
String localIP = "";
try {
localIP = getHostIp();
} catch (Exception e) {
e.printStackTrace();
}
//获取本机IP
System.out.println(localIP);
}
}
3.演练方式,本地工程启动和jar包启动查看演示效果
Redis分布式锁存在的问题
上文中用setnx 和 setex实现了简易分布式锁,我们剖析分布式锁setnx、setex的缺陷,在setnx和setex中间发生了服务down机的情况,第一台服务器加锁成功,突如其来的服务宕机导致redis锁无法释放,其他服务一直拿不到锁,也就是如何保证一致性问题。
Lua脚本实现分布式锁一致性
- Lua简介
- 从 Redis 2.6.0 版本开始,通过内置的 Lua 解释器,可以使用 EVAL 命令对 Lua 脚本进行求值。
- Redis 使用单个 Lua 解释器去运行所有脚本,并且, Redis 也保证脚本会以原子性(atomic)的方式执行:当某个脚本正在运行的时候,不会有其他脚本或 Redis 命令被执行。这和使用 MULTI / EXEC 包围的事务很类似。在其他别的客户端看来,脚本的效果(effect)要么是不可见的(not visible),要么就是已完成的(already completed)。
- Lua脚本配置流程
- 1、在resource目录下面新增一个后缀名为.lua结尾的文件
- 2、编写lua脚本
- 3、传入lua脚本的key和arg
- 4、调用redisTemplate.execute方法执行脚本
代码演练:
1.再/resources新建add.lua的脚本
-- local定义变量 KEYS[N] 是由代码传递过来的key1 key2 key3
local lockKey = KEYS[1]
local lockValue = KEYS[2]
-- 保证一致性,要么全部成功,要么全部失败 redis.call 以命令方式执行
local result_1 = redis.call('SETNX', lockKey, lockValue)
if result_1 == true
then
local result_2= redis.call('SETEX', lockKey,3600, lockValue)
return result_1
else
return result_1
end
2.RedisService 新增工具方法
/**
* 读取缓存
*
* @param key
* @return
*/
public Object genValue(final String key) {
Object result = null;
ValueOperations<String, String> operations = redisTemplate.opsForValue();
result = operations.get(key);
return result;
}
3.再schedule中新增luaDistributeLock.java
package com.gcxzflgl.redis.schedule;
import com.gcxzflgl.redis.biz.RedisService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
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.Service;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
@Service
public class LuaDistributeLock {
private static final Logger logger = LoggerFactory.getLogger(LuaDistributeLock.class);
@Autowired
private RedisService redisService;
@Autowired
private RedisTemplate redisTemplate;
private static String LOCK_PREFIX = "lua_";
private DefaultRedisScript<Boolean> lockScript;
@Scheduled(cron = "0/10 * * * * *")
public void lockJob() {
String lock = LOCK_PREFIX + "LockNxExJob";
boolean luaRet = false;
try {
luaRet = luaExpress(lock,getHostIp());
//获取锁失败
if (!luaRet) {
String value = (String) redisService.genValue(lock);
//打印当前占用锁的服务器IP
logger.info("lua get lock fail,lock belong to:{}", value);
return;
} else {
//获取锁成功
logger.info("lua start lock lockNxExJob success");
Thread.sleep(5000);
}
} catch (Exception e) {
logger.error("lock error", e);
} finally {
if (luaRet) {
logger.info("release lock success");
redisService.remove(lock);
}
}
}
/**
* 获取lua结果
* @param key
* @param value
* @return
*/
public Boolean luaExpress(String key,String value) {
//获取操作lua脚本的类
lockScript = new DefaultRedisScript<Boolean>();
//设置脚本的存放地址
lockScript.setScriptSource(
new ResourceScriptSource(new ClassPathResource("add.lua")));
lockScript.setResultType(Boolean.class);
// 封装参数
List<Object> keyList = new ArrayList<Object>();
keyList.add(key); //对应Lua脚本 KEYS[1]
keyList.add(value); //对应Lua脚本 KEYS[2]
Boolean result = (Boolean) redisTemplate.execute(lockScript, keyList);
return result;
}
/**
* 获取本机内网IP地址方法
*
* @return
*/
private static String getHostIp() {
try {
Enumeration<NetworkInterface> allNetInterfaces = NetworkInterface.getNetworkInterfaces();
while (allNetInterfaces.hasMoreElements()) {
NetworkInterface netInterface = (NetworkInterface) allNetInterfaces.nextElement();
Enumeration<InetAddress> 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;
}
}
4.用IDEA 启动端口 8080,8081模拟一致性
RedisConnection实现分布式锁一致性
RedisConnection实现分布锁的方式,采用redisTemplate操作redisConnection
实现setnx和setex两个命令连用,这种操作方式很常见。
代码演练:
1.再pom.xml引入依赖
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
2.shedule包下新增redisTmplate实现RedisConnection保证一致性
package com.gcxzflgl.redis.schedule;
@Component
public class JedisDistributedLock {
private final Logger logger = LoggerFactory.getLogger(JedisDistributedLock.class);
private static String LOCK_PREFIX = "lua_";
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private RedisService redisService;
public 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();
}
@Scheduled(cron = "0/10 * * * * *")
public void lockJob() {
String lock = LOCK_PREFIX + "JedisNxExJob";
boolean lockRet = false;
try {
lockRet = this.setLock(lock, 600);
//获取锁失败
if (!lockRet) {
String value = (String) redisService.genValue(lock);
//打印当前占用锁的服务器IP
logger.info("jedisLockJob get lock fail,lock belong to:{}", value);
return;
} else {
//获取锁成功
logger.info("jedisLockJob start lock lockNxExJob success");
Thread.sleep(5000);
}
} catch (Exception e) {
logger.error("jedisLockJob lock error", e);
} finally {
if (lockRet) {
logger.info("jedisLockJob release lock success");
redisService.remove(lock);
}
}
}
public boolean setLock(String key, long expire) {
try {
Boolean result = redisTemplate.execute(new RedisCallback<Boolean>() {
@Override
public Boolean doInRedis(RedisConnection connection) throws DataAccessException {
return connection.set(key.getBytes(), "锁定的资源".getBytes(), Expiration.seconds(expire) ,RedisStringCommands.SetOption.ifAbsent());
}
});
return result;
} catch (Exception e) {
logger.error("set redis occured an exception", e);
}
return false;
}
public String get(String key) {
try {
RedisCallback<String> callback = (connection) -> {
JedisCommands commands = (JedisCommands) connection.getNativeConnection();
return commands.get(key);
};
String result = redisTemplate.execute(callback);
return result;
} catch (Exception e) {
logger.error("get redis occured an exception", e);
}
return "";
}
/**
* 获取本机内网IP地址方法
*
* @return
*/
private static String getHostIp() {
try {
Enumeration<NetworkInterface> allNetInterfaces = NetworkInterface.getNetworkInterfaces();
while (allNetInterfaces.hasMoreElements()) {
NetworkInterface netInterface = (NetworkInterface) allNetInterfaces.nextElement();
Enumeration<InetAddress> 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;
}
}
分布式锁优化建议
上图中的方式会存在一种无锁状态
假如:A获得锁,锁超时时间是1个小时,再任务没有执行完时不会释放锁,但是超时时间到了释放了锁,此时B操作判断当前是无锁获取了锁,然而A执行完毕了,释放了锁,其实释放的是B的锁
为了解决这种问题发生,再次引入Lua脚本来执行,我们改造下RedisConnection的方法
package com.gcxzflgl.redis.schedule;
@Component
public class JedisDistributedLock {
private final Logger logger = LoggerFactory.getLogger(JedisDistributedLock.class);
private static String LOCK_PREFIX = "JedisDistributedLock_";
private DefaultRedisScript<Boolean> lockScript;
@Resource
private RedisTemplate<Object, Object> redisTemplate;
@Autowired
private RedisService redisService;
public 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();
}
@Scheduled(cron = "0/10 * * * * *")
public void lockJob() {
String lock = LOCK_PREFIX + "JedisNxExJob";
boolean lockRet = false;
try {
lockRet = this.setLock(lock, 600);
//获取锁失败
if (!lockRet) {
String value = (String) redisService.genValue(lock);
//打印当前占用锁的服务器IP
logger.info("jedisLockJob get lock fail,lock belong to:{}", value);
return;
} else {
//获取锁成功
logger.info("jedisLockJob start lock lockNxExJob success");
Thread.sleep(5000);
}
} catch (Exception e) {
logger.error("jedisLockJob lock error", e);
} finally {
if (lockRet) {
logger.info("jedisLockJob release lock success");
releaseLock(lock,getHostIp());
}
}
}
public boolean setLock(String key, long expire) {
try {
Boolean result = redisTemplate.execute(new RedisCallback<Boolean>() {
@Override
public Boolean doInRedis(RedisConnection connection) throws DataAccessException {
return connection.set(key.getBytes(), getHostIp().getBytes(), Expiration.seconds(expire) ,RedisStringCommands.SetOption.ifAbsent());
}
});
return result;
} catch (Exception e) {
logger.error("set redis occured an exception", e);
}
return false;
}
public String get(String key) {
try {
RedisCallback<String> callback = (connection) -> {
JedisCommands commands = (JedisCommands) connection.getNativeConnection();
return commands.get(key);
};
String result = redisTemplate.execute(callback);
return result;
} catch (Exception e) {
logger.error("get redis occured an exception", e);
}
return "";
}
/**
* 释放锁操作
* @param key
* @param value
* @return
*/
private boolean releaseLock(String key, String value) {
lockScript = new DefaultRedisScript<Boolean>();
lockScript.setScriptSource(
new ResourceScriptSource(new ClassPathResource("unlock.lua")));
lockScript.setResultType(Boolean.class);
// 封装参数
List<Object> keyList = new ArrayList<Object>();
keyList.add(key);
keyList.add(value);
Boolean result = (Boolean) redisTemplate.execute(lockScript, keyList);
return result;
}
/**
* 获取本机内网IP地址方法
*
* @return
*/
private static String getHostIp() {
try {
Enumeration<NetworkInterface> allNetInterfaces = NetworkInterface.getNetworkInterfaces();
while (allNetInterfaces.hasMoreElements()) {
NetworkInterface netInterface = (NetworkInterface) allNetInterfaces.nextElement();
Enumeration<InetAddress> 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;
}
}
新增unlock.lua脚本
当key的value与传递进来的value相同时进行释放锁,不会影响其他服务正常执行
local lockKey = KEYS[1]
local lockValue = KEYS[2]
-- get key
local result_1 = redis.call('get', lockKey)
if result_1 == lockValue
then
local result_2= redis.call('del', lockKey)
return result_2
else
return false
end
至此,分布式锁的讲解到此完成!