openshell 的个人博客

一天很长,但十年很短。

Open Source, Open Mind,
Open Sight, Open Future!
  menu
110 文章
5051 浏览
1 当前访客
ღゝ◡╹)ノ❤️

使用Lua实现Redis分布式锁

安装

wget http://www.lua.org/ftp/lua-5.3.0.tar.gz
tar -xvzf lua-5.3.0.tar.gz
cd lua-5.3.0
make linux test

测试环境的时候报错:

lua.c:80:31: fatal error: readline/readline.h: No such file or directory

解决办法:

yum install readline-devel -y

继续执行安装:

make install

编写脚本

基本语法:https://www.w3cschool.cn/lua/lua-basic-syntax.html

print('请输入一个数字')
while(true) do
local a= io.read("*num");
if(a>100) then
print ('太大了')
elseif(a<100) then
print('太小了')
else
print('恭喜,你猜对了')
break
end;
end;

执行脚本:

lua script.lua

redis中使用lua

:0>eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 arg1 arg2
 1)  "key1"
 2)  "key2"
 3)  "arg1"
 4)  "arg2"

image.png

1)普通互斥锁

  • 获取锁:直接使用客户端的setnx ex 命令即可,无需脚本
  • 释放锁:因为要判断锁中的表示是否是自己的,因此需要脚本,如下:
-- 判断锁是否是自己的
if(redis.call('GET',KEYS[1]) == ARGV[1])then
	-- 
	return redis.call('DEL',KEYS[1])
end
-- 不是则直接返回
return 0

参数含义:

  • KEY[1]:就是锁key,比如“lock”
  • ARGV[1]:就是线程的唯一标识,可以是随机字符串

2)可重入锁

新建一个Java Maven工程,

image.png

依赖如下:

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    </dependencies>

application.yml:

spring:
  redis:
    database: 0
    port: 6379
    host: 192.168.247.130
    password: 123456

RedisLock:

package cn.caiqz.util;

/**
 * <p>
 * redis锁接口
 * </p>
 *
 * @author openshell
 * @since 2020-09-20
 */
public interface RedisLock {

    /**
     * 获取锁
     *
     * @param releaseTime 锁的自动释放时间
     * @return 获取锁是否成功
     */
    boolean tryLock(long releaseTime);

    /**
     * 释放锁
     */
    void unlock();
}

RedisLockFactory:

package cn.caiqz.util;

import cn.caiqz.util.impl.ReentrantRedisLock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

/**
 * <p>
 * redis锁工厂
 * </p>
 *
 * @author openshell
 * @since 2020-09-20
 */
@Component
public class RedisLockFactory {

    @Autowired
    private StringRedisTemplate redisTemplate;

    public RedisLock getReentrantRedisLock(String key) {
        return new ReentrantRedisLock(redisTemplate, key);
    }
}

ReentrantRedisLock:

package cn.caiqz.util.impl;

import cn.caiqz.util.RedisLock;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.scripting.support.ResourceScriptSource;

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

/**
 * <p>
 * 可重入锁
 * </p>
 *
 * @author openshell
 * @since 2020-09-20
 */
@Slf4j
public class ReentrantRedisLock implements RedisLock {
    /**
     * 锁对应的key
     */
    private String key;

    /**
     * 锁释放时间
     */
    private String releaseTime;

    private StringRedisTemplate stringRedisTemplate;


    public ReentrantRedisLock(StringRedisTemplate stringRedisTemplate, String key) {

        this.stringRedisTemplate = stringRedisTemplate;
        this.key = key;
    }

    /**
     * 加锁脚本
     */
    private static final DefaultRedisScript<Long> LOCK_SCRIPT;

    /**
     * 解锁脚本
     */
    private static final DefaultRedisScript<Object> UNLOCK_SCRIPT;


    //初始化lua脚本
    static {
        //加载获取锁脚本
        LOCK_SCRIPT = new DefaultRedisScript<Long>();
        LOCK_SCRIPT.setScriptSource(new ResourceScriptSource(new ClassPathResource("lock.lua")));
        LOCK_SCRIPT.setResultType(Long.class);

        //加载释放锁脚本
        UNLOCK_SCRIPT = new DefaultRedisScript<Object>();
        UNLOCK_SCRIPT.setScriptSource(new ResourceScriptSource(new ClassPathResource("unlock.lua")));
    }


    /**
     * 存入线程信息的前缀,防止与其他jvm中的线程信息冲突
     */
    private final String ID_PREFIX = UUID.randomUUID().toString();

    public boolean tryLock(long releaseTime) {
        //记录释放时间
        this.releaseTime = String.valueOf(releaseTime);
        //执行脚本
        Long result = stringRedisTemplate.execute(LOCK_SCRIPT, Collections.singletonList(key), ID_PREFIX + Thread.currentThread().getId(), this.releaseTime);

        return result != null && result.intValue() == 1;
    }

    public void unlock() {
        //执行脚本
        stringRedisTemplate.execute(UNLOCK_SCRIPT, Collections.singletonList(key), ID_PREFIX + Thread.currentThread().getId(), this.releaseTime);
    }
}

编写lua脚本 lock.lua

local key = KEYS[1]; --锁的key
local threadId = ARGV[1] --线程唯一标识
local releaseTime = ARGV[2] --锁的自动释放时间
if(redis.call('exists',key)==0) then --判断是否存在
    redis.call('hset',key,threadId,'1'); --不存在,获取锁
    redis.call('expire',key,releaseTime); -- 设置有效期
    return 1; --返回结果
end;
if(redis.call('hexists',key,threadId)==1) then   --锁已经存在,判断threadId是否是自己的
    redis.call('hincrby',key,threadId,'1'); -- 存在,获取锁,重入次数+1
    redis.call('expire',key,releaseTime); -- 设置有效期
end;
return 0;

unlock.lua:

local key = KEYS[1]; --锁的key
local threadId = ARGV[1] --线程唯一标识
local releaseTime = ARGV[2] --锁的自动释放时间

if(redis.call('hexists',key,threadId)==0 )then --判断当前锁是否还是自己持有
    return nil; -- 如果已经不是自己,则直接返回
end;
local count = redis.call('hincrby',key,threadId,-1) -- 是自己的锁,则重入次数-1key
if(count >0 ) then --判断是否重入次数是否已经为0
    redis.call('expire',key,releaseTime); --大于0 说明不能释放锁,重置有效期后返回
    return nil;
else
    redis.call('del',key); -- 等于0说明可以释放锁,直接删除
    return nil;
end;

测试demo,ClearOrderTask:

package cn.caiqz.task;

import cn.caiqz.util.RedisLock;
import cn.caiqz.util.RedisLockFactory;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

/**
 * <p>
 * 订单清理任务
 * </p>
 *
 * @author openshell
 * @since 2020-09-20
 */
@Slf4j
@Component
public class ClearOrderTask {
    @Autowired
    private RedisLockFactory redisLockFactory;

    @Scheduled(cron = "0/10 * * ? * *")
    public void clearOrderTask() throws InterruptedException {
        //获取锁对象
        RedisLock lock = redisLockFactory.getReentrantRedisLock("lock1");
        //尝试获取锁
        boolean isLock = lock.tryLock(50);
        if (!isLock) {
            //获取锁失败
            log.error("获取锁失败,定时任务终止!");
            return;
        }
        try {
            //获取锁成功
            log.info("获取锁成功,开始任务!");
            clearOrder();
        } finally {
            log.warn("任务执行完毕,释放锁!");
            lock.unlock();
        }

    }

    public void clearOrder() throws InterruptedException {
        log.info("开始清理订单");
        Thread.sleep(50000);
        log.info("开始恢复库存");
    }
}

将服务拷贝一份,用不同端口启动,即可看到效果:

image.png


标题:使用Lua实现Redis分布式锁
作者:openshell
地址:http://solo.caiqz.cn/articles/2020/09/18/1600441856988.html