用Redis实现分布式锁--Redis之面试官爱我系列(3)

大家好我是霜华!!!
我是真的真的想回学校了。
想去图书馆边写代码边看来来往往的小姐姐们。
真就迷迷糊糊的大二就这样过去了。。。。
在这里插入图片描述

分布式在如今的大数据高流量的情况下是非常必要有的一个环境
而分布式锁也是重中之重

分布式锁

多个操作同时进行会参数并发问题,因为多个操作不是原子操作

(原子操作:不会被线程调度机制打断的操作,这种机制操作一旦开始就一直运行到结束,中间不会有任何线程切换)

在单机开发中,锁经常见到,简单暴力的synchronize 以及可重入锁ReenttrantLock ,

用ReenttrantLock实现单机锁

public class RedisText {
    private static Integer inventory=1001;//存货
    private static final int NUM=1000;//执行次数
    private static LinkedBlockingQueue linkedBlockingQueue=new LinkedBlockingQueue();//单向列表的阻塞队列
    static ReentrantLock reentrantLock =new ReentrantLock();//重入锁,实现公平锁机制
    public static void main(String []args){
        ThreadPoolExecutor threadPoolExecutor=//线程池
                new ThreadPoolExecutor(inventory,inventory,10L, SECONDS,linkedBlockingQueue);
        //设置线程数、最大的线程数,最大活跃市场
        final CountDownLatch countDownLatch = new CountDownLatch(NUM);//让一一个线程等待其他线程各自执行完毕后再执行//计数器
        for(int i=0;i<NUM;i++){
            threadPoolExecutor.execute(new Runnable() {//执行线程
                public void run() {
                   reentrantLock.lock();//java提供的可重入锁,
                    inventory--;//处理存货
                    reentrantLock.unlock();
                    System.out.println("线程执行"+Thread.currentThread().getName());
                    countDownLatch.countDown();//计数器减1
                }
            });
        }
        threadPoolExecutor.shutdown();//关掉计数器
        try {
            countDownLatch.await();//主动关掉计数器
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

在多线程环境中控制对资源的并发访问,但是这并适合于多机分布式环境中的对信息安全的需求,这时候分布式锁就很有必要。

分布式的使用场景:

效率:避免不同节点(可以理解成就是不同服务器实例)重复相同的工作,可能有多个服务器接到了同一个指令去做同样的是

正确性:避免多个节点在同一个数据上进行操作,让操作失真

一个优秀方分布式锁应该具备如下特点:

1.互斥性:就是你占了坑别人不能抢走

2.可重入性:一个锁支持同一个锁多次加锁

3.锁超时:一定要设置锁的实现,避免因为特殊原因没执行解锁指令形成死锁

4.高效、高可用

5.支持公平锁和非公平锁:公平锁:按照请求顺序加锁,非公平:无序加锁

悲观锁

每次去拿数据的时候都认为别⼈会修改,所以每⼀次拿数据的时候都会上锁,这样别⼈想拿数据就会被挡住

乐观锁

乐观锁策略也被称为⽆锁编程,换句话说,乐观锁其实不是锁,它仅仅只是⼀个循环重试了的CAS

算法⽽已

每次去拿数据的时候都认为别⼈不会修改,所以不会上锁,但是如果想要更新数据,则会在更新前
检查在读取⾄更新这段时间别⼈有没有修改过这个数据,如果修改过,则重新读取,再次尝试更
新,循环上述步骤指导更新成功

乐观锁的缺点—ABA问题

如果⼀个变量V初次读取的时候是A值,并且在准备复制的时候检查到它仍然是a值那我们就能说明它

的值没有被其他线程修改过了吗?很明显是不能的,因为在这段时间它的值可能被改为其他值,然

后有改回A,那CAS操作就会误认为它从来没有被修改过,这个问题被称为CAS操作的“ABA”问题

悲观锁vs 乐观锁

1)悲观锁阻塞事务,乐观锁回滚重试

2)乐观锁适⽤于写⽐较少的情况下,即冲突很少发⽣时,可以省去锁的开销,加⼤了系统吞吐量

3)悲观锁适⽤于写⽐较多的情况,因为如果乐观锁经常冲突,应⽤要不断进⾏重试,反倒降低性能

CAS算法:

Compare-and-Swap 即⽐较并替换,也有叫做Compare-and-Set的,⽐较并设置

1、⽐较:读取到了⼀个值A,在将其更新为B之前,检查原值是否仍为A(未被其他线程改动)

2、设置:如果是,将A更新为B,结束,如果不是,则什么都不做

允许多个线程同时读取(因为根本没有加锁操作),但是只有⼀个线程可以成功更新数据,并导致

其他要更新的线程回滚重试,也叫⾮阻塞同步(Non-blocking Synchronization)

分布式锁实现

在很多场景中,我们为了保证保证数据的最终的⼀致性,需要很多的技术⽅案来⽀持,⽐如分布

式,分布式锁

有时,我们需要保证⼀个⽅法在同⼀时间只能被同⼀个线程执⾏,在单机环境中,java其实提供了很

多并发处理相关的API,但是这些API在分布式场景中就⽆能为⼒,也就是说单纯的javaAPI并不能提

供分布式锁的能⼒,所以针对分布式锁,实现⽬前有多种⽅案

分布式锁是控制分布式系统之间同步访问共享资源的⼀种⽅式

分布式锁⼀般有三种实现⽅式:

1.数据库乐观锁

2.基于Redis 的分布式锁,

3.基于Zookeeper的分布式锁

分布式锁的可靠性满足条件

1.互斥性,在任意时刻,只有⼀个客户端能持有锁
2.不会发⽣死锁,即使有⼀个客户端在持有锁的期间崩溃⽽没有主动解锁,也能保证后续其他户端能加锁
3.具有容错性,只要⼤部分Redis 节点正常运⾏,客户端就可以加锁和解锁
4.解铃还需系铃⼈,加锁和解锁必须是同⼀个客户端,客户端⾃⼰不能把别⼈加的锁给解了


Redis实现分布式锁

setnx指令 实现加锁 (原本是用来:如果key值不存在就赋值)

setnx xxxx true//加锁

实现锁方法一:

setnx xxx true //加锁

xxxxxx//执行代码,不会有其他节点会去执行这套流程

del xxx //删除key释放锁

问题一:代码区出现异常,del不会执行,锁一直被占用,死锁

方法二:

setnx 加锁后 expire xxx 5s//加锁后加一个过期时间,就算del执行不了,5s后依然放锁

问题二:

setnx 后还没来得及执行expire ,服务器出现问题挂了,expire执行不了 同样死锁

因为 setnx 和expire 是两条指令不是原子指令。(事务的原子性要么一起执行要不一起不执行)

方法三:

setnx 与px的结合参数
(redis操作:

set xxx true ex 5 nx//设置xxx加锁,5s后时效xxx删除解锁

setnx 和expire的原子结合,设置锁也设置过期

问题三:超时问题

代码的执行超过了设置的过期时间,后续代码就不会严格一系列执行了,中间可能会有其他线程参一脚,改变了数据

方法四:

setnx 指令的value参数设置为一个随机数,释放锁是先匹配随机数是否一致然后在删除key

问题四:

匹配value 和删除key不是原子操作,依然有可能会造成key没删到造成死锁

方法五:

用Lua叫做处理,保证多个指令是原子性执行


redission 是一个开源的基于redis的可重入锁
redission锁的原理分析:

加锁原理:

 public boolean getLock(String value){
        SetParams params=SetParams.setParams().nx().ex(5);//set xxx ex 5 nx//nx ex命令合计
        //还有个px 跟ex是一样的效果 px以毫秒做单位,ex以秒做单位
     String answer= jedis.set("redis_lock",value,params);//返回结果是 两种 ok /null
        if(answer!=null){
            return true;
        }
        return false;
    }
   public boolean lock(String value){
        Long statrt = System.currentTimeMillis();
        try{
            for(;;){
                if(getLock(value)){
                    return true;
                }
                //设置一下找锁的时长吧如果长时间没找到我就不找了
                long end =System.currentTimeMillis()-statrt;
                if(end>5){
                    return  false;
                }
            }
        }finally {
            jedis.close();//上完厕所就关门文明你我他
        }
    }

解锁原理:原码肯定是用了Lua脚本做了一次拼装

 public boolean uvlock(String value){
        //一般情况下value 设置一个独立的随机数字,你找到了我我才给你解锁
      try {
          if(jedis.get("redis_lock").equals(value)){
              jedis.del(value);
              return true;
          }
          return false;
      }finally {
          jedis.close();
      }
    }

当然redission原码肯定比这个复杂很多很多倍,等霜华有时间了再让大家白嫖。redis分布式锁应该是面试比较常问的吧,希望大家好好理解。
我是霜华一个渴望去西溪园区撩妹的后浪,我们下期见!

原创文章 6 获赞 0 访问量 568

猜你喜欢

转载自blog.csdn.net/weixin_45898658/article/details/106159704