天天看点

Redis Lua脚本使用(分布式锁)

Lua脚本

Lua由标准C编写而成,代码简洁优美,几乎在所有操作系统和平台上都可以编译,运行。一个完整的 Lua解释器不过200k,在目前所有脚本引擎中,Lua的速度是最快的。这一切都决定了Lua是作为嵌入式 脚本的最佳选择。

Redis中使用Lua脚本的好处

  • 多个请求合并成一个发送,减少请求次数
  • redis将一个lua脚本作为一个整体操作,提供了在redis中的原子操作
  • 客户端发送的脚本在redis中永久存在,这样其他客户端可以复用脚本
  • 速度快、可移植、源码小巧

Lua脚本使用(spring-boot)

开发环境

spring-boot项目,加入spring-boot-starter-data-redis依赖

<parent>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-parent</artifactId>
   <version>2.2.5.RELEASE</version>
   <relativePath/> <!-- lookup parent from repository -->
</parent>
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-redis</artifactId>
		</dependency>
	</dependencies>
	
           

在maven build中一定要加入关于脚本文件的resource,将lua脚本打包到classes目录中,如下

<build>
   <resources>
      <resource>
         <directory>${basedir}/src/main/java</directory>
         <!-- 编译时将lua脚本打包到classes目录中 -->
         <includes>
            <include>**/*.lua</include>
         </includes>
      </resource>
   </resources>
</build>
           

Lua脚本的编写

目录结构src/main/java/demo/script/xx.lua

使用redis中setnx对key进行加锁

local expire=tonumber(ARGV[2])
local ret=redis.call('set',KEYS[1],ARGV[1],'NX','PX',expire)
local strret=tostring(ret)
-- 用于查看结果,获取锁成功后返回随机结婚"table: 0x55d040f2edc0",否则返回false
redis.call('set','result',strret)
if strret == 'false' then
    return false
else
    return true
end
           

lock.lua

KEYS,ARGV关键字的意思:

KEYS[n]表示传入参数key主键中第一个主键值,0表示没有,可以传入多个,KEYS[2]传入的第二个主键

ARGV[n]表示传入的参数值,同样可以传入多个

解锁脚本

redis.call('del','result')
if redis.call('get',KEYS[1]) == ARGV[1] then
    return redis.call('del',KEYS[1])
else
    return 0
end
           

unlock.lua

Lua脚本读取封装

将封装好的lua脚本加入Spring容器中,方便调用

@Configuration
public class RedisScriptConfig {

    @Bean
    public RedisScript<Boolean> lockScript(){
        RedisScript<Boolean> redisScript=null;
        try {
            ScriptSource scriptSource=new ResourceScriptSource(new ClassPathResource("demo\\script\\lock.lua"));
            redisScript = RedisScript.of(scriptSource.getScriptAsString(), Boolean.class);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return redisScript;
    }

    @Bean
    public RedisScript<Long> unlockScript(){
        RedisScript<Long> redisScript=null;
        try {
            ScriptSource scriptSource = new ResourceScriptSource(new ClassPathResource("demo\\script\\unlock.lua"));
            redisScript=RedisScript.of(scriptSource.getScriptAsString(),Long.class);
        } catch (IOException e) {
            e.printStackTrace();
        }

        return redisScript;
    }
}
           

RedisTemplate调用lua脚本

@RestController
public class RedisLuaController {
    private final Log logger= LogFactory.getLog(RedisLuaController.class);

    @Autowired
    private RedisTemplate<String,String> redisTemplate;

    @Autowired
    RedisScript<Boolean> lockScript;

    @Autowired
    RedisScript<Long> unlockScript;

    @RequestMapping("distrLock/{key}/{uuid}")
    public void Lock(@PathVariable String key,@PathVariable String uuid){
        try {
            Boolean flag = redisTemplate.execute(lockScript, Collections.singletonList(key), uuid, "50000");
            logger.info("locked:{}"+flag);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @RequestMapping("distrUnlock/{key}/{uuid}")
    public void unlock(@PathVariable String key,@PathVariable String uuid){
        try {
            Long unlocked = redisTemplate.execute(unlockScript, Collections.singletonList(key), uuid);
            logger.info("unlock status "+unlocked.toString());
        } catch (Exception e) {
            logger.error("解锁失败"+e.getMessage());
        }
    }
}
           

总结

lua脚本在redis中体现的原子性大大为一些复杂操作提供了事务性保证,二是减少了网络开销和请求次数。这两点笔者认为最为重要。