redis分布式锁详解、代码实现以及优化

1、分布式锁是什么 

    * 分布式锁是控制分布式系统或不同系统之间共同访问共享资源的一种锁实现

    * 如果不同的系统或同一个系统的不同主机之间共享了某个资源时,往往通过互斥来防止彼此干扰。

2、 分布锁设计目的

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

3、设计要求

      * 这把锁要是一把可重入锁(避免死锁)
      * 这把锁有高可用的获取锁和释放锁功能
      * 这把锁获取锁和释放锁的性能要好… 

分布锁实现方案一:setnx、setex命令连用  

      * 获取锁的时候,使用 setnx(SETNX key val:当且仅当 key 不存在时,set 一个 key 为 val 的字符串,返回 1;
      * 若 key 存在,则什么都不做,返回 【0】加锁,锁的 value 值为当前占有锁服务器内网IP编号拼接任务标识
      * 在释放锁的时候进行判断。并使用 expire 命令为锁添 加一个超时时间,超过该时间则自动释放锁。 
      * 返回1则成功获取锁。还设置一个获取的超时时间, 若超过这个时间则放弃获取锁。setex(key,value,expire)过期以秒为单位
      * 释放锁的时候,判断是不是该锁(即Value为当前服务器内网IP编号拼接任务标识),若是该锁,则执行 delete 进行锁释放

     

*代码实现:

/*
 * 描述:
 *  使用 setnx+setex方式进行分布式锁的实现
 *  setnx如果存在则返回(0)false,如果不存在则set成功并且返回(1)true
 *  setex设置key的value,并且设置有效时间
 *  
 * 缺陷:
 *  无法保证锁操作的原子性,非代码上的原子性。
 *  也就是说在setnx执行完成之后,在setex未执行时,redis节点宕机,此时锁key的过期时间是永久,
 *  也就是说锁永远不会被执行,所有业务线程都无法获取锁,业务代码将无法正常执行
 */
@Scheduled(cron="0/2 * * * * *")
public void lockSet(){
    int ai = a.getAndDecrement();
    String lockname = "lock_" + this.getClass().getName() + "_lockSet";
    try {
        //redisTemplate  setnx
	    Boolean bool =         
        redisTemplate.opsForValue().setIfAbsent(lockname,IpUtils.getLocalHostIp());
	    if (!bool) {//获取失败
	    	String value = (String) redisService.get(lockname);
	    	//打印当前占用锁的服务器IP
	    	logger.info("获取redis分布式锁失败,当前锁的占有服务节点为:{}", value+"_"+ai);
	    	return;
	    } else {//获取成功
	        redisTemplate.opsForValue().set(lockname, IpUtils.getLocalHostIp(), 10000);
	    	//获取锁成功
		    logger.info("获取锁success:{}",lockname+"_"+ai);
		    //Thread.sleep(5000);
	    } 
    } catch (Exception e) {
	    logger.error("lock error",e);
    }finally {
	    redisService.remove(lockname);
    }
}

分布锁实现方案二:  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方法执行脚本   

          lua eval http://doc.redisfans.com/script/eval.html
 

lua脚本

-- 传进来的变量1
local lockKey = KEYS[1]
-- 传进来的变量2
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,3000, lockValue)  -- setex
return result_1  -- 返回setnx结果
else
return result_1  --获取锁失败,返回setnx结果
end

案例代码:

package com.bjdd.redis.service.lock;

import java.util.ArrayList;
import java.util.List;

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.data.redis.core.script.DefaultRedisScript;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;

import com.bjdd.redis.service.redis.RedisService;
import com.bjdd.redis.utils.IpUtils;

@Service
public class LuaDistributeLock {

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

	@Autowired
	private RedisService redisService;
	@Autowired
	private RedisTemplate redisTemplate;

	@Scheduled(cron = "0/1 * * * * *")
	public void lockJob() {

		String lockname = "lua_lock_" + this.getClass().getName() + "_lockSet";
		boolean luaRet = false;
		try {
			
			luaRet = luaExpress(lockname, IpUtils.getLocalHostIp());

			// 获取锁失败
			if (!luaRet) {
				String value = (String) redisService.get(lockname);
				// 打印当前占用锁的服务器IP
				logger.info("lua获取锁失败:{}", value);
				return;
			} else {
				// 获取锁成功
				logger.info("lua获取锁成功:{}",Thread.currentThread().getName());
			}
		} catch (Exception e) {
			logger.error("lock error", e);

		} finally {
			if (luaRet) {
				logger.info("release lock success");
				redisService.remove(lockname);
			}
		}
	}

	/**
	 * 获取lua结果
	 * 
	 * @param key
	 * @param value
	 * @return
	 */
	public Boolean luaExpress(String key, String value) {
		DefaultRedisScript<Boolean> lockScript = new DefaultRedisScript<Boolean>();
		lockScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("lua/addLock.lua")));
		lockScript.setResultType(Boolean.class);
		return lockScript;
        // 封装参数
		List<Object> keyList = new ArrayList<Object>();
		keyList.add(key);
		keyList.add(value);
		Boolean result = (Boolean) redisTemplate.execute(lockScript, keyList);
		return result;
	}

}

 

分布锁实现方案三:  RedisConnection实现分布锁( 实现setnx和setex命令连用)(简化开发,比lua简单易懂便于排错)

**简介:RedisConnection实现分布锁的方式,采用redisTemplate操作redisConnection
              实现setnx和setex两个命令连用**

              - redisTemplate本身有没通过valueOperation实现分布式锁

通过官网api分析
             https://docs.spring.io/spring-data/redis/docs/1.5.0.RELEASE/api/
             https://docs.spring.io/spring-data/redis/docs/2.0.13.RELEASE/api/

代码实现如下:

package com.bjdd.redis.service.lock;

import javax.annotation.Resource;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisStringCommands;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.types.Expiration;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;

import com.bjdd.redis.service.redis.RedisService;
import com.bjdd.redis.utils.IpUtils;

@Service
public class RedisConnectionLockService {

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

	@Resource
	private RedisTemplate<String, Object> redisTemplate;

	@Autowired
	private RedisService redisService;
	
	
	@Scheduled(cron = "0/10 * * * * *")
    public void lockJob() {

		String lockname = "lock_RedisConnection" + this.getClass().getName() + "_lockSet";
        boolean lockRet = false;

        try {
            lockRet = this.setLock(lockname, 2);
            if (!lockRet) {//获取失败
				String value = (String) redisService.get(lockname);
				//打印当前占用锁的服务器IP
				logger.info("获取RedisConnection(setnx+setex连用)分布式锁失败,当前锁的占有服务节点为:{}", value);
				return;
			} else {//获取成功
				redisTemplate.opsForValue().set(lockname, IpUtils.getLocalHostIp(), 2000);
				//获取锁成功
				logger.info("获取RedisConnection(setnx+setex连用)success:{}",lockname);
			} 
        } catch (Exception e) {
            logger.error("jedisLockJob lock error", e);

        } finally {
            if (lockRet) {
                logger.info("jedisLockJob release lock success:{}",lockname);
                redisService.remove(lockname);
                System.out.println("---------------"+redisService.get(lockname));
            }
        }
    }


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

}

分布锁实现方案四:  RedisConnection实现分布锁( 实现setnx和setex命令连用,采用lua脚本做高可用分布式锁的优化)(推荐使用)

解锁的流程分析:

  ​     当某个锁需要持有的时间小于锁超时时间时会出现两个进程同时执行任务的情况,
  ​     这时候如果进程没限制只有占有这把锁的人才能解锁的原则就会出现,A解了B的锁。

方案一、方案二、方案三都存在该问题,问题说明如下:

服务器server1上的线程A

服务器server2上的线程B

 

- 采用lua脚本做解锁流程优化讲解,,优化:

优化的代码:

lua脚本

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

java代码

package com.bjdd.redis.service.lock;

import java.util.ArrayList;
import java.util.List;

import javax.annotation.Resource;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ClassPathResource;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisStringCommands;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.types.Expiration;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.scripting.support.ResourceScriptSource;
import org.springframework.stereotype.Service;

import com.bjdd.redis.service.redis.RedisService;
import com.bjdd.redis.utils.IpUtils;

@Service
public class LuaDistributedLockService2 {

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

	private static String LOCK_PREFIX = "Lua_Jedis_DistributedLock_";
	
	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/3 * * * * *")
	public void lockJob() {

		String lockname = "lock_RedisConnection" + this.getClass().getName() + "_lockSet";
		boolean lockRet = false;
		try {
            lockRet = this.setLock(lockname, 2);
            if (!lockRet) {//获取失败
				String value = (String) redisService.get(lockname);
				//打印当前占用锁的服务器IP
				logger.info("获取Lua+RedisConnection(setnx+setex连用)分布式锁失败,当前锁的占有服务节点为:{}", value);
				return;
			} else {//获取成功
				redisTemplate.opsForValue().set(lockname, IpUtils.getLocalHostIp(), 2000);
				//获取锁成功
				logger.info("获取Lua+RedisConnection(setnx+setex连用)success:{}",lockname);
			} 
        } catch (Exception e) {
            logger.error("jedisLockJob lock error", e);

		} finally {
			if (lockRet) {
				logger.info("jedisLockJob release lock success");
				releaseLock(lockname, IpUtils.getLocalHostIp());
			}
		}
	}

	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(), IpUtils.getLocalHostIp().getBytes(), Expiration.seconds(expire),
							RedisStringCommands.SetOption.ifAbsent());
				}
			});
			return result;
		} catch (Exception e) {
			logger.error("set redis occured an exception", e);
		}
		return false;
	}

	/**
	 * 释放锁操作
	 * 
	 * @param key
	 * @param value
	 * @return
	 */
	private boolean releaseLock(String key, String value) {
		lockScript = new DefaultRedisScript<Boolean>();
		lockScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("lua/yewu1/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;
	}

}

提醒:以上代码均建立在非并发编程基础上进行的,若考虑并发编程,请使用java.util.concurrent.locks.ReentrantLock进行具体业务的具体锁实现的并发编程代码。

 

分布锁实现方案五:使用redisson实现分布式锁(最终推荐使用

太简单,不作说明,原理一句话,通过setnx设置TTL(如30s)后,开启一个子线程进行监听,当setnx获取锁之后,在到达1/3TTL(10s)时刻的时候为该所重新设置TTL为30s(俗称续命),这些流程redissson已经全部封装好了

redisson小demo:https://blog.csdn.net/qq_22049773/article/details/109454188

 

 

 

 

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值