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

  • 分布式锁之Redis6+Lua脚本实现原生分布式锁
  • 分布式核心技术—关于高并发下分布式锁
  • 分布式锁核心知识介绍
  • 背景
  • 避免共享资源并发操作导致数据问题
  • 分布式锁应该考虑的东西
  • 基于Redis实现分布式锁
  • 详述基于Redis实现分布式锁的几种坑
  • 实现分布式锁
  • 基于Redis实现分布式锁
  • 存在的问题
  • 分布式锁lua脚本+redis原生代码编写
  • 上文说了redis做分布式锁存在的问题
  • 代码实现


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

分布式核心技术—关于高并发下分布式锁

分布式锁核心知识介绍

背景
  • 就是保证同一时间只有一个客户可以对共享资源进行操作
  • 案例:优惠券领取限制张数、商品库存超卖
  • 核心
  • 为了防止分布式系统中的多个进程之间进行互相干扰,我们需要一种分布式协调技术来对这些进行进行调度
  • 利用互斥机制来控制共享资源的访问,这就是分布式要解决的问题
避免共享资源并发操作导致数据问题
  • 加锁
  • 本地锁:synchronize、lock等,锁在当前进程内,本地集群部署下依旧存在问题
  • 分布式锁
  • redis、zookeeper等实现,虽然还是锁,但多个进程共同标记,可以用Redis、Zookeeper、MySQL等都可以

lua脚本实现分布式集群限流 令牌桶算法 时间戳 lua脚本实现分布式锁_数据库

分布式锁应该考虑的东西
  • 排他性
  • 在分布式应用集群中,同一个方法在同归时间只能被一台机器上的一个线程执行
  • 容错性
  • 分布式锁一定能得到释放,比如客户端崩溃或是网络终端
  • 满足可重入、高性能、高可用
  • 应该注意分布式锁的开销、锁粒度

基于Redis实现分布式锁

详述基于Redis实现分布式锁的几种坑

实现分布式锁
  • 可以使用Redis、Zookeeper、MySQL数据库这几种,性能最好的是Redis且是最容易理解的
  • 分布式锁离不开key-value设置
  • key 是锁的唯⼀标识,⼀般按业务来决定命名,⽐如想要给⼀种优惠券活动加锁, key 命名为 “coupon:id” 。 value就可以使用固定值,比如设置成1

基于Redis实现分布式锁

  • 文档
  • setnx 的含义就是 SET if Not Exists,有两个参数
  • setnx(key, value),该⽅法是原⼦性操作
  • 如果 key 不存在,则设置当前 key 成功,返回 1;
  • 如果当前 key 已经存在,则设置当前 key 失败,返回 0
  • 解锁del(key)
  • 得到锁的线程执行完任务,需要释放锁,以便其他线程可以进入,调用del
  • 配置锁超时expire(ket,30s)
  • 客户端网络崩溃或网络中断,资源将会永远被锁住,即死锁,因此需要给key配置过期时间,以保证及时没有被显示释放,这把锁也要再一定时间后自动释放
  • 综合伪代码
merhod(){
    String key = "coupon_66";
    if(setnx(key, 1) == 1) {
		expire(key,30,TimeUnit.MILLISECONDS);
        try {
		//做对应的业务逻辑
		//查询⽤户是否已经领券
		//如果没有则扣减库存
		//新增领劵记录
		} finally {
			del(key)
		}
	}else{
	//睡眠100毫秒,然后⾃旋调⽤本⽅法
	methodA(){
        
    }
}
  • 存在哪些问题呢?
存在的问题
  • 多个命令之间不是原子性操作,入sebnxexpire之间如果sebnx成功了,但是expire失败,且宕机了,则至厄瓜资源就是死锁
  • 使用原子命令:
  • 设置和配置过期时间 setnx / setex
  • 如: set key 1 ex 30 nx
  • java里面
  • redisTemplate.opsForValue().setIfAbsent(“seckill_1”,“success”,30,TimeUnit.MILLISECONDS)

lua脚本实现分布式集群限流 令牌桶算法 时间戳 lua脚本实现分布式锁_数据库_02

  • 业务超时,存在其他线程误删,key 30秒过期,假如线程A执行很慢超过30秒,则key就被释放了,其他线程B就得到了锁,这个时候线程A执行完成,而B还没执行完成,结果就是线程A删除了线程B加的锁
可以在 del 释放锁之前做⼀个判断,验证当前的锁是不是⾃
⼰加的锁, 那 value 应该是存当前线程的标识或者uuid
String key = "coupon_66"
String value = Thread.currentThread().getId()
if(setnx(key, value) == 1) {
expire(key,30,TimeUnit.MILLISECONDS)
	try {
	//做对应的业务逻辑
	} finally {
		//删除锁,判断是否是当前线程加的
		if(get(key).equals(value)){
		//还存在时间间隔
		del(key)
			
		}
		}else{
		//睡眠100毫秒,然后⾃旋调⽤本⽅法
		}
    }
  • 进一步细化误删
  • 当线程A获取到正常值时,返回带代码中判断期间锁过期了,线程B刚好重新设置了新值,线程A那边有判断value是自己的标识,然后调⽤del方法,结果就是删除了新设置的线程B的值
  • 核心还是判断和删除命令 不是原子性操作导致
  • 总结
  • 加锁+配置过期时间:保证原⼦性操作
  • 解锁:防止误删除、也要保证原⼦性操作

分布式锁lua脚本+redis原生代码编写

上文说了redis做分布式锁存在的问题

  • 核心是保证多个指令原子性,加锁使用setnx setex 可以保证原子性,那解锁使用判断和删除怎么保证原子性
  • ⽂档: http://www.redis.cn/commands/set.html
  • 多个命令的原子性:采用 lua脚本+redis, 由于【判断和删除】是lua脚本执行,所以要么全成功,要么全失败
//获取lock的值和传递的值⼀样,调⽤删除操作返回1,否则返回0

String script = "if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end"
  
//Arrays.asList(lockKey)是key列表, uuid是参数
Integer result = redisTemplate.execute(new DefaultRedisScript<>(script,Integer.class),Arrays.asList(lockKey), uuid);

代码实现

  • 全部代码
package net.xdclass.xdclassredis.controller;

import net.xdclass.xdclassredis.util.JsonData;
import org.springframework.beans.factory.annotation.Autowired;
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.util.Arrays;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

/**
 * @Author: ruan
 * Date: 2021/7/12 9:34
 * @Description: 原生分布式锁
 */
@RequestMapping("/api/v1/coupon")
@RestController
public class CouponController {

    @Autowired
    private StringRedisTemplate redisTemplate;

    @GetMapping("add")
    public JsonData saveCoupon(@RequestParam(value = "coupon_id",required = true) int couponId){
        lock(couponId);
        return JsonData.buildSuccess();
    }

    /**
     * 封装分布式锁加锁方法
     * @param couponId 锁id
     */
    private void lock(int couponId){

        //防止其他线程误删
        String uuid = UUID.randomUUID().toString();
        //数据的key
        String lockKey = "lock:coupon:" + couponId;
        //开始加锁
        Boolean nativeLock = redisTemplate.opsForValue().setIfAbsent(lockKey, uuid, 30, TimeUnit.SECONDS);
        System.out.println("加锁状态:" + nativeLock);
        //lua脚本
        String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
        //判断加锁状态,进行业务处理
        if (nativeLock){
            //加锁成功执行相关事务操作
            //TODO 相关操作
            try {
                TimeUnit.SECONDS.sleep(10L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally {
                //解锁
                Long result = redisTemplate.execute(new DefaultRedisScript<>(script,Long.class), Arrays.asList(lockKey), uuid);
                System.out.println("解锁状态:"+result);
            }
        }else {
            //自旋操作
            try {
                System.out.println("加锁失败,开始自旋");
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //尝试再次获取锁
            lock(couponId);
        }

    }
}
  • 遗留⼀个问题,锁的过期时间,如何实现锁的自动续期或者 避免业务执行时间过长,锁过期了?
  • 原生式的话,⼀般把锁的过期时间设置久⼀点,比如10分钟时间
  • 原生代码+redis实现分布式锁使用较复杂,且有些锁续期问题更难处理
  • 延伸出框架 官⽅推荐方式: https://redis.io/topics/distlock