行业资讯 2025年08月6日
0 收藏 0 点赞 747 浏览 5626 个字
摘要 :

本文主要讲解关于实现Redis分布式锁的Lua脚本相关内容,让我们来一起学习下吧! 以下代码示例在并发情况下存在超卖情况: @RestController public class OverSell { ……

本文主要讲解关于实现Redis分布式锁的Lua脚本相关内容,让我们来一起学习下吧!

以下代码示例在并发情况下存在超卖情况:

@RestController
public class OverSell {

    @Autowired
    private RedisTemplate redisTemplate;

    @GetMapping(\"/overSell\")
    public String overSell(@RequestParam(\"key\") String key) {
        Integer sell = (Integer) redisTemplate.opsForValue().get(key);
        sell = sell - 1;
        if (sell > 0) {
            redisTemplate.opsForValue().set(key, sell);
        } else {
            return \"该商品已售罄\";
        }
        return MessageFormat.format(\"该商品剩余余额:{0}\", sell);
    }
}

上述代码在并发情况下是会存在线程安全问题的,如果应用部署在单机可以考虑使用synchronized或者ReentrantLock进程锁是可以保证线程安全的,但是生成考虑高可用性,一般都是分布式,在分布式部署情况下,进程级别的锁是不能保证线程安全的,需要使用分布式锁来实现线程安全,redis的setnx命令就可以实现分布式锁。

分布式锁示例:

@RestController
public class OverSell {

    @Autowired
    private RedisTemplate redisTemplate;

    @GetMapping(\"/overSell\")
    public String overSell(@RequestParam(\"key\") String key) {
        try {
            Boolean absent = redisTemplate.opsForValue().setIfAbsent(\"lock.\" + key, \"value\");
            if (absent) {
                Integer sell = (Integer) redisTemplate.opsForValue().get(key);
                sell = sell - 1;
                if (sell > 0) {
                    redisTemplate.opsForValue().set(key, sell);
                } else {
                    return \"该商品已售罄\";
                }
                return MessageFormat.format(\"该商品剩余余额:{0}\", sell);
            }
            return \"活动商品太火爆,请稍后再试\";
        } finally {
            redisTemplate.delete(\"lock.\" + key);
        }
    }
}

上述代码中redisTemplate.opsForValue().setIfAbsen就是redis的setnx命令效果,如果不存在就设置成功,存在就返回false,分布式锁初步完成。上述示例在执行代码逻辑过程中,如果出现宕机finally快逻辑为被执行,那么key商品的代码逻辑永远都不会走到,分布式锁的key永远存在,基于这种情况,就需要为分布式锁加上一个过去时间,加锁和设置过期时间最好用一个命令设置,否则也会用原子性问题:

@RestController
public class OverSell {

    @Autowired
    private RedisTemplate redisTemplate;

    @GetMapping(\"/overSell\")
    public String overSell(@RequestParam(\"key\") String key) {
        try {
            Boolean absent = redisTemplate.opsForValue().setIfAbsent(\"lock.\" + key, \"value\", 30, TimeUnit.SECONDS);
            if (absent) {
                Integer sell = (Integer) redisTemplate.opsForValue().get(key);
                sell = sell - 1;
                if (sell > 0) {
                    redisTemplate.opsForValue().set(key, sell);
                } else {
                    return \"该商品已售罄\";
                }
                return MessageFormat.format(\"该商品剩余余额:{0}\", sell);
            }
            return \"活动商品太火爆,请稍后再试\";
        } finally {
            redisTemplate.delete(\"lock.\" + key);
        }
    }
}

这样为分布式锁增加了一个过期时间,这样就算程序宕机,分布式锁也会过期,不会一直存在;但是如果代码逻辑复杂,执行时间超过了设置的过期时间,那么其它线程就会获取到分布式锁,等待执行完逻辑代码后,当前线程又把其它线程的锁给删除了,由于代码逻辑执行超过了过期时间,使得每个线程都在删除其它线程加的分布式,这样导致分布式锁失效,导致超卖问题。可以考虑给锁加一个唯一id,再解锁的时候判断一下是否是自己加的锁,如果是自己加的锁才删除,示例代码:

@RestController
public class OverSell {

    @Autowired
    private RedisTemplate redisTemplate;

    @GetMapping(\"/overSell\")
    public String overSell(@RequestParam(\"key\") String key) {
        String flag = UUID.randomUUID().toString();
        try {
            Boolean absent = redisTemplate.opsForValue().setIfAbsent(\"lock.\" + key, flag, 30, TimeUnit.SECONDS);
            if (absent) {
                Integer sell = (Integer) redisTemplate.opsForValue().get(key);
                sell = sell - 1;
                if (sell > 0) {
                    redisTemplate.opsForValue().set(key, sell);
                } else {
                    return \"该商品已售罄\";
                }
                return MessageFormat.format(\"该商品剩余余额:{0}\", sell);
            }
            return \"活动商品太火爆,请稍后再试\";
        } finally {
            if (flag.equals(redisTemplate.opsForValue().get(\"lock.\" + key))) {
                redisTemplate.delete(\"lock.\" + key);
            }
        }
    }
}

上述代码,如果并发量不是很高的情况下,可能不会有什么问题;如果并发量很高,代码刚判断好是自己加的锁时,锁的过期时间刚好到了,其它线程获取了分布式锁,那么又会出现删除不是自己的锁的情况,又会出现超卖问题;这个问题主要是因为过期时间导致的,在执行代码逻辑过程的时候,锁过期了,如果有另外一个线程,在主线执行代码逻辑的时候一直监听,如果逻辑没有执行,就不让锁过期,这种锁续命机制其实是有第三方框架实现好的,redisson框架

引入redisson依赖

        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.6.5</version>
        </dependency>

需要将redisson的bean注入到springboot容器中

    @Bean
    public Redisson redisson() {
        return (Redisson) Redisson.create(new Config(){{
            useSingleServer().setAddress(\"redis://localhost:6379\").setDatabase(0);
        }});
    }

redisson框架使用非常简单,逻辑大致是如果有一个线程获取到锁,执行代码逻辑,后台会每隔一段时间检查锁是否存在,如果存在给锁续命,其它线程如果没有获取到锁,会进行while循环,间隙性的获取锁,尝试一段时间后没有获取到锁会挂起不在自旋。代码逻辑如下

@RestController
public class OverSell {

    @Autowired
    private RedisTemplate redisTemplate;

    @Autowired
    private Redisson redisson;

    @GetMapping(\"/overSell\")
    public String overSell(@RequestParam(\"key\") String key) {
        String flag = UUID.randomUUID().toString();
        RLock lock = redisson.getLock(\"lock.\" + key);
        lock.lock();
        try {
            Integer sell = (Integer) redisTemplate.opsForValue().get(key);
            sell = sell - 1;
            if (sell > 0) {
                redisTemplate.opsForValue().set(key, sell);
            } else {
                return \"该商品已售罄\";
            }
            return MessageFormat.format(\"该商品剩余余额:{0}\", sell);
        } finally {
            lock.unlock();
        }
    }
}

redisson分布式锁原理:实现Redis分布式锁的Lua脚本

Redis Lua脚本

Redis在2.6推出了脚本功能,允许开发者使用Lua语言编写脚本传到Redis中执行。使用脚本的好处如下:

1、减少网络开销:本来5次网络请求的操作,可以用一个请求完成,原先5次请求的逻辑放在redis服务器上完成。使用脚本,减少了网络往返时延。这点跟管道类似。

2、原子操作:Redis会将整个脚本作为一个整体执行,中间不会被其他命令插入。管道不是原子的,不过redis的批量操作命令(类似mset)是原子的。

3、替代redis的事务功能:redis自带的事务功能很鸡肋,而redis的lua脚本几乎实现了常规的事务功能,官方推荐如果要使用redis的事务功能可以用redis lua替代。

官网文档上有这样一段话:

A Redis script is transactional by definition, so everything you can do with a Redis transaction, you can also do with a script, and usually the script will be both simpler and faster.

从Redis2.6.0版本开始,通过内置的Lua解释器,可以使用EVAL命令对Lua脚本进行求值。EVAL命令的格式如下:

EVAL script numkeys key [key ...] arg [arg ...]

script参数是一段Lua脚本程序,它会被运行在Redis服务器上下文中,这段脚本不必(也不应该)定义为一个Lua函数。numkeys参数用于指定键名参数的个数。键名参数 key [key …] 从EVAL的第三个参数开始算起,表示在脚本中所用到的那些Redis键(key),这些键名参数可以在 Lua中通过全局变量KEYS数组,用1为基址的形式访问( KEYS[1] , KEYS[2] ,以此类推)。

在命令的最后,那些不是键名参数的附加参数 arg [arg …] ,可以在Lua中通过全局变量ARGV数组访问,访问的形式和KEYS变量类似( ARGV[1] 、 ARGV[2] ,诸如此类)。例如

127.0.0.1:6379> eval \"return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}\" 2 key1 key2 first second
1) \"key1\"
2) \"key2\"
3) \"first\"
4) \"second\"

其中 “return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}” 是被求值的Lua脚本,数字2指定了键名参数的数量, key1和key2是键名参数,分别使用 KEYS[1] 和 KEYS[2] 访问,而最后的 first 和 second 则是附加参数,可以通过 ARGV[1] 和 ARGV[2] 访问它们。

在 Lua 脚本中,可以使用redis.call()函数来执行Redis命令

Jedis调用示例详见上面jedis连接示例:

jedis.set(\"product_stock_10016\", \"15\");  //初始化商品10016的库存
String script = \" local count = redis.call(\'get\', KEYS[1]) \" +
                \" local a = tonumber(count) \" +
                \" local b = tonumber(ARGV[1]) \" +
                \" if a >= b then \" +
                \"   redis.call(\'set\', KEYS[1], a-b) \" +
                \"   return 1 \" +
                \" end \" +
                \" return 0 \";
Object obj = jedis.eval(script, Arrays.asList(\"product_stock_10016\"), Arrays.asList(\"10\"));
System.out.println(obj);

注意,不要在Lua脚本中出现死循环和耗时的运算,否则redis会阻塞,将不接受其他的命令, 所以使用时要注意不能出现死循环、耗时的运算。redis是单进程、单线程执行脚本。管道不会阻塞redis。

以上就是关于实现Redis分布式锁的Lua脚本 相关的全部内容,希望对你有帮助。欢迎持续关注潘子夜个人博客(www.panziye.com),学习愉快哦!

微信扫一扫

支付宝扫一扫

版权: 转载请注明出处:https://www.zuozi.net/9895.html

管理员

相关推荐
2025-08-06

文章目录 一、Reader 接口概述 1.1 什么是 Reader 接口? 1.2 Reader 与 InputStream 的区别 1.3 …

988
2025-08-06

文章目录 一、事件溯源 (一)核心概念 (二)Kafka与Golang的优势 (三)完整代码实现 二、命令…

465
2025-08-06

文章目录 一、证明GC期间执行native函数的线程仍在运行 二、native线程操作Java对象的影响及处理方…

348
2025-08-06

文章目录 一、事务基础概念 二、MyBatis事务管理机制 (一)JDBC原生事务管理(JdbcTransaction)…

456
2025-08-06

文章目录 一、SnowFlake算法核心原理 二、SnowFlake算法工作流程详解 三、SnowFlake算法的Java代码…

517
2025-08-06

文章目录 一、本地Jar包的加载操作 二、本地Class的加载方法 三、远程Jar包的加载方式 你知道Groo…

832
发表评论
暂无评论

还没有评论呢,快来抢沙发~

助力内容变现

将您的收入提升到一个新的水平

点击联系客服

在线时间:08:00-23:00

客服QQ

122325244

客服电话

400-888-8888

客服邮箱

122325244@qq.com

扫描二维码

关注微信客服号