一、集群下的线程并发安全问题:

通过加锁可以解决在单机情况下的一人一单安全问题,但是在集群模式下就不行了。

==》原因:

在集群模式下,有多个JVM就会有多个锁监视器,每个JVM内部可以监视到线程实现互斥,每个JVM内部都会有一个线程是成功的,如果集群模式下,部署多台的话,就会有并行的多个线程成功运行,就会出现线程安全问题

分布式锁_Redis

二、分布式锁:

(一)、概念:

分布式锁:满足分布式系统或集群模式下多进程可见并且互斥的锁。 

分布式锁_分布式锁_02

(二)、分布式锁的实现:

分布式锁的核心是实现多进程之间互斥,而满足这一点的方式有很多,常见的有三种: 

分布式锁_Redis_03

(三)、基于Redis实现分布式锁:

实现分布式锁时需要实现的两个基本方法: 

1、释放锁:

==》可能会遇到的情况:

当还未释放锁的时候,服务宕机了。那么就没办法释放锁,后面的线程就进不来。所以通常会设置一个超时时间,在服务宕机后,到一定时间锁就会自动释放了。

分布式锁_分布式锁_04

2、获取锁:

==》可能遇到的情况:

当执行完获取锁的动作后NX,还未来得及设置过期时间EX,服务就宕机了。还是会导致线程无法执行。

分布式锁_Redisson_05

所以通常把NX和EX变成一个原子操作(同时成功,同时失败)

分布式锁_分布式锁_06

==》在redis中获取锁失败后有两种机制:阻塞机制和非阻塞机制。阻塞机制就是如果获取锁失败后,会继续尝试等待锁释放;非阻塞机制是如果获取锁失败后,就直接返回结果,不再等待锁释放。

3、业务流程:

这里采用非阻塞方式。

分布式锁_Redis_07

4、基于Redis实现分布式锁--初级版本
package com.hmdp.utils;

import org.springframework.data.redis.core.StringRedisTemplate;

import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;

/**
 * @author lenovo
 * @date 2025/3/10 13:59
 */
public class SimpleRedisLock implements ILock{

    private StringRedisTemplate stringRedisTemplate;
    private String name;//业务名称,也是锁的名称

    private static final String KEY_PREFIX="lock:";

    public SimpleRedisLock(String name,StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
        this.name = name;
    }

    @Override
    public boolean tryLock(long timeoutSec) {
        //获取线程标识
        long threadId = Thread.currentThread().getId();

        //获取锁
        Boolean success = stringRedisTemplate.opsForValue()
                .setIfAbsent(KEY_PREFIX + name, threadId + "", timeoutSec, TimeUnit.SECONDS);

        return Boolean.TRUE.equals(success);//Boolean有可能为null,拆箱后就是空指针了,所以要比较一下


    }

    @Override
    public void unlock() {
        //释放锁
        stringRedisTemplate.delete(KEY_PREFIX+name);
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.

==》VoucherOrderServiceImpl:

package com.hmdp.service.impl;

import com.hmdp.controller.VoucherController;
import com.hmdp.dto.Result;
import com.hmdp.entity.SeckillVoucher;
import com.hmdp.entity.VoucherOrder;
import com.hmdp.mapper.VoucherOrderMapper;
import com.hmdp.service.ISeckillVoucherService;
import com.hmdp.service.IVoucherOrderService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.utils.RedisIDWorker;
import com.hmdp.utils.SimpleRedisLock;
import com.hmdp.utils.UserHolder;
import org.springframework.aop.framework.AopContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import java.time.LocalDateTime;

/**
 * <p>
 *  服务实现类
 * </p>
 *
 * @author LL
 * @since 2021-12-22
 */
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {

    @Resource
    private ISeckillVoucherService seckillVoucherService;
    @Resource
    private RedisIDWorker redisIDWorker;

    @Resource
    private StringRedisTemplate stringRedisTemplate;


    /**
     * 优惠券秒杀下单
     * @param voucherId
     * @return
     */


    public Result seckillVoucher(Long voucherId) {



        //1、查询优惠券信息
        SeckillVoucher voucher = seckillVoucherService.getById(voucherId);

        //2、判断秒杀是否开始和结束
        //2.1、判断秒杀是否开始
        if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
            //3、否-返回异常信息
            return Result.fail("秒杀尚未开始!");

        }
        if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
            //3、否-返回异常信息
            return Result.fail("秒杀已经结束!");

        }

        //4、是-判断库存是否充足
        if (voucher.getStock()<1) {
            //4.1、否-返回异常信息
            return Result.fail("库存不足!");

        }

       /* ------------------   一人一单------*/


        //6、查询订单
        Long userId = UserHolder.getUser().getId();

        /*因为事务是在方法执行完后才提交的,如果在方法里加锁的话会导致,当方法执行完后锁就会释放,
        * 若在此时事务还没有提交,就来了其他线程,查询订单不存在,还是可能会出现并发安全问题
        * 因此需要,在事务提交后再释放锁*/

      /*  synchronized (userId.toString().intern()) {

            *//*事务问题:这里相当于this.createVoucherOrder 拿到的是目标对象,而不是代理对象,
            * 而事务生效是使用代理对象进行的事务处理--所以这里并不会生效,需要拿到代理对象*//*

            //获取代理对象
            IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();

            return proxy.createVoucherOrder(voucherId);
        }*/

        /*----------------------------------分布式锁--------*/

        //创建所锁对象--锁的范围是用户。一个用户一把锁:order+userId

        SimpleRedisLock simpleRedisLock = new SimpleRedisLock("order" + userId, stringRedisTemplate);

        //获取锁
        boolean isLock = simpleRedisLock.tryLock(1200);
        if (!isLock) {

           //获取锁失败
           return Result.fail("不允许重复下单");

        }

        try {
            //获取代理对象
            IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
            return proxy.createVoucherOrder(voucherId);

        }  finally {

            //释放锁
            simpleRedisLock.unlock();
        }


    }

    @Transactional
    public  Result createVoucherOrder(Long voucherId) {
        //6、查询订单
        Long userId = UserHolder.getUser().getId();

        /*每个用户都加一把锁,不同用户加不同锁*/

        /*需要的是id一样的用一把锁,但是每一个请求过来都是一个全新的对象,因此对象变了,锁就变了。要的
        * 是同一个对象用一把锁,因此就通过 值一样 的方式来保证是同一把锁。但是在转为字符串时,toString方法的底层
        * 创建了String对象new String .因此虽然每次值都一样,但是每次都会使用toString 也相当于是一个全新的对象
        * 使用intern方法,这个方法的作用是:去字符串常量池中寻找与其值相同的对象,然后返回该对象的地址 */

        //synchronized (userId.toString().intern()) {

            //6.1、判断该订单是否存在
            Integer count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
            if (count > 0) {
                //订单已存在
                return Result.fail("用户已经购买过一次!");
            }


            //4.2、是-扣减库存
            boolean success = seckillVoucherService.update()
                    .setSql("stock=stock-1")
                    .eq("voucher_id", voucherId)
                    .gt("stock", 0)
                    .update();

            if (!success) {
                return Result.fail("库存不足!");
            }


            //5、创建订单
            VoucherOrder voucherOrder = new VoucherOrder();
            long orderId = redisIDWorker.nextId("order");

            voucherOrder.setId(orderId);//订单id
            voucherOrder.setUserId(userId);//用户id
            voucherOrder.setVoucherId(voucherId);//代金券id

            save(voucherOrder);
            //6、返回订单id
            return Result.ok(orderId);

       // }
    }


}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
  • 72.
  • 73.
  • 74.
  • 75.
  • 76.
  • 77.
  • 78.
  • 79.
  • 80.
  • 81.
  • 82.
  • 83.
  • 84.
  • 85.
  • 86.
  • 87.
  • 88.
  • 89.
  • 90.
  • 91.
  • 92.
  • 93.
  • 94.
  • 95.
  • 96.
  • 97.
  • 98.
  • 99.
  • 100.
  • 101.
  • 102.
  • 103.
  • 104.
  • 105.
  • 106.
  • 107.
  • 108.
  • 109.
  • 110.
  • 111.
  • 112.
  • 113.
  • 114.
  • 115.
  • 116.
  • 117.
  • 118.
  • 119.
  • 120.
  • 121.
  • 122.
  • 123.
  • 124.
  • 125.
  • 126.
  • 127.
  • 128.
  • 129.
  • 130.
  • 131.
  • 132.
  • 133.
  • 134.
  • 135.
  • 136.
  • 137.
  • 138.
  • 139.
  • 140.
  • 141.
  • 142.
  • 143.
  • 144.
  • 145.
  • 146.
  • 147.
  • 148.
  • 149.
  • 150.
  • 151.
  • 152.
  • 153.
  • 154.
  • 155.
  • 156.
  • 157.
  • 158.
  • 159.
  • 160.
  • 161.
  • 162.
  • 163.
  • 164.
  • 165.
  • 166.
  • 167.
  • 168.
  • 169.
  • 170.
  • 171.
  • 172.
  • 173.
  • 174.
  • 175.
  • 176.
  • 177.

(四)、分布式锁误删问题:

1、问题一描述:

当线程1获取锁后开始执行业务,在这期间发生业务阻塞,导致超过了给锁设置的超时时间,导致锁自动释放了。然后线程2进来了,获取锁后开始执行业务,在线程2执行业务的期间,线程1的业务完成了,然后就去释放锁,直接把线程2的锁给释放了。释放完锁后,线程3又获取锁,开始执行业务............

分布式锁_Redisson_08

2、解决:

在释放锁的时候判断一下是不是自己的锁。

分布式锁_Redis_09

package com.hmdp.utils;

import cn.hutool.core.lang.UUID;
import org.springframework.data.redis.core.StringRedisTemplate;

import java.util.concurrent.TimeUnit;

/**
 * @author lenovo
 * @date 2025/3/10 13:59
 */
public class SimpleRedisLock implements ILock{

    private StringRedisTemplate stringRedisTemplate;
    private String name;//业务名称,也是锁的名称

    private static final String KEY_PREFIX="lock:";
    private static final String ID_PREFIX= UUID.randomUUID().toString(true)+"-";

    public SimpleRedisLock(String name,StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
        this.name = name;
    }

    @Override
    public boolean tryLock(long timeoutSec) {
        //获取线程标识
        String threadId = ID_PREFIX+Thread.currentThread().getId();

        //获取锁
        Boolean success = stringRedisTemplate.opsForValue()
                .setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);

        return Boolean.TRUE.equals(success);//Boolean有可能为null,拆箱后就是空指针了,所以要比较一下


    }

    @Override
    public void unlock() {
        //获取线程标识
        String threadId = ID_PREFIX+Thread.currentThread().getId();
        //获取锁的标识
        String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);

        //判断是否一致
        if(threadId.equals(id)){

            //释放锁
            stringRedisTemplate.delete(KEY_PREFIX+name);

        }

    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
3、问题二描述:

当判断完锁的唯一标识后,正准备释放锁,此时又发生了阻塞(JVM的垃圾回收机制)超过了给锁设置的超时时间...产生了误删锁的问题

分布式锁_分布式锁_10

4、解决:

使得判断锁的唯一标识和释放锁 为一个原子操作:

4.1、Redis的Lua脚本:

Redis提供了Lua脚本功能,在一个脚本中编写多条Redis命令,确保多条命令执行时的原子性。Lua是一种编程语言, 参考语法

分布式锁_Redis_11

分布式锁_Redis_12

4.2、基于Lua脚本实现分布式锁的释放锁逻辑:

分布式锁_分布式锁_13

分布式锁_Redisson_14

3、代码实现:

分布式锁_Redis_15

unlock.lua:

if(redis.call('get',KEYS[1])==ARGV[1])then
    --释放锁
    return redis.call('del',KSYS[1])
end
return 0
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

 SimpleRedisLock:

package com.hmdp.utils;

import cn.hutool.core.lang.UUID;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;

import java.util.Collections;
import java.util.concurrent.TimeUnit;

/**
 * @author lenovo
 * @date 2025/3/10 13:59
 */
public class SimpleRedisLock implements ILock{

    private StringRedisTemplate stringRedisTemplate;
    private String name;//业务名称,也是锁的名称

    private static final String KEY_PREFIX="lock:";
    private static final String ID_PREFIX= UUID.randomUUID().toString(true)+"-";
    /*初始化Lua脚本-先加载
    * */
    private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
    static{
        UNLOCK_SCRIPT = new DefaultRedisScript<>();
        UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
        UNLOCK_SCRIPT.setResultType(Long.class);
    }
    

    public SimpleRedisLock(String name,StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
        this.name = name;
    }

    @Override
    public boolean tryLock(long timeoutSec) {
        //获取线程标识
        String threadId = ID_PREFIX+Thread.currentThread().getId();

        //获取锁
        Boolean success = stringRedisTemplate.opsForValue()
                .setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);

        return Boolean.TRUE.equals(success);//Boolean有可能为null,拆箱后就是空指针了,所以要比较一下


    }


    @Override
    public void unlock() {
       //调用Lua脚本
        stringRedisTemplate.execute(UNLOCK_SCRIPT, 
                Collections.singletonList(KEY_PREFIX+name),
                ID_PREFIX+Thread.currentThread().getId()
        );

    }


   /* @Override
    public void unlock() {
        //获取线程标识
        String threadId = ID_PREFIX+Thread.currentThread().getId();
        //获取锁的标识
        String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);

        //判断是否一致
        if(threadId.equals(id)){

            //释放锁
            stringRedisTemplate.delete(KEY_PREFIX+name);

        }

    }*/
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
  • 72.
  • 73.
  • 74.
  • 75.
  • 76.
  • 77.
  • 78.
  • 79.
  • 80.

3、总结:

分布式锁_Redisson_16

三、分布式锁Redisson:

(一)、引入:

分布式锁_Redisson_17

(二)、Redission的概念

Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务,其中就包含了各种分布式锁的实现

分布式锁_分布式锁_18

官网地址:  https://redisson.org GitHub地址:  https://github.com/redisson/redisson

(三)、Redission入门:

分布式锁_分布式锁_19

package com.hmdp.utils;

import cn.hutool.core.lang.UUID;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;

import java.util.Collections;
import java.util.concurrent.TimeUnit;

/**
 * @author lenovo
 * @date 2025/3/10 13:59
 */
public class SimpleRedisLock implements ILock{

    private StringRedisTemplate stringRedisTemplate;
    private String name;//业务名称,也是锁的名称

    private static final String KEY_PREFIX="lock:";
    private static final String ID_PREFIX= UUID.randomUUID().toString(true)+"-";
    /*初始化Lua脚本-先加载
    * */
    private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
    static{
        UNLOCK_SCRIPT = new DefaultRedisScript<>();
        UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
        UNLOCK_SCRIPT.setResultType(Long.class);
    }
    

    public SimpleRedisLock(String name,StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
        this.name = name;
    }

    @Override
    public boolean tryLock(long timeoutSec) {
        //获取线程标识
        String threadId = ID_PREFIX+Thread.currentThread().getId();

        //获取锁
        Boolean success = stringRedisTemplate.opsForValue()
                .setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);

        return Boolean.TRUE.equals(success);//Boolean有可能为null,拆箱后就是空指针了,所以要比较一下


    }


    @Override
    public void unlock() {
       //调用Lua脚本
        stringRedisTemplate.execute(UNLOCK_SCRIPT, 
                Collections.singletonList(KEY_PREFIX+name),
                ID_PREFIX+Thread.currentThread().getId()
        );

    }


   /* @Override
    public void unlock() {
        //获取线程标识
        String threadId = ID_PREFIX+Thread.currentThread().getId();
        //获取锁的标识
        String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);

        //判断是否一致
        if(threadId.equals(id)){

            //释放锁
            stringRedisTemplate.delete(KEY_PREFIX+name);

        }

    }*/
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
  • 72.
  • 73.
  • 74.
  • 75.
  • 76.
  • 77.
  • 78.
  • 79.
  • 80.
1、可重入锁:

可重入锁指的是同一个线程可无限次地进入同一把锁的不同代码,又因该锁通过线程独占共享资源的方式确保并发安全,又称为独占锁。

==》举例:比如对于同一个线程而言,有方法1调用了方法2尝试去获取锁,在方法2里又尝试去获取锁,一个线程两次去获得锁,这就是重入。

分布式锁_Redis_20

===》而对于之前的业务,如下图,每次获取锁使用后就释放掉,所以对于同一个线程而言,如果连续两次获取锁,获取到的不会是同一把锁

String类型:

分布式锁_Redisson_21

1.1、Redisson实现可重入锁原理:

每次获取锁时都会判断下标识,如果是自己的锁,就把锁的个数加1,然后释放锁的时候还会判断下是否是自己的标识,如果是的话,再判断下锁的个数,如果值为0了,就代表该线程所有的业务都执行完了,就会释放锁。如果值不为0,由于进来的个数和出去的个数相等,那么就代表,还有业务没有执行完,就不会把锁释放掉。

hash类型:

分布式锁_分布式锁_22

2、Redisson实现主从一致性原理:
2.1、主从一致性问题:

主节点会进行数据同步给从节点。只有主节点可以读写。当一个锁的标识存入主节点后,还未来得及同步给从节点,此时主节点发生了故障,然后就会从任一从节点选择一个作为主节点,但是但是锁的标识还未同步,就会锁不生效,然后发生并发安全问题

分布式锁_Redis_23

2.2、multiLock:

不设置主从节点,而是一个个独立的Redis节点,必须依次向多个Redis节点获取锁,每个Redis都获取了锁,并且保存了锁的唯一标识才算获取锁成功。因为没有主从,就不会有主从一致性问题。如果想让可用性更好,可以在每个redis后面再加几个Redis从节点,即使这里有主从结构也不会有并发安全问题,因为当某个主节点发生故障后,从节点会变成主节点,但并没有获取到锁的标识。前面说过,只有每一个节点都拿到锁才能成功,所有最终也会失败。

分布式锁_Redis_24