大厂生产级Redis高并发分布式锁实战

news2024/11/17 5:34:29

文章目录

    • 一、扣减库存不加锁
    • 二、加一把jvm锁试试看
    • 三、引入分布式锁
    • 四、try finally
    • 五、设置key的过期时间
    • 六、原子设置锁和过期时间
    • 七、给线程设置唯一id
    • 八、锁续命redisson
    • 九、redisson加锁释放锁的逻辑
    • 十、redisson源码分析

一、扣减库存不加锁

先看一段扣减库存的代码

 @Autowired
 private StringRedisTemplate stringRedisTemplate;

  @RequestMapping("/deduct_stock")
    public String deductStock() {
          int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); // jedis.get("stock")
            if (stock > 0) {
                int realStock = stock - 1;
                stringRedisTemplate.opsForValue().set("stock", realStock + ""); // jedis.set(key,value)
                System.out.println("扣减成功,剩余库存:" + realStock);
            } else {
                System.out.println("扣减失败,库存不足");
            }
      return "end";
}

这段代码明显有很严重的并发问题,多线程并发执行的时候,假如三个线程同时执行,如果原先300的库存,理论三个线程执行完剩余库存是297,但是因为代码没有任何锁的控制,会导致同时读取300的库存,同时扣减1,又同时设置299到redis中去,会导致超卖问题

二、加一把jvm锁试试看

 @Autowired
 private StringRedisTemplate stringRedisTemplate;

  @RequestMapping("/deduct_stock")
    public String deductStock() {
         synchronized(this){
            int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); // jedis.get("stock")
            if (stock > 0) {
                int realStock = stock - 1;
                stringRedisTemplate.opsForValue().set("stock", realStock + ""); // jedis.set(key,value)
                System.out.println("扣减成功,剩余库存:" + realStock);
            } else {
                System.out.println("扣减失败,库存不足");
            }
        }
         
      return "end";
}

但是这还会有问题,如果是单机那确实没问题,但是在分布式环境下,还是会存在并发安全问题

三、引入分布式锁

 @Autowired
 private StringRedisTemplate stringRedisTemplate;

  @RequestMapping("/deduct_stock")
    public String deductStock() {
         String lockKey = "lock:product_101";
         Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "zhuge");//利用redis加分布式锁
           if (!result) {
              return "error_code";
           }
          int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); // jedis.get("stock")
            if (stock > 0) {
                int realStock = stock - 1;
                stringRedisTemplate.opsForValue().set("stock", realStock + ""); // jedis.set(key,value)
                System.out.println("扣减成功,剩余库存:" + realStock);
            } else {
                System.out.println("扣减失败,库存不足");
            }
            stringRedisTemplate.delete(lockKey);//释放锁
      return "end";
}

但这只能算一个入门级别的分布式锁,假如业务代码出问题了,那么最后释放锁的代码就不会去执行,就会导致死锁

四、try finally

 @Autowired
 private StringRedisTemplate stringRedisTemplate;

  @RequestMapping("/deduct_stock")
    public String deductStock() {
         String lockKey = "lock:product_101";
         Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "zhuge");//利用redis加分布式锁
           if (!result) {
              return "error_code";
           }
         try { 
          int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); // jedis.get("stock")
            if (stock > 0) {
                int realStock = stock - 1;
                stringRedisTemplate.opsForValue().set("stock", realStock + ""); // jedis.set(key,value)
                System.out.println("扣减成功,剩余库存:" + realStock);
            } else {
                System.out.println("扣减失败,库存不足");
            }
            }finally{
              stringRedisTemplate.delete(lockKey);//释放锁
            }
      return "end";
}

这样就保证了,即使业务代码出问题了也能去释放锁。
但是还是有问题,假如机器宕机了也会出现死锁

五、设置key的过期时间

 @Autowired
 private StringRedisTemplate stringRedisTemplate;

  @RequestMapping("/deduct_stock")
    public String deductStock() {
         String lockKey = "lock:product_101";
         Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "zhuge");//利用redis加分布式锁
         stringRedisTemplate.expire(lockKey, 10, TimeUnit.SECONDS);//加上过期时间
           if (!result) {
              return "error_code";
           }
         try { 
          int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); // jedis.get("stock")
            if (stock > 0) {
                int realStock = stock - 1;
                stringRedisTemplate.opsForValue().set("stock", realStock + ""); // jedis.set(key,value)
                System.out.println("扣减成功,剩余库存:" + realStock);
            } else {
                System.out.println("扣减失败,库存不足");
            }
            }finally{
              stringRedisTemplate.delete(lockKey);//释放锁
            }
      return "end";
}

但是还是有问题,因为上锁和设置过期时间是两步操作,存在原子性问题(如果加了锁,还没来得及执行设置过期时间的代码 ,就宕机了依然存在问题)

六、原子设置锁和过期时间

 @Autowired
 private StringRedisTemplate stringRedisTemplate;

  @RequestMapping("/deduct_stock")
    public String deductStock() {
         String lockKey = "lock:product_101";
         Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, 10, TimeUnit.SECONDS); //jedis.setnx(k,v)
           if (!result) {
              return "error_code";
           }
         try { 
          int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); // jedis.get("stock")
            if (stock > 0) {
                int realStock = stock - 1;
                stringRedisTemplate.opsForValue().set("stock", realStock + ""); // jedis.set(key,value)
                System.out.println("扣减成功,剩余库存:" + realStock);
            } else {
                System.out.println("扣减失败,库存不足");
            }
            }finally{
              stringRedisTemplate.delete(lockKey);//释放锁
            }
      return "end";
}

但是这样写还是会有问题,假如业务代码+接口响应时间的执行时间超过了10s,那么key就自动过期了,会导致其他线程抢占到锁,但是之前的线程执行结束的时候,会去释放锁,但是释放的不是自己的锁,而是后来的线程的锁

七、给线程设置唯一id

 @Autowired
 private StringRedisTemplate stringRedisTemplate;

  @RequestMapping("/deduct_stock")
    public String deductStock() {
         String lockKey = "lock:product_101";
         String clientId = UUID.randomUUID().toString();
         Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, clientId, 30, TimeUnit.SECONDS); //jedis.setnx(k,v)
           if (!result) {
              return "error_code";
           }
         try { 
          int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); // jedis.get("stock")
            if (stock > 0) {
                int realStock = stock - 1;
                stringRedisTemplate.opsForValue().set("stock", realStock + ""); // jedis.set(key,value)
                System.out.println("扣减成功,剩余库存:" + realStock);
            } else {
                System.out.println("扣减失败,库存不足");
            }
            }finally{
              if (clientId.equals(stringRedisTemplate.opsForValue().get(lockKey))) {//判断当前的clientid和枷锁的id是否相同
                stringRedisTemplate.delete(lockKey);
                }
            }
      return "end";
}

但是,还是有问题;就是锁释放的时候,依然存在原子性问题
我们发现上述的大部分问题都是锁过期导致,那么我们引入锁续命的概念

八、锁续命redisson

        <dependency>
			<groupId>org.redisson</groupId>
			<artifactId>redisson</artifactId>
			<version>3.6.5</version>
		</dependency>
 @Autowired
 private Redisson redisson;
 @Autowired
 private StringRedisTemplate stringRedisTemplate;

  @RequestMapping("/deduct_stock")
    public String deductStock() {
         String lockKey = "lock:product_101";
         //获取锁对象
        RLock redissonLock = redisson.getLock(lockKey);
        //加分布式锁
        redissonLock.lock();  //  .setIfAbsent(lockKey, clientId, 30, TimeUnit.SECONDS);
         try { 
          int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); // jedis.get("stock")
            if (stock > 0) {
                int realStock = stock - 1;
                stringRedisTemplate.opsForValue().set("stock", realStock + ""); // jedis.set(key,value)
                System.out.println("扣减成功,剩余库存:" + realStock);
            } else {
                System.out.println("扣减失败,库存不足");
            }
            }finally{
                //解锁
               redissonLock.unlock();
            }
      return "end";
}

九、redisson加锁释放锁的逻辑

在这里插入图片描述

十、redisson源码分析

我们进入 redissonLock.lock();方法内部

 public void lock() {
        try {
            this.lockInterruptibly();
        } catch (InterruptedException var2) {
            Thread.currentThread().interrupt();
        }

    }
public void lockInterruptibly() throws InterruptedException {
        this.lockInterruptibly(-1L, (TimeUnit)null);
    }

再进入 lockInterruptibly方法

  public void lockInterruptibly(long leaseTime, TimeUnit unit) throws InterruptedException {
        long threadId = Thread.currentThread().getId();
        Long ttl = this.tryAcquire(leaseTime, unit, threadId);//这是核心逻辑
        if (ttl != null) {
            RFuture<RedissonLockEntry> future = this.subscribe(threadId);
            this.commandExecutor.syncSubscription(future);

            try {
                while(true) {
                    ttl = this.tryAcquire(leaseTime, unit, threadId);
                    if (ttl == null) {
                        return;
                    }

                    if (ttl >= 0L) {
                        this.getEntry(threadId).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
                    } else {
                        this.getEntry(threadId).getLatch().acquire();
                    }
                }
            } finally {
                this.unsubscribe(future, threadId);
            }
        }
    }

再进入 tryAcquire方法


  private Long tryAcquire(long leaseTime, TimeUnit unit, long threadId) {
        return get(tryAcquireAsync(leaseTime, unit, threadId));
    }

  private <T> RFuture<Long> tryAcquireAsync(long leaseTime, TimeUnit unit, final long threadId) {
        if (leaseTime != -1) {
            return tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
        }
        RFuture<Long> ttlRemainingFuture = tryLockInnerAsync(commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
        ttlRemainingFuture.addListener(new FutureListener<Long>() { //tryLockInnerAsync方式执行结束,会回调addListener方法
            @Override
            public void operationComplete(Future<Long> future) throws Exception {
                if (!future.isSuccess()) {
                    return;
                }

                Long ttlRemaining = future.getNow();
                // lock acquired
                if (ttlRemaining == null) {
                    scheduleExpirationRenewal(threadId);
                }
            }
        });
        return ttlRemainingFuture;
    }

leaseTime 上面传进来是-1,会进入 tryLockInnerAsync

 <T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
        internalLockLeaseTime = unit.toMillis(leaseTime);

        return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,
                  "if (redis.call('exists', KEYS[1]) == 0) then " +   //判断是否有key,keys[1]等同于getName()
                      "redis.call('hset', KEYS[1], ARGV[2], 1); " + //第一遍肯定是空的,所以设置hash结构,key就是getName(),也就是我们定义的“lock:product_101”,filed是getLockName(threadId)相当于clientId,value为1表示可重入
                      "redis.call('pexpire', KEYS[1], ARGV[1]); " +//设置key的超时时间internalLockLeaseTime,默认30s
                      "return nil; " +
                  "end; " +
                  "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
                      "redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
                      "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                      "return nil; " +
                  "end; " +
                  "return redis.call('pttl', KEYS[1]);",
                    Collections.<Object>singletonList(getName()), internalLockLeaseTime, getLockName(threadId));
    }
  final UUID id;

    String getLockName(long threadId) {
        return id + ":" + threadId;
    }

我们会发现 原来底层就是一段lua脚本,这段lua脚本执行的主体逻辑就是加锁,加锁成功返回nil(null)

我们继续看看门狗的逻辑

public void operationComplete(Future<Long> future) throws Exception {
                if (!future.isSuccess()) {//加锁失败就返回
                    return;
                }

                Long ttlRemaining = future.getNow();//一般加锁成功,这里返回的就是null
                // lock acquired
                if (ttlRemaining == null) {
                    scheduleExpirationRenewal(threadId);//超时时间的刷新,也就是锁续命逻辑
                }
            }

private void scheduleExpirationRenewal(final long threadId) {
        if (expirationRenewalMap.containsKey(getEntryName())) {
            return;
        }

        Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
            @Override
            public void run(Timeout timeout) throws Exception {//这个run方法会在延时之后执行,延时多久呢,internalLockLeaseTime / 3,30/3 =10s
                
                RFuture<Boolean> future = commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
                        "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +//判断主线程是否结束了
                            "redis.call('pexpire', KEYS[1], ARGV[1]); " +//没结束,就续命internalLockLeaseTime
                            "return 1; " +
                        "end; " +
                        "return 0;",
                          Collections.<Object>singletonList(getName()), internalLockLeaseTime, getLockName(threadId));
                
                future.addListener(new FutureListener<Boolean>() {//续命逻辑执行完会回调此方法
                    @Override
                    public void operationComplete(Future<Boolean> future) throws Exception {
                        expirationRenewalMap.remove(getEntryName());
                        if (!future.isSuccess()) {
                            log.error("Can't update lock " + getName() + " expiration", future.cause());
                            return;
                        }
                        
                        if (future.getNow()) {//如果续命成功会返回1,进入if
                            // reschedule itself
                            scheduleExpirationRenewal(threadId);//再次调用此方法,又会等待10s再次去执行续命逻辑
                        }
                    }
                });
            }
        }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);

        if (expirationRenewalMap.putIfAbsent(getEntryName(), task) != null) {
            task.cancel();
        }
    }

以上就是redisson的看门狗续命逻辑

我们继续再看看,其他线程加锁失败的底层逻辑,还是加锁的那段lua脚本

 <T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
        internalLockLeaseTime = unit.toMillis(leaseTime);

        return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,
                  "if (redis.call('exists', KEYS[1]) == 0) then " +   //判断是否有key,keys[1]等同于getName()
                      "redis.call('hset', KEYS[1], ARGV[2], 1); " + //第一遍肯定是空的,所以设置hash结构,key就是getName(),也就是我们定义的“lock:product_101”,filed是getLockName(threadId)相当于clientId,value为1表示可重入
                      "redis.call('pexpire', KEYS[1], ARGV[1]); " +//设置key的超时时间internalLockLeaseTime,默认30s
                      "return nil; " +
                  "end; " +
                  "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
                      "redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
                      "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                      "return nil; " +
                  "end; " +
                  "return redis.call('pttl', KEYS[1]);",//加锁失败会走这段逻辑,返回这把锁剩余的超时时间
                    Collections.<Object>singletonList(getName()), internalLockLeaseTime, getLockName(threadId));

我们返回到 lockInterruptibly方法

   @Override
    public void lockInterruptibly(long leaseTime, TimeUnit unit) throws InterruptedException {
        long threadId = Thread.currentThread().getId();
        Long ttl = tryAcquire(leaseTime, unit, threadId);//加锁成功,返回的null
        // lock acquired
        if (ttl == null) {
            return;
        }
         //下面是失败的逻辑,ttl=那把锁剩余的超时时间
        RFuture<RedissonLockEntry> future = subscribe(threadId);
        commandExecutor.syncSubscription(future);

        try {
            while (true) {
                ttl = tryAcquire(leaseTime, unit, threadId);//又尝试加锁,相当于刷新了ttl超时时间
                // lock acquired
                if (ttl == null) {
                    break;
                }

                // waiting for message
                if (ttl >= 0) {//如果超时时间大于0,调用getLatch方法,返回一个信号量,然后tryacquire,获取一个许可,阻塞ttl的时间,等ttl时间一到,重新进入while循环
                    getEntry(threadId).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
                } else {
                    getEntry(threadId).getLatch().acquire();
                }
            }
        } finally {
            unsubscribe(future, threadId);
        }
//        get(lockAsync(leaseTime, unit));
    }

信号量的阻塞等待不会占用cpu,所以解释了上图中的 间歇性等待机制

 public Semaphore getLatch() {
        return latch;
    }

有阻塞,必有唤醒机制,不可能让这些线程干巴巴全部阻塞在这,等超时时间
没有抢到锁的线程会去监听一个队列,等待释放锁发布订阅,我们去看解锁逻辑

protected RFuture<Boolean> unlockInnerAsync(long threadId) {
        return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
                "if (redis.call('exists', KEYS[1]) == 0) then " + //判断锁是否还存在
                    "redis.call('publish', KEYS[2], ARGV[1]); " +//不存在就发布消息,
                    "return 1; " +
                "end;" +
                "if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " + //判断这把锁是不是当前线程加的
                    "return nil;" +//不是,就返回nul
                "end; " +
                "local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " + //是,就把key对应的value -1 = 0
                "if (counter > 0) then " +
                    "redis.call('pexpire', KEYS[1], ARGV[2]); " +
                    "return 0; " +
                "else " +
                    "redis.call('del', KEYS[1]); " +//直接解锁
                    "redis.call('publish', KEYS[2], ARGV[1]); " +//发布消息
                    "return 1; "+
                "end; " +
                "return nil;",
                Arrays.<Object>asList(getName(), getChannelName()), LockPubSub.unlockMessage, internalLockLeaseTime, getLockName(threadId));

    }

上面是发布消息,那么阻塞的线程在哪去订阅消息的呢?

在这里插入图片描述
阻塞的线程在订阅的时候,会去监听这个onMessage消息

  @Override
    protected void onMessage(RedissonLockEntry value, Long message) {
        if (message.equals(unlockMessage)) {//这是解锁lua脚本里的0
            value.getLatch().release();//唤醒机制

            while (true) {
                Runnable runnableToExecute = null;
                synchronized (value) {
                    Runnable runnable = value.getListeners().poll();
                    if (runnable != null) {
                        if (value.getLatch().tryAcquire()) {
                            runnableToExecute = runnable;
                        } else {
                            value.addListener(runnable);
                        }
                    }
                }
                
                if (runnableToExecute != null) {
                    runnableToExecute.run();
                } else {
                    return;
                }
            }
        }
    }

至此,redisson源码主体的加锁,解锁,等待锁,唤醒源码分析完毕!

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

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

相关文章

vscode登录租的新服务器

1.connect to…… 选择 connect current window to host 2.configure SSH Host 选择本地配置文件 打开配置文件&#xff0c;把主机名端口号写进去 再返回vscode远程登录页面&#xff0c;左侧栏就会出现这个主机名了。

Hadoop启动后jps发现没有DateNode解决办法

多次使用 Hadoop namenode -format 格式化节点后DateNode丢失 找到hadoop配置文件core-site.xml查找tmp路径 进入该路径&#xff0c;使用rm -rf data删除data文件 再次使用Hadoop namenode -format 格式化后jps后出现DateNode节点

实现springboot的简单使用~

在之前学习SpringSpringMVCMybatis框架时&#xff0c;我们学习了多种配置spring程序的方式&#xff0c;例如&#xff1a;使用XML&#xff0c;注解&#xff0c;Java配置类&#xff0c;或者是将它们结合使用&#xff0c;但配置文件配置起来依然过于复杂&#xff0c;而我们接下来要…

VC++创建windows服务程序

目录 1.关于windows标准可执行程序和服务程序 2.服务相关整理 2.1 VC编写服务 2.2 服务注册 2.3 服务卸载 2.4 启动服务 2.5 关闭服务 2.6 sc命令 2.7 查看服务 3.标准程序 3.1 后台方式运行标准程序 3.2 查找进程 3.3 终止进程 以前经常在Linux下编写服务器程序…

【操作系统】进程同步与进程互斥

&#x1f40c;个人主页&#xff1a; &#x1f40c; 叶落闲庭 &#x1f4a8;我的专栏&#xff1a;&#x1f4a8; c语言 数据结构 javaEE 操作系统 Redis 石可破也&#xff0c;而不可夺坚&#xff1b;丹可磨也&#xff0c;而不可夺赤。 进程同步与进程互斥 一、什么是进程同步二、…

RabbitMQ-主题模式

接上文 RabbitMQ-发布订阅模式和路由模式 1 主题模式 #通配符 代表0个或多个。*通配符 代表 1个或多个 进行测试&#xff0c;修改配置文件 Configuration public class RabbitConfiguration {Bean("topicExchange") //这里使用预置的Topic类型交换机public Exchan…

深度学习(3)---PyTorch中的张量

文章目录 一、张量简介与创建1.1 简介1.2 张量的创建 二、张量的操作2.1 张量的拼接与切分2.2 张量索引 三、张量的数学运算 一、张量简介与创建 1.1 简介 1. 张量是一个多维数组&#xff0c;它是标量、向量、矩阵的高维拓展。 2. 在张量的定义中&#xff0c;方括号用于表示张…

智慧公厕是什么?

随着城市化进程的不断加速&#xff0c;公共厕所作为城市基础设施之一&#xff0c;也在不断进行着前所未有的变革。智慧公厕作为新一代的公共厕所形式&#xff0c;旨在提供更便捷、舒适、卫生的使用体验&#xff0c;不仅仅是个人使用需求的满足&#xff0c;更是城市文明程度和城…

山西电力市场日前价格预测【2023-10-05】

日前价格预测 预测说明&#xff1a; 如上图所示&#xff0c;预测明日&#xff08;2023-10-05&#xff09;山西电力市场全天平均日前电价为363.87元/MWh。其中&#xff0c;最高日前电价为649.89元/MWh&#xff0c;预计出现在18: 45。最低日前电价为291.58元/MWh&#xff0c;预计…

实验三十四、串联型稳压电路参数的选择

一、题目 电路如图1所示。已知输入电压为 50 Hz 50\,\textrm{Hz} 50Hz 的正弦交流电&#xff0c;来源于电源变压器副边&#xff1b;输出电压调节范围为 5 ∼ 20 V 5\sim20\,\textrm V 5∼20V&#xff0c;满载为 0.5 A 0.5\,\textrm A 0.5A&#xff1b; C 3 C_3 C3​ 为消振…

【itext7】使用itext7将多个PDF文件、图片合并成一个PDF文件,图片旋转、图片缩放

这篇文章&#xff0c;主要介绍使用itext7将多个PDF文件、图片合并成一个PDF文件&#xff0c;图片旋转、图片缩放。 目录 一、itext7合并PDF 1.1、引入依赖 1.2、合并PDF介绍 1.3、采用字节数组方式读取PDF文件 1.4、合并多个PDF文件 1.5、合并图片到PDF文件 1.6、旋转图…

LVGL_基础控件label

LVGL_基础控件label 1、创建一个基础对象 /* 创建一个基础对象 label */ lv_obj_t * label lv_label_create(lv_scr_act()); // 创建一个label部件(对象),他的父对象是活动屏幕对象2、设置显示内容 char * text "www.100ask.net"; // 要显示的文字 /* 展示文…

K8S网络原理

文章目录 一、Kubernetes网络模型设计原则IP-per-Pod模型 二、Kubernetes的网络实现容器到容器的通信Pod之间的通信同一个Node内Pod之间的通信不同Node上Pod之间的通信 CNI网络模型CNM模型CNI模型在Kubernetes中使用网络插件 开源的网络组件FlannelFlannel实现图Flannel特点 Op…

视频批量剪辑工具,自定义视频速率,批量剪辑工具助力创意无限”

在视频制作的世界里&#xff0c;每一个细节都至关重要。今天&#xff0c;让我们来探索一项强大且创新的功能——自定义视频速率。利用它&#xff0c;你可以轻松地调整视频播放速度&#xff0c;赋予你的作品独特的个性和风格。 首先第一步&#xff0c;我们要打开好简单批量智剪…

智慧公厕有什么?

智慧公厕作为一种新形态的公共厕所&#xff0c;把智慧化的技术融入到公共厕所的日常使用与管理当中&#xff0c;赋予公共厕所更良好的信息化、数字化、科技化、联网化。 那么&#xff0c;智慧公厕有什么&#xff1f;本文从设施、技术、服务三方面进行快速了解。 首先&#xf…

vue实现轮播图详解

vue实现轮播图详解 目录 vue实现轮播图详解1 引言2 vue实现轮播图2.1 Vant组件引入2.1.1 vant组件引入2.2.2 使用van-swipe组件 2.2 vue代码实现2.2.1 功能需求2.2.2 实现思路2.2.3 代码实现2.2.4 实现效果 3 总结 1 引言 在互联网日渐内卷的情况下&#xff0c;越来越注重用户…

【重拾C语言】四、循环程序设计(后判断条件循环、先判断条件循环、多重循环;典例:计算平均成绩、打印素数、百钱百鸡问题)

目录 前言 四、循环程序设计 4.1 计算平均成绩——循环程序 4.1.1 后判断条件的循环 a. 语法 b. 典例 4.1.2 先判断条件的循环 a. 语法 b. 典例 4.1.3 for语句 a. 语法 b. 典例 4.2 计算全班每人平均成绩—多重循环 4.2.1 打印100以内素数 4.2.2 百钱百…

批量png图片格式转eps格式

问题描述&#xff1a; 在利用Latex排版论文格式时&#xff0c;当插入图片的格式要求为eps格式 &#xff0c;当然也适用于其它文件格式转换 解决方法&#xff1a; 推荐一格好用的免费在线格式转换工具&#xff1a;https://cdkm.com/cn/ 操作步骤&#xff1a; step1:打开网址 ste…

Access注入---Cookie注入

Access注入----Cookie注入Access数据库&#xff08;微软&#xff09; 逐渐淘汰 &#xff08;没有库的概念&#xff0c;是表的集合&#xff09;Access没有系统自带库Cookie注入&#xff08;头注入HEAD注入的&#xff09;php中产生Cookie注入的可能性小&#xff0c;但ASP产生Cook…

CCF CSP认证 历年题目自练Day21

题目一 试题编号&#xff1a; 201909-1 试题名称&#xff1a; 小明种苹果 时间限制&#xff1a; 2.0s 内存限制&#xff1a; 512.0MB 题目分析&#xff08;个人理解&#xff09; 先看输入&#xff0c;第一行输入苹果的棵树n和每一次掉的苹果数m还是先如何存的问题&#xf…