高性能分布式缓存Redis-分布式锁与布隆过滤器

news2024/12/25 14:37:29

一、分布式锁

我们先来看一下本地锁

在并发编程中,我们通过锁,来避免由于竞争而造成的数据不一致问题。通常,我们以 synchronized 、Lock 来使用它(单机情况)
来看这段代码
@Autowired
RedisTemplate<String,String> redisTemplate;

String maotai = "maotai20210321001";//茅台商品编号 

@PostConstruct
public void init(){ 
    //此处模拟向缓存中存入商品库存操作 
    redisTemplate.opsForValue().set(maotai,"100"); 
}

@GetMapping("/get/maotai2")
public String seckillMaotai2() {
    synchronized (this) {
        Integer count = Integer.parseInt(redisTemplate.opsForValue().get(maotai)); 
        // 1 //如果还有库存 
        if (count > 0) { 
            //抢到了茅台,库存减一 
            redisTemplate.opsForValue().set(maotai,String.valueOf(count-1));
            //后续操作 do something
            log.info("我抢到茅台了!");
            return "ok"; 
        }else { 
            return "no"; 
        }
    }
}
  • 现象:本地锁在多节点下失效(集群/分布式)
  • 原因:本地锁它只能锁住本地JVM进程中的多个线程,对于多个JVM进程的不同线程间是锁不住的
  • 解决:分布式锁(在分布式环境下提供锁服务,并且达到本地锁的效果)

那么,到底什么是分布式锁呢?

  • 当在分布式架构下,数据只有一份(或有限制),此时需要利用锁的技术控制某一时刻修改数据的进程数。
  • 用一个状态值表示锁,对锁的占用和释放通过状态值来标识。

基于Redis实现分布式锁

锁的实现主要基于 redis SETNX 命令:

大致流程:

  • 1. 使用 SETNX 命令获取锁,若返回0key已存在,锁已存在)则获取失败,反之获取成功
  • 2. 执行业务逻辑
  • 3. 释放锁,使用 DEL 命令将锁数据删除

锁超时

仅仅使用这个流程,会出现什么问题吗?思考:如果程序在第2步执行出现异常退出或宕机,没有执行第3步释放锁,岂不就使得这个锁一直未释放,别的请求一直得不到这个锁,这就产生了死锁问题!这也就是分布式锁的锁超时特点。

如何解决?在抢到锁的时候,给这个锁加上一个过期时间,即使没有执行手动释放锁,也会在过期时间到了自动释放锁!

代码如下:

    @GetMapping("/get/maotai3")
    public String seckillMaotai3() {
        
        //获取锁
        Boolean islock = redisTemplate.opsForValue().setIfAbsent(lockey, "1");
        if (islock) {
            //设置锁的过期时间
            redisTemplate.expire(lockey,5, TimeUnit.SECONDS);
            try {
                Integer count = Integer.parseInt(redisTemplate.opsForValue().get(maotai)); // 1
                //如果还有库存
                if (count > 0) {
                    //抢到了茅台,库存减一
                    redisTemplate.opsForValue().set(maotai,String.valueOf(count-1));
                    //后续操作 do something
                    log.info("我抢到茅台了!");
                    return "ok";
                }else {
                    return "no";
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                //释放锁
                redisTemplate.delete(lockey);
            }
        }
        return "dont get lock";
    }

原子性操作

在以上代码中,setnx和expire的操作是分开执行的,假设此时,setnx执行完毕,程序异常退出或宕机了,那就无法设置好过期时间,这样也会导致锁一直得不到释放

究其原因,这两步是非原子性操作(解决:2.6以前可用使用lua脚本,2.6以后可用set命令),使用lua脚本把这两个命令变成一气呵成的操作,确保都同时执行成功。

@GetMapping("/get/maotai4")
    public String seckillMaotai4() {
        //获取锁
        String locklua ="" +
                "if redis.call('setnx',KEYS[1],ARGV[1]) == 1 then redis.call('expire',KEYS[1],ARGV[2]) ; return true " +
                "else return false " +
                "end";
        Boolean islock = redisTemplate.execute(new RedisCallback<Boolean>() {
            @Override
            public Boolean doInRedis(RedisConnection redisConnection) throws DataAccessException {
                Boolean eval = redisConnection.eval(
                        locklua.getBytes(),
                        ReturnType.BOOLEAN,
                        1,
                        lockey.getBytes(),
                        requestid.getBytes(),
                        "5".getBytes()
                );
                return eval;
            }
        });
        
        if (islock) {
            try {
                Integer count = Integer.parseInt(redisTemplate.opsForValue().get(maotai)); // 1
                //如果还有库存
                if (count > 0) {
                    //抢到了茅台,库存减一
                    redisTemplate.opsForValue().set(maotai,String.valueOf(count-1));
                    //后续操作 do something
                    log.info("我抢到茅台了!");
                    return "ok";
                }else {
                    return "no";
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                //释放锁
                redisTemplate.delete(lockey);
            }
        }
        return "dont get lock";
    }

错误解锁

思考以上代码:假设设置的超时时间小于业务执行时间,导致业务代码没有执行完,锁就被释放了,这时候其他请求就能进到这个锁,操作一些共享变量导致线程安全问题

解决方案:在setnx时,将value设置成一个唯一标识,解锁的时候,要进行value的匹配,匹配上了才能解锁

@GetMapping("/get/maotai4")
    public String seckillMaotai4() {
        String requestid = UUID.randomUUID().toString() + Thread.currentThread().getId();
        /*String locklua ="" +
                "if redis.call('setnx',KEYS[1],ARGV[1]) == 1 then redis.call('expire',KEYS[1],ARGV[2]) ; return true " +
                "else return false " +
                "end";
        Boolean islock = redisTemplate.execute(new RedisCallback<Boolean>() {
            @Override
            public Boolean doInRedis(RedisConnection redisConnection) throws DataAccessException {
                Boolean eval = redisConnection.eval(
                        locklua.getBytes(),
                        ReturnType.BOOLEAN,
                        1,
                        lockey.getBytes(),
                        requestid.getBytes(),
                        "5".getBytes()
                );
                return eval;
            }
        });*/
        //获取锁
        Boolean islock = redisTemplate.opsForValue().setIfAbsent(lockey,requestid,5,TimeUnit.SECONDS);
        if (islock) {
            try {
                Integer count = Integer.parseInt(redisTemplate.opsForValue().get(maotai)); // 1
                //如果还有库存
                if (count > 0) {
                    //抢到了茅台,库存减一
                    redisTemplate.opsForValue().set(maotai,String.valueOf(count-1));
                    //后续操作 do something
                    log.info("我抢到茅台了!");
                    return "ok";
                }else {
                    return "no";
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                //释放锁
                //判断是自己的锁才能去释放 这种操作不是原子性的
                /*String id = redisTemplate.opsForValue().get(lockey);
                if (id !=null && id.equals(requestid)) {
                    redisTemplate.delete(lockey);
                }*/
                String unlocklua = "" +
                        "if redis.call('get',KEYS[1]) == ARGV[1] then redis.call('del',KEYS[1]) ; return true " +
                        "else return false " +
                        "end";
                redisTemplate.execute(new RedisCallback<Boolean>() {
                    @Override
                    public Boolean doInRedis(RedisConnection redisConnection) throws DataAccessException {
                        Boolean eval = redisConnection.eval(
                                unlocklua.getBytes(),
                                ReturnType.BOOLEAN,
                                1,
                                lockey.getBytes(),
                                requestid.getBytes()
                        );
                        return eval;
                    }
                });
            }
        }
        return "dont get lock";
    }

锁续期

思考上面代码:在下图里面,只是解决了业务代码执行完,释放锁只能释放自己的锁,但是,当锁过期了,其他请求不也一样的进到了锁里面操作共享变量吗?究其原因是锁的过期时间小于业务代码执行时间,那解决办法自然就是把这个时间延长呗

那这个时间延长到底如何做?

给拿到锁的线程创建一个守护线程(看门狗),守护线程 定时/延迟(如每隔5s检查业务是否执行完毕) 判断拿到锁的线程是否还继续持有锁,如果持有则为其续期

    //模拟一下守护线程为其续期
    ScheduledExecutorService executorService;//创建守护线程池
    ConcurrentSkipListSet<String> set = new ConcurrentSkipListSet<String>();//队列

    @PostConstruct
    public void init2(){
        executorService = Executors.newScheduledThreadPool(1);

        //编写续期的lua
        String expirrenew = "" +
                "if redis.call('get',KEYS[1]) == ARGV[1] then redis.call('expire',KEYS[1],ARGV[2]) ; return true " +
                "else return false " +
                "end";

        executorService.scheduleAtFixedRate(new Runnable() {
            @Override
            public void run() {
                Iterator<String> iterator = set.iterator();
                while (iterator.hasNext()) {
                    String rquestid = iterator.next();

                    redisTemplate.execute(new RedisCallback<Boolean>() {
                        @Override
                        public Boolean doInRedis(RedisConnection redisConnection) throws DataAccessException {
                            Boolean eval = false;
                            try {
                                eval = redisConnection.eval(
                                        expirrenew.getBytes(),
                                        ReturnType.BOOLEAN,
                                        1,
                                        lockey.getBytes(),
                                        rquestid.getBytes(),
                                        "5".getBytes()
                                );
                            } catch (Exception e) {
                                log.error("锁续期失败,{}",e.getMessage());
                            }
                            return eval;
                        }
                    });

                }
            }
        },0,1,TimeUnit.SECONDS);
    }

    @GetMapping("/get/maotai5")
    public String seckillMaotai5() {
        String requestid = UUID.randomUUID().toString() + Thread.currentThread().getId();
        //获取锁
        Boolean islock = redisTemplate.opsForValue().setIfAbsent(lockey,requestid,5,TimeUnit.SECONDS);
        if (islock) {
            //获取锁成功后让守护线程为其续期
            set.add(requestid);
            try {
                Integer count = Integer.parseInt(redisTemplate.opsForValue().get(maotai)); // 1
                //如果还有库存
                if (count > 0) {
                    //抢到了茅台,库存减一
                    redisTemplate.opsForValue().set(maotai,String.valueOf(count-1));
                    //后续操作 do something
                    //seckillMaotai5();
                    //模拟业务超时
                    TimeUnit.SECONDS.sleep(10);
                    log.info("我抢到茅台了!");
                    return "ok";
                }else {
                    return "no";
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                //解除锁续期
               set.remove(requestid);
                //释放锁
                String unlocklua = "" +
                        "if redis.call('get',KEYS[1]) == ARGV[1] then redis.call('del',KEYS[1]) ; return true " +
                        "else return false " +
                        "end";
                redisTemplate.execute(new RedisCallback<Boolean>() {
                    @Override
                    public Boolean doInRedis(RedisConnection redisConnection) throws DataAccessException {
                        Boolean eval = redisConnection.eval(
                                unlocklua.getBytes(),
                                ReturnType.BOOLEAN,
                                1,
                                lockey.getBytes(),
                                requestid.getBytes()
                        );
                        return eval;
                    }
                });
            }
        }
        return "dont get lock";
    }

锁的可重入

思考一个问题,如果在一段代码里,我拿到了锁,这个代码要递归调用自己,就面临着再次获取锁,所以确保这次获取锁是成功的,这称为锁的可重入

可重入如何做呢?我们可不可以使得value是一个数值,在锁的内部每加一次锁就让数值加1,每释放一次锁就让数值减1。我们用hash的类型来做这个。基于这个思路我们看一下实现流程:

  1. 加锁:当一个客户端尝试获取锁时,首先检查锁是否已被其他客户端持有。如果没有,则设置锁并成为持有者;如果有,但持有者是自己,则增加计数器。
  2. 解锁:当一个客户端释放锁时,减少计数器。如果计数器变为0,则删除锁,这样其他客户端才能获取锁。

 加锁的lua脚本:

                        "if (redis.call('exists', KEYS[1]) == 0) then " +
                             #设置锁key,field是唯一标识,value是重入次数
                            "redis.call('hset', KEYS[1], ARGV[2], 1); " +
                            #设置锁key的过期时间 默认30s
                            "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                            "return nil; " +
                        "end; " +
                        #如果锁key存在
                        "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
                             #重入次数+1
                            "redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
                             #重置过期时间
                            "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                            "return nil; " +
                        "end; " +
                        "return redis.call('pttl', KEYS[1]);"

解锁的lua脚本:

                      "if (redis.call('exists', KEYS[1]) == 0) then " +
                          "redis.call('publish', KEYS[2], ARGV[1]); " +
                          "return 1; " +
                      "end;" +
                       // hash 中的field 不存在时直接返回,
                      "if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +
                          "return nil;" +
                      "end; " +
                      "local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +
                      //重入次数-1后如果还大于0,延长过期时间
                      "if (counter > 0) then " +
                          "redis.call('pexpire', KEYS[1], ARGV[2]); " +
                          "return 0; " +
                      "else " +
                      //重入次数-1后如果归0,则删除key,并向redisson_lock__channel:{key}频道发布锁释放消息
                          "redis.call('del', KEYS[1]); " +
                          "redis.call('publish', KEYS[2], ARGV[1]); " +
                          "return 1; "+
                      "end; " +
                      "return nil;"

阻塞锁

我们要使得,每个请求来获取锁,如果锁已被占用,那么获取不到就等待锁的释放,直到获取到锁或者等待超时,常见方案有两种:

  • 1:基于客户端轮询的方案
  • 2:基于redis的发布/订阅方案

第2种方案:对于每个抢不到锁的进程,就订阅一个频道,当释放锁时,会向这个频道发布通知, 收到通知再进行重新抢锁

以上这些特性都是分布式锁应该满足的,那么自己写起来还是不太方便,Redisson就帮我们封装好了这一切,直接用

基于Redisson 实现分布式锁

Redisson的介绍

        Redisson内置了一系列的分布式对象,分布式集合,分布式锁,分布式服务等诸多功能特性,是一款基于Redis实现,拥有一系列分布式系统功能特性的工具包,是实现分布式系统架构中缓存中间件的最佳选择。

使用步骤

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.8.2</version>
</dependency>
    @Bean
    public RedissonClient redissonClient() {
        Config config = new Config();
        config.useSingleServer().setAddress("redis://"+host+":"+port);
        return Redisson.create(config);
    }

    @Autowired
    RedissonClient redissonClient;


    @GetMapping("/get/maotai6")
    public String seckillMaotai6() {
        //要去获取锁
        RLock lock = redissonClient.getLock(lockey);
        lock.lock();
        try {
            Integer count = Integer.parseInt(redisTemplate.opsForValue().get(maotai)); // 1
            //如果还有库存
            if (count > 0) {
                //抢到了茅台,库存减一
                redisTemplate.opsForValue().set(maotai,String.valueOf(count-1));
                //后续操作 do something
                log.info("我抢到茅台了!");
                return "ok";
            }else {
                return "no";
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();;
        }
        return "";
    }

分布式锁特点

总结来说,设计一个完善的分布式锁,需要满足下面这些特点

  • 互斥性:不仅要在同一jvm进程下的不同线程间互斥,更要在不同jvm进程下的不同线程间互斥
  • 锁超时:支持锁的自动释放,防止死锁
  • 正确,高效,高可用(解决错误解锁问题):解铃还须系铃人(加锁和解锁必须是同一个线程),加锁和解锁操作一定要高效,提供锁的服务要具备容错性
  • 可重入:如果一个线程拿到了锁之后继续去获取锁还能获取到,我们称锁是可重入的(方法的递归调用)
  • 阻塞/非阻塞:如果获取不到直接返回视为非阻塞的,如果获取不到会等待锁的释放直到获取锁或者等待超时,视为阻塞的
  • 公平/非公平:按照请求的顺序获取锁视为公平的

前三点使我们必须要满足的,后两点是分布式锁的类型 

二、布隆过滤器

我们先从一个场景说起

如果数据在缓存和数据库中都不存在,以id=-1为例,如果频繁的请求这个id的数据,会被频繁的访问数据库判断是否存在,直至请求多到把数据库压垮,这就是缓存穿透问题

那如何解决呢?我们就可以用布隆过滤器,布隆过滤器就可以实现在海量元素中,快速判断一个元素是否存在

布隆过滤器本质上其实就是一个很长的二进制向量和一系列随机映射函数。专门用来检测集合中是否存在特定的元素

布隆过滤器的设计

BF是由一个长度为m比特的位数组(bit array)k个哈希函数(hash function)组成的数据结构。位数组均初始化为0,所有哈希函数都可以分别把输入数据尽量均匀地散列。

这个二进制位数据就由bitmap来实现

如果我们要映射一个值到布隆过滤器中,我们需要使用多个不同的哈希函数生成多个哈希值,并对每个生成的哈希值指向的 bit 位,设置为1

 

 当要插入一个元素时,将其数据分别输入k个哈希函数,产生k个哈希值。以哈希值作为位数组中的下标,将所有k个对应的比特置为1。

当要查询(即判断是否存在)一个元素时,同样将其数据输入哈希函数,然后检查对应的k个比特。如果有任意一个比特为0,表明该元素一定不在集合中。如果所有比特均为1,表明该集合有(较大的)可能性在集合中。为什么不是一定在集合中呢?因为一个比特被置为1有可能会受到其他元素的影响,这就是所谓“假阳性”(false positive)。相对地,“假阴性”(false negative)在BF中是绝不会出现的。

总结,对于BF的查询结果:

  • 如果这些点有任何一个 0,则被检索元素一定不在
  • 如果都是 1,则被检索元素很可能在。

 布隆过滤器的误判是指多个输入经过哈希之后在相同的bit位置1了,这样就无法判断究竟是哪个输入产生的,因此误判的根源在于相同的 bit 位被多次映射且置 1。

 误报率指的是你愿意接受的误报概率。误报是指布隆过滤器告诉你一个元素“可能”存在于集合中,但实际上并不在集合中。误报率越低,布隆过滤器所需的内存就越多。

在redis中使用BF

<dependency>
  <groupId>org.redisson</groupId>
  <artifactId>redisson-spring-boot-starter</artifactId>
  <version>3.13.4</version>
</dependency>
public class RedissonBloomFilter {

  public static void main(String[] args) {
    Config config = new Config();
    config.useSingleServer().setAddress("redis://127.0.0.1:6379");
    config.useSingleServer().setPassword("1234");
    //构造Redisson
    RedissonClient redisson = Redisson.create(config);

    RBloomFilter<String> bloomFilter = redisson.getBloomFilter("phoneList");
    //初始化布隆过滤器:预计元素为100000000L,偏差率为3%
    bloomFilter.tryInit(100000000L,0.03);
    //将号码10086插入到布隆过滤器中
    bloomFilter.add("10086");

    //判断下面号码是否在布隆过滤器中
    //输出false
    System.out.println(bloomFilter.contains("123456"));
    //输出true
    System.out.println(bloomFilter.contains("10086"));
  }
}

解释一下bloomFilter.tryInit(100000000L,0.03);

  • 100000000L 表示你期望这个布隆过滤器能够容纳大约一亿(100,000,000)个元素。
  • 0.03 表示你希望这个布隆过滤器误判率不超过 3%。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2241117.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

SpringSecurity+jwt+captcha登录认证授权总结

SpringSecurityjwtcaptcha登录认证授权总结 版本信息&#xff1a; springboot 3.2.0、springSecurity 6.2.0、mybatis-plus 3.5.5 认证授权思路和流程&#xff1a; 未携带token&#xff0c;访问登录接口&#xff1a; 1、用户登录携带账号密码 2、请求到达自定义Filter&am…

从社交媒体到元宇宙:Facebook未来发展新方向

Facebook&#xff0c;作为全球最大的社交媒体平台之一&#xff0c;已经从最初的简单互动工具发展成为一个跨越多个领域的科技巨头。无论是连接人与人之间的社交纽带&#xff0c;还是利用大数据、人工智能等技术为用户提供个性化的体验&#xff0c;Facebook一直引领着社交网络的…

javascript用来干嘛的?赋予网站灵魂的语言

javascript用来干嘛的&#xff1f;赋予网站灵魂的语言 在互联网世界中&#xff0c;你所浏览的每一个网页&#xff0c;背后都有一群默默工作的代码在支撑着。而其中&#xff0c;JavaScript就像是一位技艺精湛的魔术师&#xff0c;它赋予了网页生命力&#xff0c;让原本静态的页…

Wordpress常用配置,包括看板娘跨域等

一个Wordpress的博客已经搭建完成了&#xff0c;那么为了让它看起来更有人间烟火气一点&#xff0c;有一些常用的初始配置&#xff0c;这里整理一下。 修改页脚 页脚这里默认会显示Powered by Wordpress&#xff0c;还有一个原因是这里要加上备案信息。在主题里找到页脚&…

The Internals of PostgreSQL 翻译版 持续更新...

为了方便自己快速学习&#xff0c;整理了翻译版本&#xff0c;目前翻译的还不完善&#xff0c;后续会边学习边完善。 文档用于自己快速参考&#xff0c;会持续修正&#xff0c;能力有限,无法确保正确!!! 《The Internals of PostgreSQL 》 不是 《 PostgreSQL14 Internals 》…

FlinkPipelineComposer 详解

FlinkPipelineComposer 详解 原文 背景 在flink-cdc 3.0中引入了pipeline机制&#xff0c;提供了除Datastream api/flink sql以外的一种方式定义flink 任务 通过提供一个yaml文件&#xff0c;描述source sink transform等主要信息 由FlinkPipelineComposer解析&#xff0c…

MybatisPlus知识

mybatis与mybatisplus的区别&#xff1a; mybatisplus顾名思义时mybatis的升级版&#xff0c;提供了更多的API和方法&#xff0c;是基于mybatis框架基础上的升级&#xff0c;更加方便开发。mybatisplus继承BaseMapper接口并调用其中提供的方法来操作数据库&#xff0c;不需要再…

利用飞书多维表格自动发布版本

文章目录 背景尝试1&#xff0c;轮询尝试2&#xff0c;长连接 背景 博主所在的部门比较奇特&#xff0c;每个车型每周都需要发版&#xff0c;所以实际上一周会发布好几个版本。经过之前使用流水线自动发版改造之后&#xff0c;发版的成本已经大大降低了&#xff0c;具体参考&a…

Qwen2-VL:发票数据提取、视频聊天和使用 PDF 的多模态 RAG 的实践指南

概述 随着人工智能技术的迅猛发展&#xff0c;多模态模型在各类应用场景中展现出强大的潜力和广泛的适用性。Qwen2-VL 作为最新一代的多模态大模型&#xff0c;融合了视觉与语言处理能力&#xff0c;旨在提升复杂任务的执行效率和准确性。本指南聚焦于 Qwen2-VL 在三个关键领域…

蓝桥杯每日真题 - 第7天

题目&#xff1a;&#xff08;爬山&#xff09; 题目描述&#xff08;X届 C&C B组X题&#xff09; 解题思路&#xff1a; 前缀和构造&#xff1a;为了高效地计算子数组的和&#xff0c;我们可以先构造前缀和数组 a&#xff0c;其中 a[i] 表示从第 1 个元素到第 i 个元素的…

家政服务小程序,家政行业数字化发展下的优势

今年以来&#xff0c;家政市场需求持续增长&#xff0c;市场规模达到了万亿级别&#xff0c;家政服务行业成为了热门行业之一&#xff01; 家政服务种类目前逐渐呈现了多样化&#xff0c;月嫂、保姆、做饭保洁、收纳、维修等家政种类不断出现&#xff0c;满足了居民日益增长的…

蓝桥杯每日真题 - 第12天

题目&#xff1a;&#xff08;数三角&#xff09; 题目描述&#xff08;14届 C&C B组E题&#xff09; 解题思路&#xff1a; 给定 n 个点的坐标&#xff0c;计算其中可以组成 等腰三角形 的三点组合数量。 核心条件&#xff1a;等腰三角形的定义是三角形的三条边中至少有…

Linux系统下svn新建目录

Linux安装svn自行查找 新建目录 新建一个自定义库的文件夹&#xff1a;mkdir security 使用svnadmin命令在新创建的目录中创建一个新的SVN版本库。例如&#xff1a; svnadmin create security 执行完成以上命令就会生成默认配置文件 通过pwd命令查找当前目录路径 路径&…

SpringCloud基础 入门级 学习SpringCloud 超详细(简单通俗易懂)

Spring Cloud 基础入门级学习 超详细&#xff08;简单通俗易懂&#xff09; 一、SpringCloud核心组件第一代&#xff1a;SpringCloud Netflix组件第二代&#xff1a;SpringCloud Alibaba组件SpringCloud原生组件 二、SpringCloud体系架构图三、理解分布式与集群分布式集群 四、…

性能调优专题(9)之从JDK源码级别解析JVM类加载机制

一、类加载运行全过程 当我们用java命令运行某个类的main函数启动程序时&#xff0c;首先需要通过类加载器把主类加载到JVM。 package com.tuling.jvm;public class Math {public static final int initData 666;public static User user new User();public int compute() {…

【全面系统性介绍】虚拟机VM中CentOS 7 安装和网络配置指南

一、CentOS 7下载源 华为源&#xff1a;https://mirrors.huaweicloud.com/centos/7/isos/x86_64/ 阿里云源&#xff1a;centos-vault-7.9.2009-isos-x86_64安装包下载_开源镜像站-阿里云 百度网盘源&#xff1a;https://pan.baidu.com/s/1MjFPWS2P2pIRMLA2ioDlVg?pwdfudi &…

「JVM详解」

JVM JVM概述 基本介绍 JVM&#xff1a;全称 Java Virtual Machine&#xff0c;即 Java 虚拟机&#xff0c;一种规范&#xff0c;本身是一个虚拟计算机&#xff0c;直接和操作系统进行交互&#xff0c;与硬件不直接交互&#xff0c;而操作系统可以帮我们完成和硬件进行交互的…

正点原子IMX6ULL--嵌入式Linux开发板学习中常用命令和笔记记录

学习路线图 传驱动文件 sudo cp chrdevbase.ko chrdevbaseApp /home/txj/linux/nfs/rootfs/lib/modules/4.1.15/ -f bootcmd setenv bootcmd tftp 80800000 zImage;tftp 83000000 imx6ull-alientek-emmc.dtb;bootz 80800000 - 83000000 setenv bootcmd tftp 80800000 zImag…

DVWA靶场通关——SQL Injection篇

一&#xff0c;Low难度下unionget字符串select注入 1&#xff0c;首先手工注入判断是否存在SQL注入漏洞&#xff0c;输入1 这是正常回显的结果&#xff0c;再键入1 You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for…

CSS回顾-基础知识详解

一、引言 在前端开发领域&#xff0c;CSS 曾是构建网页视觉效果的关键&#xff0c;与 HTML、JavaScript 一起打造精彩的网络世界。但随着组件库的大量涌现&#xff0c;我们亲手书写 CSS 样式的情况越来越少&#xff0c;CSS 基础知识也逐渐被我们遗忘。 现在&#xff0c;这种遗…