怎样实现redis分布式锁?

  • 时间:
  • 浏览:0
  • 来源:跟我学网络

分布式锁是什么?

分布式锁是控制分布式系统或不同系统之间共同访问共享资源的一种锁实现,如果不同的系统或同一个系统的不同主机之间共享了某个资源时,往往通过互斥来防止彼此干扰。

分布锁设计目的?

可以保证在分布式部署的应用集群中,同一个方法在同一操作只能被一台机器上的一个线程执行。

分布式锁设计要求

1、这是一把可重入锁(避免死锁)

2、这把锁具备高性能获取锁和释放锁的功能

3、这把锁具有高可用获取锁和释放锁的功能

分布锁实现方案分析

1、加锁的时候,使用 setnx(SETNX 当且仅当 key 不存在时,set 一个 key 为 val 的字符串才成功)

2、锁的 value 值可以为当前占有锁服务器内网IP编号拼接任务标识,如:lock1_127.0.0.1

3、使用 expire 命令为锁添 加一个超时时间,超过该时间则自动释放锁

4.、释放锁的时候,判断是不是该锁(即Value为当前服务器内网IP编号拼接任务标识),若是该锁,则执行 delete 进行锁释放

</svg>" width="1751">

Redis分布式锁可能出现的问题

基于上面的分析,我们很容易能想到采用setnx进行锁独占性保证,此外,除了独占性,我们还要保证这把锁的有效期,如果锁长期不过期将导致某个系统执行一次任务之后,其他任务都无法被当前系统或其他系统执行,所以我们将采用setex来保证这把锁的有效期。但在系统设计的时候我们得为百分之一的可能性作为百分之百的设计,当我们执行完setnx之后,此时,如果出现Redis宕机或者我们的Server服务宕机情况,我们的setex命令将无法执行。宕机情况如图

</svg>" width="1751">

基于故障服务机故障和Redis故障,我们必须要保证setnx和setex这两个命令的原子性, 那么redis保证这两个命令原子性的方式主要有两种,一种是采用lua脚本(此种方式是保证所有命令原子性的方法),一种是基于官方的setnx和setex命令连用。

我们先来讲一下Lua脚本整合springboot的redisTemplate这种实现流程步骤

在resource目录下面新增一个后缀名为.lua结尾的文件

编写lua脚本

传入lua脚本的key和arg

调用redisTemplate.execute方法执行脚本

下面我们讲一下Lua脚本整合springboot的redisTemplate这种实现的代码

引入maven pom.xml文件依赖

<dependency>

<groupId>org.springframework.session</groupId>

<artifactId>spring-session-data-redis</artifactId>

<version>2.0.5.RELEASE</version>

</dependency>

注入redisTemplate的bean

@Configuration

@EnableCaching

public class RedisConfig {

@Bean

public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory factory){

RedisTemplate<String, String> redisTemplate = new RedisTemplate<String,String>();

redisTemplate.setConnectionFactory(factory);

// 使用Jackson2JsonRedisSerialize 替换默认序列化

/**Jackson序列化 json占用的内存最小 */

Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);

ObjectMapper om = new ObjectMapper();

om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);

om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);

jackson2JsonRedisSerializer.setObjectMapper(om);

/**Jdk序列化 JdkSerializationRedisSerializer是最高效的*/

// JdkSerializationRedisSerializer jdkSerializationRedisSerializer = new JdkSerializationRedisSerializer();

/**String序列化*/

StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();

/**将key value 进行stringRedisSerializer序列化*/

redisTemplate.setKeySerializer(stringRedisSerializer);

redisTemplate.setValueSerializer(stringRedisSerializer);

/**将HashKey HashValue 进行序列化*/

redisTemplate.setHashKeySerializer(stringRedisSerializer);

redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);

redisTemplate.afterPropertiesSet();

return redisTemplate;

}

}

分布式锁代码实现类:

package com.xdclass.mobile.xdclassmobileredis.schedule;

import com.xdclass.mobile.xdclassmobileredis.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(LockNxExJob.class);

@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)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) {

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);

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;

}

private static Object genValue(final String key){

Object result = null;

ValueOperations<String, String> operations = redisTemplate.opsForValue();

result = operations.get(key);

}

}

lua脚本add.lua

local lockKey = KEYS[1]

local lockValue = KEYS[2]

-- setnx info

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

下面我们来讲解基于springboot的整合官方提供的setnx和setex命令连用的方式实战方案,此处我们采用的是springboot2.X里面spring-data-redis提供的redisTemplate的实现方案,此方案和lua脚本一样,需要引入上面的pom.xml和redisTemplate这两个步

package com.xdclass.mobile.xdclassmobileredis.schedule;

import com.xdclass.mobile.xdclassmobileredis.RedisService;

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.data.redis.core.RedisTemplate;

import org.springframework.scheduling.annotation.Scheduled;

import org.springframework.stereotype.Service;

import java.net.Inet4Address;

import java.net.InetAddress;

import java.net.NetworkInterface;

import java.net.UnknownHostException;

import java.util.Enumeration;

@Service

public class LockNxExJob {

private static final Logger logger = LoggerFactory.getLogger(LockNxExJob.class);

@Autowired

private RedisTemplate redisTemplate;

private static String LOCK_PREFIX = "prefix_";

@Scheduled(cron = "0/10 * * * * *")

public void lockJob() {

String lock = LOCK_PREFIX + "LockNxExJob";

boolean nxRet = false;

try{

//redistemplate setnx操作 setex

nxRet = redisTemplate.opsForValue().setIfAbsent(lock,getHostIp());

Object lockValue = redisService.genValue(lock);

//获取锁失败

if(!nxRet){

String value = (String)genValue(lock);

//打印当前占用锁的服务器IP

//logger.info("get lock fail,lock belong to:{}",value);

return;

}else{

redisTemplate.opsForValue().set(lock,getHostIp(),3600);

//Thread.sleep(30)

//获取锁成功

//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;

}

private static Object genValue(final String key){

Object result = null;

ValueOperations<String, String> operations = redisTemplate.opsForValue();

result = operations.get(key);

}

public static void main(String[] args) {

String localIP = "";

try {

localIP = getHostIp();

} catch (Exception e) {

e.printStackTrace();

}

//获取本机IP

System.out.println(localIP);

}

}

至此,我们采用Lua脚本和setIfAbsent 实现了redis分布式锁,大家如果对这两种实现方式有疑问的话可以欢迎和我私聊 ,QQ 1151427440,这篇文章出自小D课堂,转载注明出处。