lua脚本实现Redis分布式锁

262 阅读3分钟

写一个demo来简单的实现Redis分布式锁

一,导入需要的依赖

<!--redis启动类依赖-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--lua脚本依赖-->
<dependency>
    <groupId>org.luaj</groupId>
    <artifactId>luaj-jse</artifactId>
    <version>3.0.1</version>
</dependency>

二,写一个工具类,用来集成上锁和解锁的操作

package com.atguigu.gulimall.product.config;

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.data.redis.core.script.RedisScript;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.util.Collections;

@Component
public class RedisLock {
    @Autowired
    private RedisTemplate redisTemplate;

    private final RedisSerializer stringRedisSerializer = new StringRedisSerializer();

    private final RedisScript<Boolean> lockScript;

    private final RedisScript<Boolean> unlockScript;

    public RedisLock() throws IOException {
//        this.redisTemplate = redisTemplate;
        this.lockScript = new DefaultRedisScript<>("local lockKey = KEYS[1]\n" +
                "local lockValue = ARGV[1]\n" +
                "local lockExpire = tonumber(ARGV[2])\n" +
                "\n" +
                "if redis.call("setnx", lockKey, lockValue) == 1 then\n" +
                "    redis.call("expire", lockKey, lockExpire)\n" +
                "    return true\n" +
                "else\n" +
                "    return false\n" +
                "end", Boolean.class);
        this.unlockScript = new DefaultRedisScript<>("return redis.call('del', KEYS[1]) == 1", Boolean.class);
    }

    public boolean lock(String key, String value, long expire) {
        Object result = redisTemplate.execute(lockScript, stringRedisSerializer, stringRedisSerializer, Collections.singletonList(key), value, String.valueOf(expire));
        return result != null && (Boolean) result;
    }

    public boolean unlock(String key, String value) {
        Object result = redisTemplate.execute(unlockScript, stringRedisSerializer, stringRedisSerializer, Collections.singletonList(key), value);
        return result != null && (Boolean) result;
    }
}

三,这个Bean需要注入的RedisTemplate

@Bean
public RedisTemplate getRedisTemplate(RedisConnectionFactory connectionFactory){
    RedisTemplate<String, Object> template = new RedisTemplate<>();
    template.setConnectionFactory(connectionFactory);
    template.setKeySerializer(new StringRedisSerializer());
    template.setValueSerializer(new Jackson2JsonRedisSerializer<>(Object.class));
    return template;
}

四,配置文件里面需要有Redis的连接地址,端口号,我这里密码没有设置。

spring:
  redis:
    host: 127.0.0.1
    port: 6379
    password:

五,写一个接口用来进行测试

package com.atguigu.gulimall.product.controller;

import com.atguigu.common.utils.R;
import com.atguigu.gulimall.product.config.RedisLock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.UUID;

@RestController
@RequestMapping("redisluatest")
public class RedisLuaTest {
    @Autowired
    private RedisLock redisLock;
    @RequestMapping("test")
    public R test(@RequestBody String orderId){
        String lockKey = "order_lock_" + orderId;
        String lockValue = UUID.randomUUID().toString();
        long expire = 30000L; // 30秒过期时间

        try {
            if (redisLock.lock(lockKey, lockValue, expire)) {
                // 获取锁成功,执行业务代码
                System.out.println("获取锁成功");
            } else {
                // 获取锁失败,提示用户稍后再试
                System.out.println("获取锁失败");
            }
        } finally {
            // 释放锁
            System.out.println("释放锁");
            redisLock.unlock(lockKey, lockValue);
        }
        return R.ok();
    }
}

六,测试结果

执行上锁操作 image.png

使用可视化工具查看Redis是否有这个key image.png

可以看到已经加锁成功,这里\xac\xed\x00\x05t\x00\x14是因为在RedisTemplate中使用了StringRedisSerializer对key进行序列化。StringRedisSerializer使用UTF-8编码将Java字符串转换为字节数组,并在字节数组前添加一个长度前缀。这个长度前缀用于在读取键时确定字节数组的长度,以便正确地反序列化Java字符串。

在示例代码中,\xac\xed\x00\x05t\x00\x14是长度前缀,表示后面的字节数组的长度为20字节。order_lock_orderId:7是实际的键名。因此,实际的键名是order_lock_orderId:7,但是在存储时,会使用StringRedisSerializer将它转换为\xac\xed\x00\x05t\x00\x14order_lock_orderId:7这种形式。

这种序列化方式可以确保键名在存储和读取时都能正确地转换为字节数组和Java字符串。但是,它也会使键名变得难以阅读和调试。如果您希望键名更易于阅读和调试,可以尝试使用其他的序列化方式,如Jackson JSON序列化器或JdkSerializationRedisSerializer。 image.png

继续往下走,进行释放锁的操作 image.png

可以看到已经成功将锁释放掉了

七,需要注意的几个问题

以上只是lua脚本实现分布式锁的简单应用,实际我们需要考虑的东西会更多,列如

  1. 锁的过期时间应该设置得足够短,以避免锁被长时间占用。但是,如果锁的过期时间设置得太短,可能会导致任务无法完成。因此,需要根据实际情况来设置锁的过期时间。
  2. 在获取锁时,需要传递一个唯一的锁值,以确保每个线程都可以获得唯一的锁。锁值可以使用UUID来生成。
  3. 在释放锁时,需要确保只有持有锁的线程才能释放锁。因此,在释放锁时,应该检查锁值是否匹配,以确保只有持有锁的线程才能释放锁,并且注意业务代码没有执行完锁就过期了的情况,需要设置看门狗来给锁续时间。
  4. 在使用Lua脚本时,应该确保脚本的正确性和安全性。不正确的脚本可能会导致锁无法正常工作,甚至可能会导致数据丢失或安全漏洞。因此,应该使用可靠的脚本,并遵循最佳实践来编写和使用Lua脚本。