Redis学习_第五章(redis分布式锁)

目录

1:回顾以前

2:redis分布式锁

2.1:Redis分布锁原理依赖

2.2:redis的分布式锁SpringBoot实现

2.2.1:使用redisTemplate.opsForValue().setIfAbsent()

2.2.2:谁上的锁只能谁删除

2.2.3:谁上锁谁删除原子操作

3:redisson分布式锁

3.1:redisson的优点

3.2:redisson使用

3.3:RedLock(红锁)

3:基于数据库的分布式锁

4:基于Zookeeper的分布式锁


1:回顾以前

在我们的单机服务情况下,高并发的时候,我们采用synchronized和ReentrantLock来保证多线程下的锁竞争。

代码案例如下:模拟10个线程抢5张票。使用了synchronized来保证超卖和线程安全

/**
 * 在redis之前 同一个进程我们可以使用synchronized和ReentrantLock加锁
 * 但是在分布式环境下 这种情况就不行了
 */
public class Test1 {
    //一共10张票
    public static int num = 5;
//    Lock lock = new ReentrantLock();
    public static void main(String[] args) {
        Test1 t1 = new Test1();
        for (int i = 0; i < 10; i++) {
            //10个线程抢票
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        t1.sale();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }).start();
        }

    }

    //方法非static加锁,属于单个对象锁
    public synchronized void sale() throws InterruptedException {
        if (num > 0) {
            System.out.println(Thread.currentThread().getName() + "抢票成功:" + num);
            num--;
        }else {
            System.out.println(Thread.currentThread().getName() + "抢票失败!"+num);
        }
    }

}

2:redis分布式锁

2.1:Redis分布锁原理依赖

在redis中我们依赖redis的string数据类型中的命令,展示如下

当key不存在的时候设置key的vaule

//命令1:当key不存在的时候设置key的值是value  并且设置key的过期时间
//命令1的缺点,这是两条命令 设置key的值和设置过期时间不是原子性的,需要依赖lua
setnx key value
expire key time

//命令2:在redis的2.6.12版本开始后 redis支持了同时设置值和过期时间
//当key不存在的时候设置key的值是value,并且设置过期时间
//EX seconds 秒
//PX milliseconds 毫秒
//NX:只在键不存在时设置键的值。如果键已经存在,则不做任何操作。
//XX:只在键已经存在时设置键的值。如果键不存在,则不做任何操作。
set key value [EX seconds] [PX milliseconds] [NX|XX] 

我们在redis中验证setnx key value如下:

我们在redis中验证set key value [EX seconds] [PX milliseconds] [NX|XX]如下:

2.2:redis的分布式锁SpringBoot实现

2.2.1:使用redisTemplate.opsForValue().setIfAbsent()

 /**
     * redis的分布式锁实现 伪代码
     * 
     */
    @GetMapping(value = "redisLock1")
    public String getLock1(int num) throws InterruptedException {
        //设置redisLock1和过期时间100秒
        if (Boolean.TRUE.equals(redisTemplate.opsForValue().setIfAbsent("redisLock1", "锁1", 100, TimeUnit.SECONDS))) {
            //模拟抢到锁的线程执行任务耗时
            Thread.sleep(10000);
            
            System.out.println("并发执行下的 "+num+" :做了一些事情!");

            //做了一些事情后删除锁,让分布式线程进来获取锁
            redisTemplate.delete("redisLock1");
            return num+"抢到了锁!";
        }else {
            System.out.println(num+"没有抢到锁!");
            return num+"没有抢到锁!";
        }
    }

缺点分析:

缺点1:锁过期时间和线程执行时间不一致导致的问题

我们想一下,如果如果线程1进来,设置锁过期时间是3秒,但是任务执行了10秒,10秒后才能删除锁,那么锁就会提前过期,放他的线程2进来了。线程2设置了锁由于key相等,线程1在10秒结束后就会删除线程2设置的锁。我们该怎么办?😁

2.2.2:谁上的锁只能谁删除

实现方法是:当前线程判断value的值是不是当前前程的value

 /**
     * redis的分布式锁实现 伪代码
     *
     */
    @GetMapping(value = "redisLock2")
    public String getLock2(int num) throws InterruptedException {
        //设置redisLock1和过期时间100秒

        //解决谁上的锁 谁来解锁 我们设置锁的value是UUID
        String key="redisLock2";
        String value = UUID.randomUUID().toString().replace("-", "");
        try {
            if (Boolean.TRUE.equals(redisTemplate.opsForValue().setIfAbsent(key, value, 5, TimeUnit.SECONDS))) {
                //模拟抢到锁的线程执行任务耗时 10秒
                Thread.sleep(10000);
                System.out.println("并发执行下的 "+num+" :做了一些事情!");
                return num+"抢到了锁!";
            }else {
                System.out.println(num+"没有抢到锁!");
                return num+"没有抢到锁!";
            }
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            //谁上锁,谁删除 当前线程判断value的值是不是当前前程的value
            val o = redisTemplate.opsForValue().get(key);
            if (o!=null&&o.toString().equals(value)) {
                System.out.println("删除锁:"+redisTemplate.opsForValue().get(key));
                redisTemplate.delete(key);
            }
        }



    }

2.2.2的缺点:

1:在2.2.2中还是解决不了锁的过期时间和当前任务的执行时间的差,当锁提前过期的时候,任务没有执行完毕时候,会放进来其他的线程。

2:在2.2.2中的谁上锁谁删除的逻辑中,需要判断当前锁的值然后删除,在finally中的这两个操作也不是原子操作。并发的时候也会出问题,并发嘛,就是要保证数据的一致性,保证数据的一致性,最好要保证对数据的操作具有原子性。

2.2.3:谁上锁谁删除原子操作

使用了lua保证原子性

 /**
     * redis的分布式锁实现 伪代码
     * 谁上锁 谁删除 原子操作使用了lua
     */
    @GetMapping(value = "redisLock3")
    public String getLock3(int num) throws InterruptedException {
        //设置redisLock1和过期时间100秒

        //解决谁上的锁 谁来解锁 我们设置锁的value是UUID
        String key="redisLock3";
        String value = UUID.randomUUID().toString().replace("-", "");
        try {
            if (Boolean.TRUE.equals(redisTemplate.opsForValue().setIfAbsent(key, value, 10, TimeUnit.SECONDS))) {
                //模拟抢到锁的线程执行任务耗时 10秒
                Thread.sleep(8000);
                System.out.println("并发执行下的 "+num+" :做了一些事情!");
                return num+"抢到了锁!";
            }else {
                System.out.println(num+"没有抢到锁!");
                return num+"没有抢到锁!";
            }
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            //谁上锁,谁删除 当前线程判断value的值是不是当前前程的value
            //优化为原子操作  利用了lua
            String lua="if redis.call('get',KEYS[1]) == ARGV[1] then " +
                    "   return redis.call('del',KEYS[1]) " +
                    "else " +
                    "   return 0 " +
                    "end; ";
            DefaultRedisScript<Long> script = new DefaultRedisScript<>(lua, Long.class);
            Object eval =  redisTemplate.execute(script, Collections.singletonList(key), value);
            if("1".equals(eval.toString())){
                System.out.println("锁成功删除");
            }else{
                System.out.println("锁删除失败");
            }
        }
    }

总结:

为什么要设置随机值?

主要是为了防止锁被其他客户端删除。有这么一种情况:

  1. 客户端 A 获得了锁,还没有执行结束,但是锁超时自动释放了;

  2. 客户端 B 此时过来,是可以获得锁的,加锁成功;

  3. 此时,客户端 A 执行结束了,要去释放锁,如果不对比随机值,就会把客户端 B 的锁给释放了。

在2.2.3中还是解决不了由于任务耗时比较长,但是锁的过期时间短,锁提前删除的问题。

我们可以把锁的过期时间设置的长点,防止万一提前过期,但是又有一个问题,当程序死机了,finally的代码一致执行不了,到时锁迟迟无法删除。

3:redisson分布式锁

为啥推荐使用Redission

redis的分布式锁10大缺点:Redis分布式锁的10个坑-腾讯云开发者社区-腾讯云

1:客户端1加锁的锁key默认生存时间才30秒,如果超过了30秒,客户端1没有执行完毕,还想一直持有这把锁,怎么办呢?(锁过期释放,业务没执行完

2:锁不可重入,所谓的不可重入,就是当前线程执行某个方法已经获取了该锁,那么在方法中尝试再次获取锁时,会阻塞,不可以再次获得锁。同一个人拿一个锁 ,只能拿一次不能同时拿2次。

3:在redis集群中,主节点上有锁,但还没有考虑缓存续命,以及Redis集群部署下,异步复制造成的锁丢失:主节点没来得及把刚刚set进来这条数据给从节点,就挂了。从节点开始工作,但是从节点上没有锁,导致锁丢失

为了解决这些缺点,所以使用redisson,使用 Redisson 实现的分布式锁相对于直接使用 Redis 的分布式锁,具有一些显著的优势,尤其是在功能完善性、开发便捷性以及可扩展性方面。以下是 Redisson 实现的分布式锁相对于手动实现 Redis 分布式锁的几个主要优势:

3.1:redisson的优点

1:看门狗机制(watch dog

加锁后,如果超时了,Redis会自动释放清除锁,这样有可能业务还没处理完,锁就提前释放了。怎么办呢?

有些小伙伴认为,稍微把锁过期时间设置长一些就可以啦。其实我们设想一下,是否可以给获得锁的线程,开启一个定时守护线程,每隔一段时间检查锁是否还存在,存在则对锁的过期时间延长,防止锁过期提前释放

客户端1加锁的锁key默认生存时间是30秒,如果超过了30秒,客户端1还想一直持有这把锁,怎么办呢?

简单!只要客户端1一旦加锁成功,就会启动一个watch dog看门狗,他是一个后台线程,会每隔10秒检查一下,如果客户端1还持有锁key,那么就会不断的延长锁key的生存时间。

redission,采用的就是这种方案, 此方案不会入侵业务代码。

当前开源框架Redisson解决了这个问题。我们一起来看下Redisson底层原理图吧

只要线程加锁成功,就会启动一个watch dog看门狗,它是一个后台线程,会每隔10秒检查一下,如果线程一还持有锁,那么就会不断的延长锁key的生存时间。因此,Redisson就是使用Redisson解决了锁过期释放,业务没执行完问题

3.2:redisson使用

重学SpringBoot3-集成Redis(四)之Redisson-腾讯云开发者社区-腾讯云

1:导入jar

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson-spring-boot-starter</artifactId>
            <version>3.23.4</version>
        </dependency>

2:创建bean

@Autowired
    private RedissonProperties redissonProperties;

    @Bean
    public RedissonClient redissonClient() throws Exception{
        //1:读取配置文件中的redissonProperties
//        Config config = Config.fromYAML(redissonProperties.getConfig());
//        Redisson.create(config);
//        System.out.println("Redisson 已启动8080");
//        return Redisson.create(config);

        //2:创建配置对象
        Config config = new Config();
        // 指定使用单节点部署方式
        config.useSingleServer().setAddress("redis://localhost:6379").setDatabase(0);
        config.useSingleServer().setPassword("123456");
        // 创建Redisson客户端实例
        return Redisson.create(config);
    }

3:springboot代码使用

@RestController
public class RedissonController1 {

    @Autowired
    RedissonClient redissonClient;

    /**
     * Redisson的分布式锁 伪代码
     * 缺点:一个线程抢到了锁 其他的线程会进行排队
     */
    @GetMapping(value = "Rlock1")
    public String getLock1(int num) throws InterruptedException {
        //解决谁上的锁 谁来解锁 我们设置锁的value是UUID
        String key = "Rlock1";
        RLock lock = redissonClient.getLock(key);
        try {
            lock.lock();//加锁
            //模拟抢到锁的线程执行任务耗时 10秒
            Thread.sleep(10000);
            System.out.println("并发执行下的 " + num + " :做了一些事情!");
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            System.out.println("解锁");
            lock.unlock();//解锁
        }
        return num + "抢到了锁!";
    }

    /**
     * 此方法更好
     * Redisson的分布式锁 伪代码
     * tryLock尝试获取锁 超时返回
     */
    @GetMapping(value = "Rlock2")
    public String getLock2(int num) throws InterruptedException {
        //解决谁上的锁 谁来解锁 我们设置锁的value是UUID
        String key = "Rlock2";
        RLock lock = redissonClient.getLock(key);
        // 尝试获取锁,等待时间 2ms,锁定时间 10秒
        if (lock.tryLock(0, 10, TimeUnit.SECONDS)) {
            try {
                Thread.sleep(8000);
                System.out.println("并发执行下的 " + num + " :做了一些事情!");
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            } finally {
                System.out.println(num+"解锁");
                lock.unlock();//解锁
            }
        } else {
            System.out.println(num+"没有抢到锁");
            return num+"没有抢到锁!";
        }          //模拟抢到锁的线程执行任务耗时 10秒
        return num + "抢到了锁!";
    }

}

总结:lock.lock()方法的源码中我们可以可以看到lockWatchdogTimeout是30秒,也就是默认加锁啥时间是30秒,锁的value是uuid+线程id,底层也是使用了lua脚本同时设置值和过期时间,但是重点在设置完redis之后,开启了线程一个延迟执行(lockWatchdogTimeout/3)10秒的任务,再次去redis中把延迟时间加到30秒,递归调用,只在主线程结束删除key。

private long lockWatchdogTimeout = 30 * 1000;

redisson还有很多其他功能详情见官网。这里只介绍分布式锁的功能

3.3:RedLock(红锁)

使用了redisson感觉好像完美了,自动续期,使用简单,但是还有一个问题,当集群的redis中锁落在的主节点1上,锁没有及时同步到从节点1,主节点1挂了,从节点1上线,从节点1没有了锁。那该怎么办?

Redisson中红锁(RedLock)的实现_redisson红锁-CSDN博客

红锁怎使用?

 /**
     * Redisson的redLock分布式锁 伪代码
     * tryLock尝试获取锁 获取锁的过程中,会多次加锁
     * redlock是一种基于多节点redis实现分布式锁的算法,
     * 可以有效解决redis单点故障的问题。
     * 官方建议搭建五台 redis服务器对redlock算法进行实现。
     * 这里使用了3台
     */
    @GetMapping(value = "RedLock")
    public String getRedLock3(int num) throws InterruptedException {
        //解决谁上的锁 谁来解锁 我们设置锁的value是UUID
        String key = "RedLock";

        //创建多个锁
        //锁1
        Config config1 = new Config();
        config1.useSingleServer().setAddress("redis://127.0.0.1:7000").setDatabase(0);
        RedissonClient redissonClient1 = Redisson.create(config1);
        RLock lock1= redissonClient1.getLock(key);
        //锁2
        Config config2 = new Config();
        config2.useSingleServer().setAddress("redis://127.0.0.1:7001").setDatabase(0);
        RedissonClient redissonClient2 = Redisson.create(config2);
        RLock lock2= redissonClient2.getLock(key);
        //锁3
        Config config3 = new Config();
        config3.useSingleServer().setAddress("redis://127.0.0.1:7002").setDatabase(0);
        RedissonClient redissonClient3 = Redisson.create(config3);
        RLock lock3= redissonClient3.getLock(key);

        RedissonRedLock lock= new RedissonRedLock(lock1,lock2,lock3);

        // 尝试获取锁,等待时间 2ms,锁定时间 10秒
        // 加锁的过程中迭代这三个锁,逐个加锁
        //locks.size()/2 + 1 在这里3个所,必须3/2+1=2,2个以上的锁加锁成功才可以
        if (lock.tryLock(0, 10, TimeUnit.SECONDS)) {
            try {
                Thread.sleep(8000);
                System.out.println("并发执行下的 " + num + " :做了一些事情!");
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            } finally {
                System.out.println(num+"解锁");
                lock.unlock();//解锁
            }
        } else {
            System.out.println(num+"没有抢到锁");
            return num+"没有抢到锁!";
        }          //模拟抢到锁的线程执行任务耗时 10秒
        return num + "抢到了锁!";
    }

redlock的原理

 

3:基于数据库的分布式锁

基于数据库的分布式锁‌实现原理‌:

利用数据库唯一索引或SELECT ... FOR UPDATE排他锁实现互斥性。插入唯一业务ID记录成功视为加锁,删除记录视为解锁。‌‌‌‌


优化方案:通过版本号实现乐观锁(CAS机制)。‌‌

‌缺点‌:
性能低,频繁操作易引发锁表风险;
需额外处理锁超时和可重入性问题

4:基于Zookeeper的分布式锁

利用了zookeeper的强一致性,通过Watcher机制监听节点变化实现锁释放通知。‌‌

(这个需要写一篇新的博客)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值