redis脚本lua实现分布式锁,分布式锁

时间:2024-03-22 15:13:15

    项目是基于springboot 开发,前提要把redis环境配置好  

使用一个中心化的锁服务

首先,我们需要一个所有线程都可以访问到的地方来存储锁。这个锁只能存在于一个地方,从而保证只有一个权威的地方可以定义锁的建立和释放。

Redis是实现锁的一个理想的候选方案。作为一个轻量级的内存数据库,快速,事务性和一致性是选择redis所为锁服务的主要原因。

设计锁

锁本身是很简单的,就是redis数据库中一个简单的key。建立和释放锁,并保证绝对的安全,是这个锁的设计比较棘手的地方。有两个潜在的陷阱:

  1. 应用程序通过网络和redis交互,这意味着从应用程序发出命令到redis结果返回之间会有延迟。这段时间内,redis可能正在运行其他的命令,而redis内数据的状态可能不是你的程序所期待的。如果保证程序中获取锁的线程和其他线程不发生冲突?

  2. 如果程序在获取锁后突然crash,而无法释放它?这个锁会一直存在而导致程序进入“饿死”(原文成为“死锁”,感觉不太准确)。

建立锁

可能想到的最简单的方法是“用GET方法检查锁,如果锁不存在,就用SET方式设置一个值”。

这个方法虽然简单,但是不能保证独占锁。回顾前面所说的第1个陷阱:因为在GET和SET操作之间有延迟,我们没法知道从“发送命令”到“redis服务器返回结果”之间的这段时间内是否有其他线程也去建立锁。当然,这些都在几毫秒之内,发生的可能性相当低。但是如果在一个繁忙的环境中运行着大量的并发线程和命令,重叠的可能性并不是微不足道的。

为了解决这个问题,应该用SETNX命令。SETNX消除了GET命令需要等待返回值的问题,SETNX只有在key不存在时才返回成功。这意味着只有一个线程可以成功运行SETNX命令,而其他线程会失败,然后不断重试,直到它们能建立锁。

释放锁

一旦线程成功执行了SETNX命令,它就建立了锁并且可以基于资源进行工作。工作完成后,线程需要通过删除redis的key来释放这个锁,从而允许其他线程能尽快的获取锁。

尽管如此,也有需要小心的地方!回顾前面说的第2个陷阱:如果线程crash了,它永远都不会删除redis的key,所以这个锁会一直存在,从而导致“饿死”现象。那么如何避免这个问题呢?

锁的存活时间

我们可以给锁加一个存活时间(TTL),这样一旦TTL超时,这个锁的key会被redis自动删除。任何由于线程错误而遗留下来的锁在一个合适的时间之后都会被释放,从而避免了“饿死”。这纯粹是一个安全特性,更有效的方式仍然是确保尽量在线程里面释放锁。

可以通过PEXPIRE命令为Redis的key设置TTL,而且线程里可以通过MULTI/EXEC事务的方式在SETNX命令后立即执行,例如:

MULTI
SETNX lock-key
PEXPIRE 10000 lock-key
EXEC
  • 尽管如此,这会产生另外一个问题。PEXPIRE命令没有判断SETNX命令的返回结果,无论如何都会设置key的TTL。如果这个地方无法获取到锁或有异常,那么多个线程每次想获取锁时,都会频繁更新key的TTL,这样会一直延长key的TTL,导致key永远都不会过期。为了解决这个问题,我们需要Redis在一个命令里面处理这个逻辑。我们可以通过Redis脚本的方式来实现。

注意-如果不采用脚本的方式来实现,可以使用Redis 2.6.12之后版本SET命令的PX和NX参数来实现。为了考虑兼容2.6.0之前的版本,我们还是采用脚本的方式来实现。

   上面转载一段结束

   例子测试通过 并发100 请求1000次,仍然能得到很好的控制  

下面直接代码 


import java.util.Collections;
import java.util.UUID;

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.stereotype.Service;

/*
 *  使用redis的脚本实现分布式锁, 利用lua脚本原子性,返回锁资源
 *  完整的把分布工锁机制解决
*/
@Service
public class RedisLockLuaScript {
	
	@Autowired
	RedisTemplate<String, String> redisTemplate;
	
	
	 private static final Long SUCCESS = 1L;
	 public static final String LOCK_PRE_KEY = "nnl_";

	   // 加锁脚本
	    private static final String SCRIPT_LOCK = "if redis.call('setnx', KEYS[1], ARGV[1]) == 1 then redis.call('pexpire', KEYS[1], ARGV[2]) return 1 else return 0 end";
	    // 解锁脚本
	    private static final String SCRIPT_UNLOCK = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
	 
	    
	    public  boolean getLock(String key,String requestId) {
	        long start = System.currentTimeMillis();
	        StringBuilder lockScript = new StringBuilder();
	        lockScript.append("local ok = redis.call('setnx', 'nnl_")
	                .append(key)
	                .append("', ARGV[1]);if ok == 1 then redis.call('expire', 'nnl_")
	                .append(key)
	                .append("', 5) end; return ok");
	        while (true) {
	        	//返回参数不一样 按照类型分,主要看脚本返回什么,商品秒杀返回的是字符串,这里返回数字 5,
	        	DefaultRedisScript<Long> longDefaultRedisScript = new DefaultRedisScript<>(lockScript.toString(), Long.class);
	        	Long result  = redisTemplate.execute(longDefaultRedisScript, 
	        			Collections.singletonList(key),// KEYS[1]
	        			  requestId // ARGV[1]
	        			//  String.valueOf(expireTimeMilliseconds) // ARGV[2]
	        			);
	        	 Long  ret =(Long) result;
	            if (ret != null && ret == 1) 
	            	return true;
	            try {
	                Thread.sleep(1000);
	            } catch (InterruptedException e) {
	                e.printStackTrace();
	            }
	            if (System.currentTimeMillis() - start > 15 * 1000) {
	            	redisTemplate.delete(LOCK_PRE_KEY + key);
	            }
	        }
	    }
	    /**
如果一直依赖TTL来释放锁,效率会很低。Redis的SET操作文档就提供了一个释放锁的脚本:
	     * 释放分布式锁
	     * @param redisTemplate
	     * @param lockKey  锁
	     * @param requestId  请求标识
	     * @return 返回true表示释放锁成功
	     */
	    public  boolean releaseLock(String lockKey, String requestId) {
	    	
	     	DefaultRedisScript<Long> longDefaultRedisScript = new DefaultRedisScript<>(SCRIPT_UNLOCK.toString(), Long.class);
        	Long result  = redisTemplate.execute(longDefaultRedisScript, 
        			Collections.singletonList(lockKey),// KEYS[1]
        			  requestId // ARGV[1]  删除KEY的标识
        			//  String.valueOf(expireTimeMilliseconds) // ARGV[2]   过期时间
        			);
	        return SUCCESS.equals(result);
	    }

		 
	public static void main(String[] args) {
		//测试代码逻辑,放在测试包下运行   用UUID去获得锁,然后用uuid去删除这个锁
			String key="locking";
			String requestId = UUID.randomUUID().toString();
	/*		boolean ret = RedisLockLuaScript.getLock(key,requestId);
			 if(ret){
				 System.out.println("成功获取到锁: 执行业务逻辑 时间较长的话,可能会失败,超时等");
				 RedisLockLuaScript.releaseLock(key, requestId);
				 System.out.println("释放锁----"+requestId);
			 }else{
				 System.out.println("获取锁失败----");
			 }*/
	}

}

 

一段代码足以说明 问题,关键是要保证事务的原子性,删除的时候,必须是获得锁的线程去删除,删除标记就是验证删除的线程操作合法与否,如果不合法就失败; 我这里把超时时间设置成5秒,也可以把参数放开,自己随意设值

用 linux  ab 做压力测试

ab -n1000 -c100 -p data.json  -T application/json http://192.168.11.126:5887/redis/redisLockLua

 测试效果还是很不错

redis脚本lua实现分布式锁,分布式锁

redis脚本lua实现分布式锁,分布式锁

这里是更详细的文章 https://blog.csdn.net/thc1987/article/details/80355155