概述

什么是分布式锁?

为了防止分布式系统中的多个进程之间相互干扰,我们需要一种分布式协调技术来对这些进程进行调度,而这个分布式协调技术的核心就是来实现这个分布式锁。 说人话: 在分布式系统环境下,一个方法在同一时间只能被一个机器的一个线程执行。

ruoyi redis 分布式锁_分布式

代码实践

1. 上来就看代码

/**
 * 测试Redis实现分布式锁
 * 扣减库存
 * @return
 */
@GetMapping("/deduct_stock")
public String deductStock() {
    // 获取库存
    int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));

    if(stock > 0) {
        int realStock = stock - 1;
        stringRedisTemplate.opsForValue().set("stock", realStock + "");
        log.info("下单成功,剩余库存为 = {}", realStock);
    } else {
        log.warn("下单失败,库存不足");
    }

    return "ok";
}

2. 思考,有啥问题?

多线程并发请求时,可能导致超卖。

/**
 * 测试Redis实现分布式锁
 * @return
 */
@GetMapping("/deduct_stock")
public String deductStock() {

synchronized (this) {
    // 将下面两个redis关键操作,放入同步代码块中即可,JDK1.8性能还好,底层原理做了升级
    // 获取库存
    int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));

    if(stock > 0) {
        int realStock = stock - 1;
        stringRedisTemplate.opsForValue().set("stock", realStock + "");
        log.info("下单成功,剩余库存为 = {}", realStock);
    } else {
        log.warn("下单失败,库存不足");
    }
}

    return "ok";
}

3. 思考,还有啥问题?

synchronized 只在同一个jvm级别内有效,如果集群呢? 2个应用集群,外加一个NGINX做负载,可以用压测工具压下试试,看下效果。

/**
 * 测试Redis实现分布式锁
 *
 * @return
 */
@GetMapping("/deduct_stock")
public String deductStock() {

    Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lockKey", "qinchen");
    if(!lock) {
        log.info("未获取到锁,下单失败");
        return "5001";
    }

    // 获取库存
    int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));

    if (stock > 0) {
        int realStock = stock - 1;
        stringRedisTemplate.opsForValue().set("stock", realStock + "");
        log.info("下单成功,剩余库存为 = {}", realStock);
    } else {
        log.warn("下单失败,库存不足");
    }

    // 释放锁
    stringRedisTemplate.delete("lockKey");

    return "ok";
}

4. 思考,还有啥问题?

中间可以出现异常,导致锁释放失败,最后导致死锁。

/**
 * 测试Redis实现分布式锁
 *
 * @return
 */
@GetMapping("/deduct_stock")
public String deductStock() {

    try {
        Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lockKey", "qinchen");
        if(!lock) {
            log.info("未获取到锁,下单失败");
            return "5001";
        }

        // 获取库存
        int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));

        if (stock > 0) {
            int realStock = stock - 1;
            stringRedisTemplate.opsForValue().set("stock", realStock + "");
            log.info("下单成功,剩余库存为 = {}", realStock);
        } else {
            log.warn("下单失败,库存不足");
        }

    } finally {
        // 释放锁
        stringRedisTemplate.delete("lockKey");
    }

    return "ok";
}

5. 思考,还有啥问题?

有没有可能代码还没执行到finally,正好运维杀进程,或者宕机了。怎么办?

@GetMapping("/deduct_stock")
public String deductStock() {

    String lockKey = "lockKey:10001";

    try {
        Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "qinchen");
        // 给锁设置一个过期时间
        stringRedisTemplate.expire(lockKey, 10, TimeUnit.SECONDS);

        if(!lock) {
            log.info("未获取到锁,下单失败");
            return "5001";
        }

        // 获取库存
        int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));

        if (stock > 0) {
            int realStock = stock - 1;
            stringRedisTemplate.opsForValue().set("stock", realStock + "");
            log.info("下单成功,剩余库存为 = {}", realStock);
        } else {
            log.warn("下单失败,库存不足");
        }

    } finally {
        // 释放锁
        stringRedisTemplate.delete(lockKey);
    }

    return "ok";
}

6. 思考,还有啥问题?

设置锁与设置过期时间是原子操作吗?如果在这两行代码直接出现了意外呢?

//  Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "qinchen");
//  给锁设置一个过期时间
//  stringRedisTemplate.expire(lockKey, 10, TimeUnit.SECONDS);

// 设置锁并同时给一个过期时间
Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "qinchen", 10, TimeUnit.SECONDS);

7. 思考,还有啥问题吗?

如果业务执行时间超过10s呢?比如15秒,可能会出现上一个线程加的锁被下一个误删除。 仔细思考,上一个线程10s还未执行完时,锁已经过期了,下一个线程是不是就可以获取到锁了。 而这个时间,如果下一个线程执行的时间假定为8秒,在上一个线程15秒执行完时,是不是把刚刚第二个线程加的锁给删除了?而第二个线程可能才执行5秒,还有3秒才执行完。 以此类推,3秒后,第二个线程执行完了,是不是有可能把第三个获取到锁的线程的锁又给删了。 最终导致锁失效现象。

@GetMapping("/deduct_stock")
public String deductStock() {

    String lockKey = "product:10001";

    String clientId = UUID.randomUUID().toString();

    try {

        // 设置锁并同时给一个过期时间
        Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, clientId , 10, TimeUnit.SECONDS);

        if(!result) {
            log.info("未获取到锁,下单失败");
            return "5001";
        }

        // 获取库存
        int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));

        if (stock > 0) {
            int realStock = stock - 1;
            stringRedisTemplate.opsForValue().set("stock", realStock + "");
            log.info("下单成功,剩余库存为 = {}", realStock);
        } else {
            log.warn("下单失败,库存不足");
        }

    } finally {
        // 释放锁
        if(clientId.equals(stringRedisTemplate.opsForValue().get(lockKey))) {
            stringRedisTemplate.delete(lockKey);
        }
    }

    return "ok";
}

如果是数据库,这条SQL语句是否可以解决此问题?思考: update tb_stock set sotck_tatal = sotck_tatal - 1 where (sotck_tatal - 1 >= 0)

8. 再讨论下,还有啥问题吗?

finally里面的get与delete操作未保证原子性,会怎么样?可能出现一定时间导致死锁。 其实一般软件公司,实现到这里,算笔记完美了,可以用了。 但是在真正互联网场景下,还是存在一定问题。 主要是超时设置问题。上面说的,如果业务执行时间超10s以上,还是存在两个或以上的线程同时执行我们业务代码的可能性。超时时间到底设置多大合适? 这个时候,提到一个概念:锁续命。

解决思路:

  1. 应用内部使用一个定时器子线程
  2. 取1/3超时时间,比如每隔3秒,判断下锁是否还存在?
  3. 存在,则再讲该锁设置过期时间为10s
  4. 如果不存在,则定时任务子线程直接结束。

Redison框架

  1. 这里提到Redison框架,用来实现我们一把强壮的分布式锁场景。
  2. Redisson底层实现原理:
  3. 怎么用?
@GetMapping("/deduct_stock_redisson")
public String deductStockRedisson() {

    String lockKey = "product:10002";

    RLock redissonLock = redisson.getLock(lockKey);

    try {

        // 加锁,外加内部实现了锁续命,默认超时时间为30s
        redissonLock.lock();

        // 获取库存
        int stock_redisson = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock_redisson"));

        if (stock_redisson > 0) {
            int realStock = stock_redisson - 1;
            stringRedisTemplate.opsForValue().set("stock_redisson", realStock + "");
            log.info("下单成功,剩余库存为 = {}", realStock);
        } else {
            log.warn("下单失败,库存不足");
        }

    } finally {
        redissonLock.unlock();
    }

    return "ok";
}
  1. Redisson底层使用了较多lua脚本,保证了较多redis操作的原子性,从性能上来说,减少了网络开销,提升了redis命令操作性能。
  2. Redisson默认模式为:自旋锁(while循环,重复尝试加锁)
  3. Redisson支持:可重入锁、公平锁、联锁,红锁、读写锁、信号量、闭锁等特性。
  4. 更深入学习可点击我查看文档。

8. 再全局考虑,还有啥问题吗?

  1. Redis高可用结构下,若在主节点获取到锁后,设置的锁key还未同步到slave
  2. 此时Master挂了,Slave提升为新的Master
  3. 后面的线程是不是即可再次加锁成功
  4. 但前面那个线程有可能还未执行完成,此时的锁机制还是存在一定不可靠性

扩展学习zk分布式锁

  1. 可扩展学习基于zk实现的分布式锁,可以解决此问题,能够得到一把可靠性高的锁
  2. 基于zk支持数据强一致性特性
  3. 但是基于zk实现的分布式锁,性能不如redis

了解下Redlock红锁

  1. 主要原理就是同时从多个RedisServer中拿锁
  2. 达到高可用,但牺牲性能较大
  3. 一般作为了解,不建议使用

总体上,绝大部分公司的业务,基于Redis的分布式锁都能满足需求了.

思考,如何提升分布锁性能?

  1. 大量流量集中引入,Redis服务器压力大
  2. 思路:分段锁
  3. 将库存进行分段,放入redis中,例如1000库存,可分10段放入Redis
  4. key的设计可以为Product:10001:0 | Product:10001:1 ....
  5. Redis底层集群,将根据key,计算器槽位,放入不同节点中
  6. 这样便分担了压力