第6章:分布式锁之Redis+Lua脚本实现原生分布式锁

分布式核心技术-高并发下分布式锁知识

产生背景:需要保证同一时间只有一个客户端可以对共享资源进行操作。比如优惠卷领取限制次数,商品库存超卖等。

其核心技术是为了防止分布式系统中的多个进程之间相互干扰,我们需要一种分布式协调技术来对这些进程进行调度。利用互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题。

避免共享资源并发操作导致数据问题可以用加锁来实现,本地锁(synchronize,lock等),分布式锁(redis,zookeeper等)。

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

  1. 排他性:在分布式应用集群中,同一个方法在同一时间只能被一台机器上的一个线程执行。
  2. 容错性:分布式锁一定能得到释放,比如客户端崩溃或者网络中断。
  3. 满足可重入,高性能,高可用。
  4. 注意分布式锁的开销,锁粒度。

Redis实现分布式锁的坑

Redis实现分布式锁文档:http://www.redis.cn/commands.html#string

加锁 SETNX key value

sentx 的含义就是 SET if Not Exists , 有两个参数setnx(key,value),该方法是原则操作。

如果key不存在,则设置当前key成功,返回1。

如果存在,则设置失败,返回0。

解锁 del(key)

得到锁的线程执行完任务,需要释放锁,以便其他线程可以进入,调用del(key)。

配置锁超时expire(key,30s)

客户端崩溃或者网络中断,资源将会永远被锁住,即死锁,因此需要给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失败了,且宕机了,则这个资源就是死锁。

加锁:配置过期时间:保证原子性操作。

解锁:防止误删除,也要保证原子性操作。

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

多个命令的原子性操作:采用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);

实例代码

import com.guslegend.util.JsonData;
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.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.UUID;
import java.util.concurrent.TimeUnit;

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

    @Autowired
    private RedisTemplate redisTemplate;

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

        //防止被其他线程误删
        String uuid = UUID.randomUUID().toString();
        String lockKey = "lock:coupon:"+countId;
        lock(countId,uuid,lockKey);
        return JsonData.buildSuccess("添加成功");
    }

    private void lock(int countId, 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 (Exception e){
                
            }finally {
                //解锁
                Long result = (Long) redisTemplate.execute(new DefaultRedisScript<>(script,Long.class), Arrays.asList(lockKey,uuid));
                System.out.println("解锁状态"+result);
            }
        }else {
            // TODO 获取锁失败
            try {
                System.out.println("获取锁失败,睡眠5秒,进行自旋");
            }catch (Exception e){
                
            }
            lock(countId,uuid,lockKey);
        }
    }
}

官方推荐的分布式锁方式:https://redis.io/docs/latest/develop/clients/patterns/distributed-locks/

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值