分布式锁之Redis6+Lua脚本实现原生分布式锁

本文详细介绍了分布式锁的概念、设计要点和实现方式,以Redis为例,探讨了加锁、解锁的原子性操作以及可能出现的问题。通过使用lua脚本解决了命令非原子性的问题,同时提出了锁自动续期的挑战,并提供了官方推荐的解决方案。
摘要由CSDN通过智能技术生成

简介

分布式锁核⼼知识介绍和注意事项

背景

就是保证同⼀时间只有⼀个客户端可以对共享资源进⾏操作

案例

优惠券领劵限制张数、商品库存超卖

核⼼

为了防⽌分布式系统中的多个进程之间相互⼲扰,我们需要⼀种分布式协调技术来对这些进程进⾏调度利⽤互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题避免共享资源并发操作导致数据问题

加锁

本地锁:synchronize、lock等,锁在当前进程内,集群部署下依旧存在问题
分布式锁:redis、zookeeper等实现,虽然还是锁,但是多个进程共⽤的锁标记,可以⽤Redis、Zookeeper、Mysql等都可以

设计分布式锁应该考虑的东⻄

1.排他性 --在分布式应⽤集群中,同⼀个⽅法在同⼀时间只能被⼀台机器上的⼀个线程执⾏
2.容错性 --分布式锁⼀定能得到释放,⽐如客户端奔溃或者⽹络中断
3.满⾜可重⼊、⾼性能、⾼可⽤
4.注意分布式锁的开销、锁粒度

基于Redis实现分布式锁

实现分布式锁 可以⽤ Redis、Zookeeper、Mysql数据库这⼏种 , 性能最好的是Redis

分布式锁离不开 key - value 设置
key 是锁的唯⼀标识,⼀般按业务来决定命名,⽐如想要给⼀种优惠券活动加锁,key 命名为 “coupon:id” 。value就可以使⽤固定值,⽐如设置成1

加锁 SETNX key value
解锁 del (key)
配置锁超时 expire (key,30s)

综合伪代码

1.setnx 的含义就是 SET if Not Exists,有两个参数setnx(key, value),该⽅法是原⼦性操作
2.如果 key 不存在,则设置当前 key 成功,返回 1
3.如果当前 key 已经存在,则设置当前 key 失败,返回 0
4.得到锁的线程执⾏完任务,需要释放锁,以便其他线程可以进⼊,调⽤ del(key)
5.客户端奔溃或者⽹络中断,资源将会永远被锁住,即死锁,因此需要给key配置过期时间,以保证即使没有被显式释放,这把锁也要在⼀定时间后⾃动释放

methodA(){
 String key = "coupon_66";
 if(setnx(key,1== 1{
	  expire(key,30,TimeUnit.MILLISECONDS)
	  try {
		 //做对应的业务逻辑
		 //查询⽤户是否已经领券
		 //如果没有则扣减库存
		 //新增领劵记录
	  } finally {
		 del(key)
	  }
 }else{
		 //睡眠100毫秒,然后⾃旋调⽤本⽅法
		 methodA()
	 }
}

存在什么问题?

多个命令之间不是原⼦性操作,如setnx和expire之间,如果setnx成功,但是expire失败,且宕机了,则这个资源就是死锁业务超时,存在其他线程勿删,key 30秒过期,假如线程A执⾏很慢超过30秒,则key就被释放了,其他线程B就得到了锁,这个时候线程A执⾏完成,⽽B还没执⾏完成,结果就是线程A删除了线程B加的锁

问题解决

使⽤原⼦命令:设置和配置过期时间 setnx / setex
如: set key 1 ex 30 nx
java⾥⾯redisTemplate.opsForValue().setIfAbsent(“seckill_1”,“success”,30,TimeUnit.MILLISECONDS)
可以在 del 释放锁之前做⼀个判断,验证当前的锁是不是⾃⼰加的锁, 那 value 应该是存当前线程的标识或者uuid
String key = "coupon_66"String value = Thread.currentThread().getId()
进⼀步细化误删当线程A获取到正常值时,返回带代码中判断期间锁过期了,线程B刚好重新设置了新值,线程A那边有判断value是⾃⼰的标识,然后调⽤del⽅法,结果就是删除了新设置的线程B的值核⼼还是判断和删除命令不是原⼦性操作导致

总结

加锁+配置过期时间:保证原⼦性操作
解锁: 防⽌误删除、也要保证原⼦性操作

解决解锁的原子性

前⾯说了redis做分布式锁存在的问题核⼼是保证多个指令原⼦性,加锁使⽤setnx setex 可以保证原⼦性,那解锁使⽤ 判断和删除怎么保证原⼦性

多个命令的原⼦性:采⽤ lua脚本+redis, 由于【判断和删除】是lua脚本执⾏,所以要么全成功,要么全失败

//获取lock的值和传递的值⼀样,调⽤删除操作返回1,否则返回0
String script = "if redis.call('get',KEYS[1])== ARGV[1] then returnredis.call('del',KEYS[1]) else return 0 end";
//Arrays.asList(lockKey)是key列表,uuid是参数
Integer result = redisTemplate.execute(newDefaultRedisScript<>(script, Integer.class),Arrays.asList(lockKey), uuid);
代码实现
import net.xdclass.xdclassredis.util.JsonData;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.time.Duration;
import java.util.Arrays;
import java.util.Random;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

@RestController
@RequestMapping("/api/v1/coupon")
public class CouponController {

    @Autowired
    private RedisTemplate<String,Object> redisTemplate;


    @GetMapping("add")
    public JsonData saveCoupon(@RequestParam(value = "coupon_id",required = true) int couponId){

        //防止其他线程误删
        String uuid = UUID.randomUUID().toString();

        String lockKey = "lock:coupon:"+couponId;

        lock(couponId,uuid,lockKey);

        return JsonData.buildSuccess();

    }


    private void lock(int couponId,String uuid,String lockKey){


        //lua脚本
        String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";

        Boolean nativeLock = redisTemplate.opsForValue().setIfAbsent(lockKey,uuid,Duration.ofSeconds(30));
        System.out.println(uuid+"加锁状态:"+nativeLock);
        if(nativeLock){
            //加锁成功

            try{
                //TODO 做相关业务逻辑
                TimeUnit.SECONDS.sleep(10L);

            } catch (InterruptedException e) {

            } finally {
                //解锁
                Long result = redisTemplate.execute( new DefaultRedisScript<>(script,Long.class),Arrays.asList(lockKey),uuid);
                System.out.println("解锁状态:"+result);

            }

        }else {
            //自旋操作
            try {
                System.out.println("加锁失败,睡眠5秒 进行自旋");
                TimeUnit.MILLISECONDS.sleep(5000);
            } catch (InterruptedException e) { }

            //睡眠一会再尝试获取锁
            lock(couponId,uuid,lockKey);
        }
    }


}

遗留⼀个问题

锁的过期时间,如何实现锁的⾃动续期或者避免业务执⾏时间过⻓,锁过期了?

1.原⽣⽅式的话,⼀般把锁的过期时间设置久⼀点,⽐如10分钟时间
原⽣代码+redis实现分布式锁使⽤⽐较复杂,且有些锁续期问题更难处理

2.框架 官⽅推荐⽅式:https://redis.io/topics/distlock 使⽤特别简单

下面是一个使用Lua脚本实现Redis分布式锁的代码示例: ```lua -- Lua脚本实现Redis分布式锁 local lockKey = 'lock' local uuid = ARGV\[1\] if redis.call('get', lockKey) == uuid then redis.call('del', lockKey) return 1 else return 0 end ``` 这段代码首先定义了一个锁的键名为`lockKey`,然后通过传入的参数`ARGV\[1\]`获取到要删除的锁的UUID。接下来,它会通过`redis.call('get', lockKey)`来获取当前锁的值,如果与传入的UUID相等,则说明当前锁是由该UUID持有的,此时会使用`redis.call('del', lockKey)`来删除锁,并返回1表示删除成功。如果锁的值与传入的UUID不相等,则说明当前锁不是由该UUID持有的,此时直接返回0表示删除失败。 这段代码可以用于实现Redis分布式锁的原子性删除操作,确保只有持有锁的客户端才能删除锁,避免误删锁的问题。同时,使用Lua脚本可以保证删除锁的操作是原子性的,避免并发情况下的竞争问题。 #### 引用[.reference_title] - *1* *2* [Redis 实现分布式锁+执行lua脚本](https://blog.csdn.net/qq_34285557/article/details/129700808)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] - *3* [Redis分布式锁问题(九)Redis + Lua 脚本实现分布式锁](https://blog.csdn.net/weixin_43715214/article/details/127982757)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

AloneDrifters

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值