分布式锁

分布式锁

分布式锁介绍

什么是分布式

一个大型的系统往往被分为几个子系统来做,一个子系统可以部署在多个服务器上,
分布式就是通过计算机网络将后端工作分布到多台主机上,多个主机一起协同完成工作

什么是锁

java程序中,当存在多个线程可以同时改变某个变量(可变共享变量)时,就需要对变量或代码做同步,使其在修改这种变量时能够线性执行消除并发修改变量,而同步的本质是通过锁来实现的.如java中synchronize是在对象头设置标记,Lock接口的实现类基本上都只是某一个volitile修饰的int类型变量其保证每个线程都能拥有对该int的可见性和原子修改

什么是分布式锁

任何一个分布式系统都无法同时满足一致性,可用性和分区容错性,最多只能满足两项
当在分布式模型下,数据只有一份,此时需要利用锁的技术控制某一时刻修改数据的进程数.
分布式锁:在分布式环境下,多个程序都需要对某一份(或有限制)的数据进行修改时,针对程序进行控制,保证同一时间节点下,只有一个程序对数据进行操作的技术

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-83HuClbo-1619060478621)(C:\Users\lenovo\AppData\Local\Temp\1608605072464.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jTDpET0W-1619060478625)(C:\Users\lenovo\AppData\Local\Temp\1608606005527.png)]

分布式锁执行流程

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0S1BwgGe-1619060478628)(C:\Users\lenovo\AppData\Local\Temp\1608606171366.png)]

分布式锁具备的条件

1.互斥性:同一时刻只能有一个服务(或应用)访问资源,特殊情况下有读写锁
2.原子性:一致性要求保证加锁和解锁的行为是原子性的
3.安全性:锁只能被持有该锁的服务(或应用)释放
4.容错性:在持有锁的服务崩溃时,锁仍能得到释放避免死锁
5.可重用性:同一个客户端获得锁后可递归调用 ---重入锁和不可重入锁
6.公平性:看业务是否需要公平,避免饿死--- 公平锁和非公平锁
7.支持阻塞和非阻塞: 和ReentrantLock一样支持lock和unlock 以及trylock(long timeout)--阻塞锁和非阻塞锁
8.高可用: 获取锁和释放锁要高可用
9.高性能: 获取锁和释放锁的性能要好
10.持久性: 锁按业务需要自动续约/自动延期

分布式锁的解决方案

数据库实现分布式锁
1.基于数据库表实现

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SWbSo9MP-1619060478633)(C:\Users\lenovo\AppData\Local\Temp\1608642748748.png)]

2.基于数据库排他锁实现

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vlQ6ngFO-1619060478637)(C:\Users\lenovo\AppData\Local\Temp\1608642943603.png)]

实现步骤:

1.java代码中,进行一个带有for update的查询,锁定某行数据
2.进行休眠5秒中,这5秒时间内,其他的连接一律不能够使用这条数据
3.进行commit,释放锁,其他连接就可以操作这条数据了

优点:简单,好实现 缺点:基于数据库,开销比较大,对数据库性能可能会存在影响

Redis实现分布式锁

基于redis的setnx(),expire(),getset()方法做分布式锁

@RestController
@RequestMapping("/lock")
public class LockController {

    @Autowired
    private StringRedisTemplate redisTemplate;

    private static String PREFIX_KEY = "LOCK_";

    /**
     * redis 实现分布式锁
     * @return
     */
    @GetMapping("/order")
    public R order(){
        String orderId ="1";
        //值设置成当前线程的唯一标识
        String threadId = UUID.randomUUID().toString();
        try{
            /*
               1. 加锁,setnx 命令, 如果该key有值(意味着已经上锁),则返回null  如果该key无值(意味着无					 锁,可以加锁),则返回1
               2. 并且设置过期时间,防止程序崩溃导致无法释放锁,从而形成死锁(注意设置值和设置过期时间是一				   条API,要保证两者的原子性,
                  不能分为两个API进行调用,如果在调用完设置值后服务down掉,岂不是过期时间没有设置成功后,                   那么当服务起来后,该锁会一直存在且不会被释放)
               3. 如果程序执行时间超过了设置的过期时间数,怎么办?当第一个线程如果执行了12秒才结束,那么他                   的锁自己早就被释放了,那他在第12秒执行完后去释放的锁是谁的?是后面的第二个线程的...,
                  而后面的第二个线程还没有执行完,锁却被释放掉了,
                  此时第三个线程进来轻而易举拿到了锁,那此时的锁还是锁吗?(永久失效)

                  那么这个过期时间应该怎么设置呢? 设置20S? 30S? 都不太合适
                  思路一: 可以在主线程获取到锁后,开一个分支线程去执行定时查询,查询锁的过期时间,如果剩余                           时间不足三分之一,则重新设置该锁的过期时间,
                          业务执行完成,停止定时任务并释放锁,  但是思路是对的,实现起来相对复杂,所以我们						   可以采用现成的解决方案->redission

            */
            Boolean res = redisTemplate.opsForValue()
            .setIfAbsent(PREFIX_KEY + orderId, threadId,10, TimeUnit.SECONDS);

            if(!res){
                return R.failed("服务器繁忙");
            }
            //执行业务代码
            //xxxxxxxx
        }finally {
            //释放锁(哪个线程加的锁,哪个线程释放锁)
            if(threadId.equals(redisTemplate.opsForValue().get(PREFIX_KEY + orderId))){
                redisTemplate.delete(PREFIX_KEY + orderId);
            }
        }
            return R.ok();
    }
}
Redisson实现分布式锁
/**
     * redisson 实现分布式锁
     * @return
     */
    @GetMapping("/order1")
    public R order1(){
        RLock redissonLock = redisson.getLock(PREFIX_KEY + orderId);
        try{
            //加锁
            redissonLock.lock(30,TimeUnit.SECONDS);
            //执行业务代码
        }finally {
            //释放锁
            redissonLock.unlock();
        }
        return R.ok();
    }

Redisson实现分布式锁原理

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Jfv8Q8EJ-1619060478641)(C:\Users\lenovo\AppData\Local\Temp\1608729458834.png)]

问题一:但是如果redis部署的是集群架构的话:

当主从结构中,主库down掉,那么从库会跟据选举机制选举出新的主库,然后再把数据同步,但就在数据同步之前,第二个请求线程进来想要获取锁,此时是可以获取到的,因为锁还没有通不过来嘛,那么这便出现了BUG

问题二:

虽然redis的性能已经很不错了,可以满足大多数中小互联网公司的业务量,但是如何能将性能成倍数的提升呢?如果有更大并发量的业务需要实现呢?

缓存使用

为了系统性能的提升,我们一般都会将部分数据放入缓存中,加速访问.而DB承担数据落盘工作

哪些数据适合放入缓存?

1.即时性,数据一致性要求不高的.
2.访问量大且更新频率不高的数据(读多,写少)

springboot引入starter依赖,想知道yml里怎么配置,可以查看xxxAutoConfigutarion里的xxxproperties
如果想把json串转换为对象,并且是一个复杂的对象,比如:Map<String,List<xxx>>
JSON.parseObject(jsonStrng,new TypeReference<Map<String,List<xxx>>>(){});

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TVU5bHuw-1619060478645)(C:\Users\lenovo\AppData\Local\Temp\1612680021380.png)]

压测下原生spring-data-redis-starter的问题

进行压力测试,发现运行一会后会爆出OutOfDirectMemoryError->堆外内存溢出
1.springboot2.0以后spring-data-redis-starter默认会使用lettuce作为操作redis的客户端,它使用netty进行网络通信
2.lettuce的bug导致netty堆外内存溢出,这里如果没有指定堆外内存,那么默认使用的就是服务启动指定的jvm内存
3.解决方案:(1)改造lettuce (2)使用jedis

<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>io.lettuce</groupId>
                    <artifactId>lettuce-core</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
        </dependency>

springboot2.1.8版本是这样的,后续lettuce如果进行了修复,还是可以直接使用的



后续:
其实不排除lettuce也可以使用jedis,只需要引入jedis依赖后,在yml中做出相应的配置
spring.redis.client-type=jedis

缓存穿透

指查询一个一定不存在的数据,由于缓存是不命中,将去查询数据库,但是数据库也无此记录,我们没有将这次查询的null写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义

风险

利用不存在的数据进行攻击,数据库瞬时压力增大,最终导致崩溃

解决

null结果缓存,并加入短暂过期时间

缓存雪崩

指我们设置缓存时key采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB瞬时压力过重雪崩
解决
原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件

缓存击穿

对于一些设置了过期过期时间的key,如果这些key可能会在某些时间点被超高并发的访问,是一种非常"热点"的数据
如果这个key在大量请求同时进来前正好失效,那么所有对这个key的数据查询都落在db,我们称之为缓存击穿

解决

加锁,大量并发只让一个人去查,其他人会等待,查到以后入数据至缓存,然后释放锁,其他人获取到锁,先查缓存,就会有数据,不用去DB查

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-G6V5328P-1619060478649)(C:\Users\lenovo\AppData\Local\Temp\1612684314882.png)]

分布式锁

核心命令

set nx 
向redis当中存入一个key,若这个key存在,则执行失败,也就是占锁失败,执行成功,则占锁成功

Redisson

<!-- https://mvnrepository.com/artifact/org.redisson/redisson -->
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.14.1</version>
</dependency>


1.Redisson实现可重入锁(Reentrant lock)

 /**
     * redisson 实现分布式锁
     * @return
     */
    @GetMapping("/hello")
    public R hello(){
        //1.获取锁,只要锁的名字一样,所有人的锁就是同一把
        RLock lock = redisson.getLock("mylock");
        System.out.println(Thread.currentThread().getId()+":  尝试获取锁");
        //2.加锁
        lock.lock();
        System.out.println(Thread.currentThread().getId()+":  成功获取锁");
        try {
            //3.执行业务代码
            System.out.println(Thread.currentThread().getId()+":  正在执行业务");
            try {
                Thread.sleep(30000);
            } catch (InterruptedException e) {

            }
        }finally {
            //4.解锁  -------->  
            
            lock.unlock();
            System.out.println(Thread.currentThread().getId()+":  释放锁");
        }
        return R.ok();
    }
    
    如果再执行解锁代码之前,服务down掉,那么锁会不会被释放掉呢?
    答: 锁会自动释放掉
    如果业务超长,锁到达了释放时间,会被删除掉吗?
    答: 不会,因为锁会自动续期,如果运行时间超长,运行期间redisson会自动给锁续上30s.(redisson锁默认30s)
        如果业务运行完成,就不会给锁续期,我们可以手动释放锁,甚至无手动释放代码,锁也会满足过期时间后被释放
        
        
   lock.lock();该方法锁在过期前如果业务还没有执行完,会自动续期     
   如果未指定超时时间,就是用默认配置30s为锁过期时间,只要占锁成功,
   就会启动定时任务[重新给锁设置过期时间,重新设置的过期时间还是默认配置30s]
   定时任务的执行周期是,锁的过期时间的1/3
   
   
   lock.lock(10,TimeUnit.SECONDS); //10后自动解锁
   如果使用该方法,锁不会自动续期
   如果传递了锁的超时时间,就发送给redis执行脚本,进行占锁,默认超时就是我们指定的时间
   
        
   最佳实战:
   lock.lock(10,TimeUnit.SECONDS);省掉了整个续期操作,手动解锁
   
   
   

尝试获取锁,超过了一定时间放弃获锁

public void test1(){
        RLock mylock = redisson.getLock("mylock");
        try {
            boolean b = mylock.tryLock(100, 10, TimeUnit.SECONDS);
            if(b){
                //执行业务代码
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            mylock.unlock();
        }
    }

公平锁

RLock fairLock =  redisson.getFairLock("anyLock");

fairLock.lock();

当10个线程争抢锁,第一个线程抢到锁后,剩余的线程则会依照顺序依次获取锁

读写锁

ReadWriteLock	rwlock  = redisson.getReadWriteLock("xxx");
//
rwlock.readLock().lock();
//
rwlock.writelock().lock();

分布式可重入读写锁允许同时又多个读锁和一个写锁处于加锁状态

保证一定能读到最新数据,修改期间,写锁是一个排它锁,读锁是一个共享锁
写锁没释放,读就必须等待
反之
读锁如果没释放,写锁就必须等待
但是读锁没释放,如果再来读操作,则无需等待

可见:  读锁为共享锁,写锁为排它锁

 /**
     * 读写锁
     * 写操作
     */
    @GetMapping("/write")
    public String write(){
        RReadWriteLock lock = redisson.getReadWriteLock("wr-lock");
        RLock rlock = lock.writeLock();
        String s =UUID.randomUUID().toString();
        try{
        //.写操作就加写锁,读操作就加读锁
            rlock.lock();
            System.out.println("写锁加锁成功......"+Thread.currentThread().getId());
            //模拟写的过程,写锁没释放,如果有人请求读,也会阻塞,知道写锁释放后,读请求才能成功
            Thread.sleep(10000);
            redisTemplate.opsForValue().set("writeValue",s);
        }catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            rlock.unlock();
            System.out.println("写锁释放锁");
            return s;
        }
    }

    /**
     * 读写锁
     * 读操作
     */
    @GetMapping("/read")
    public String read(){
        RReadWriteLock lock = redisson.getReadWriteLock("wr-lock");
        RLock rlock = lock.readLock();
        String writeValue =null;
        try{
            //.写操作就加写锁,读操作就加读锁
            rlock.lock();
            System.out.println("读锁加锁成功......"+Thread.currentThread().getId());
            writeValue = redisTemplate.opsForValue().get("writeValue");
        }catch (Exception e) {
            e.printStackTrace();
        }finally {
            rlock.unlock();
            System.out.println("读锁释放锁.....");
            return writeValue;
        }
    }

闭锁(CountDownLatch)

基于Redisson的分布式闭锁,采用了与JUC包相似的接口和用法


 /**
     * 5个班级人都做了,保安放假锁门
     * @return
     */
    @GetMapping("/lockDoor")
    public R lockDoor() throws InterruptedException {
        RCountDownLatch door = redisson.getCountDownLatch("door");
        door.trySetCount(5);
        door.await(); //等待闭锁都完成

        return R.ok("放假了...");
    }
    @GetMapping("/gogogo/{id}")
    public R gogogo(@PathVariable("id")Long id){
        RCountDownLatch door = redisson.getCountDownLatch("door");
        door.countDown();
        return R.ok(id+"班的人都走了");
    }

信号量(Semaphore)

/**
     * 信号量也可以用作分布式限流
     * 车库停车
     * @return
     * @throws InterruptedException
     */
    @GetMapping("/park")
    public R park() throws InterruptedException {
        RSemaphore semaphore = redisson.getSemaphore("park");
        semaphore.acquire();//获取一个信号,占一个车位

        semaphore.tryAcquire();// 尝试获取一个车位,有就获取,没有也不等待,直接返回
        return R.ok();
    }

    @GetMapping("/go")
    public R go(){
        RSemaphore semaphore = redisson.getSemaphore("park");
        semaphore.release(); //释放一个车位
        return R.ok();
    }

缓存数据一致性–解决方案

无论是双写模式还是失效模式,都会导致缓存的不一致问题,即多个实例同时更新会出事,怎么办?

1.如果是用户维度数据(订单数据,用户数据),这种并发几率小,不用考虑这个问题,缓存数据加上过期时间,每隔一段时间触发读的主动更新即可
2.如果是菜单,商品介绍等基础数据,也可以去使用canal订阅binlog的方式
3.缓存数据+过期时间也足够解决大部分企业业务对于缓存的要求
4.通过加锁保证并发读写,写写的时候按顺序排好队.读读无所谓,所以适合使用读写锁.(业务不关心脏数据,允许临时脏数据可忽略)
总结:
我们能放入缓存的数据本就不该是实时性,一致性要求超高的,所以缓存数据的时候加上过期时间,保证每天拿到当前最新数据即可
我们不应该过渡设计,增加系统的复杂性
遇到实时性,一致性要求高的数据,就应该查数据库,即使慢点

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-srqzBpN0-1619060478651)(C:\Users\lenovo\AppData\Local\Temp\1614240011107.png)]

canal作为一个阿里开源的中间件,可以实现代码层面无感知的刷新数据缓存,

还可以作为分析处理中间件,比如分析电商网站的用户访问记录表,收藏点赞表,购物车表等等,拿来数据做分析计算,
然后组装好用户推荐表,这样用户登录后直接访问的便是这个表数据,提升了整体加载性能

但canal的使用还是对于相对复杂的或大数据量的处理来用的,一般的系统也要视情况而定

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值