分布式锁的几种方案对比?你了解多少种呢?

news2024/9/24 3:25:43

目录标题

  • 1.关于分布式锁
  • 2.分布式锁的实现方案
    • 2.1 基于数据库实现
      • 2.1.1乐观锁的实现方式
      • 2.1.2 悲观锁的实现方式
      • 2.1.3 数据库锁的优缺点
    • 2.2 基于Redis实现
      • 2.2.1 基于缓存实现分布式锁
      • 2.2.2缓存实现分布式锁的优缺点
    • 2.3 基于Zookeeper实现
      • 2.3.1 如何实现?
      • 2.3.2 zk实现分布式锁的优缺点
  • 3. 三种方案的对比总结

一个分布式系统无法同时满足三个特性,所以在设计系统之初,就有一个特性要被妥协和牺牲,因为分区容错性的不可或缺性,一般我们的选择是AP或者CP,这就要求我们要么舍弃强一致性,要么舍弃高可用。

为了达到数据的一致性,或者说至少达到数据的最终一致性,我们需要一些额外的方法来保证,比如分布式事务,分布式锁等等。

1.关于分布式锁

在单体系统中,我们经常会遇到很多高并发的场景,比如热点数据、热点缓存,短时间会有大量的请求进行访问,当多个线程同时访问共享资源的时候,就可能产生数据不一致的情况。

为了保证操作的顺序性、原子性,所以我们需要辅助,比如在线程间中加锁,当某个线程得到资源的时候,就对当前的资源进行加锁,等完成操作之后,进行释放,其他线程就可以继续使用了。

Java在多线程实现中,专门提供了一些锁机制来保障线程的互斥同步(synchronized/ReentrantLock)等。

synchronized

 synchronized(object:this){
     // todo 业务逻辑
 }

ReentrantLock

package com.huser.jbc.algorithm.multithreading.twothread;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class AlternatePrinting {
    private final ReentrantLock lock = new ReentrantLock();
    //这是 lock 的条件对象,用于控制线程之间的等待和通知。
    private final Condition condition = lock.newCondition();
    //当前要打印的数字,初始值为 1。
    private int currentNumber = 1;
    //布尔标志变量,表示当前是奇数线程打印(true)还是偶数线程打印(false)。初始值为 true,表示从奇数开始。
    private boolean isOddTurn = true;

    public void printNumbers(int max, boolean isOdd) {
        for (int i = 1; i <= max; i++) {
            lock.lock();
            try {
                while (isOdd != isOddTurn) {
                    condition.await(); // 等待轮到自己打印
                }
                if (isOdd) {
                    System.out.println("Odd: " + currentNumber);
                } else {
                    System.out.println("Even: " + currentNumber);
                }
                currentNumber++;
                isOddTurn = !isOddTurn; // 切换标志
                condition.signal(); // 通知另一个线程
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            } finally {
                lock.unlock();
            }
        }
    }

    public static void main(String[] args) {
        AlternatePrinting printer = new AlternatePrinting();

        Thread oddThread = new Thread(() -> printer.printNumbers(10, true));
        Thread evenThread = new Thread(() -> printer.printNumbers(10, false));

        //oddThread 和 evenThread 同时启动,但只有一个线程可以获取到锁。
        oddThread.start();
        evenThread.start();

        //使用 join 方法等待两个线程完成,确保主线程在子线程结束后才退出。
        try {
            oddThread.join();
            evenThread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

这种方式对于同一个module里面的操作是没什么问题,但是在分布式系统中,就没什么用了,比如很典型的支付场景、跨行转账场景,均属于多系统之间的资源操作。

所以,为了解决这个问题,我们就必须引入分布式锁,来保障多个不同系统对共享资源进行互斥访问。

分布式锁需要解决的问题一般包含如下:

  1. 排他性:分布式部署的应用集群中,同一个方法在同一时间只能被一台机器上的一个线程执行。

  2. 避免死锁:锁在执行一段有限的时间之后,会被释放(正常释放或异常导致自动释放),并且可以被重入,即当前线程可重复获取。

  3. 高可用/高性能:获取锁和释放锁具备高可用;获取和释放锁的性能优良。

2.分布式锁的实现方案

分布式锁的实现,比较常见的方案有3种:

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

2、基于缓存(Redis或其他类型缓存)实现分布式锁

3、基于Zookeeper实现分布式锁

这三种方案,从实现的复杂度上来看,从1到3难度依次递增。而且并不是每种解决方案都是完美的,它们都有各自的特性,还是需要根据实际的场景进行抉择的。

2.1 基于数据库实现

2.1.1乐观锁的实现方式

乐观锁机制其实就是在数据库表中引入一个版本号(version)字段来实现。如下,再表上添加了一个version字段,并且设置为bigint类型:

CREATE TABLE `t_pay` (
`id` BIGINT ( 20 ) NOT NULL AUTO_INCREMENT,
`pay_id` BIGINT (8) NOT NULL COMMENT '支付id',
`pay_count` BIG (8) DEFAULT 0 not NULL COMMENT '支付次数',
`balance` DECIMAL (6,2) DEFAULT 0 not NULL COMMENT '总额度',
`version` BIGINT (10) DEFAULT 0  NOT NULL COMMENT '版本号',
PRIMARY KEY ( `id` )
) ENGINE = INNODB AUTO_INCREMENT = 137587 DEFAULT CHARSET = utf8 COMMENT = '用户支付信息表'; 

在每次进行数据库表之前先查询一下当前记录信息,然后执行更新语句并且让指定字段进行自增,即 version = version+1 (因为MySQL同一张表只支持一个自增键,这边已经被id用了)。

修改完将新的数据与新的version更新到数据表中,更新的同时检查目前数据库里version值是不是之前的那个version,如果是,则正常更新。

如果不是,则更新失败,说明在这个执行间隙有其它的进程去更新过数据了,这时候如果强行更新进去,支付次数和总额度就不对了。操作如下:

-- 先查询数据信息
select pay_id,pay_count,balance,version from t_pay where id= #{id}
-- 判断当前表中的version 是否与刚才查出的version一致,是的话正常更新
update t_pay set pay_count=paycount + 1, balance = balance + '具体消费额度' ,version = version+1 where id=#{id} and version= #{version};

根据返回修改记录条数来判断当前更新是否生效,如果改动的是0条数据,说明version发生了变更,导致改动无效,这时候可以根据自己业务逻辑来判断是否回滚事务。

下面图例分析一下:
在这里插入图片描述

举例如图,你跟你老婆用同一个账户在支付,你支付燃气费,你老婆够买手表,如果没有锁机制,在并发的情况下,可能会出现同时被扣25和8000,导致最终余额的不正确。

但是如果使用乐观锁机制,当两个请求同时到达的时候,需要获取到账号信息包括版本号信息,不管是A操作(支付燃气费)还是B操作(购买手表),都会将版本号加1,即version=2,

那么另外一个操作执行的时候,发现当前版本号变成了2,不再是之前读取的 1,则更新失败。

通过上面这个例子可以看出来,使用「乐观锁」机制,必须得满足:

a)锁服务要有递增的版本号 version

b)每次更新数据的时候都必须先判断版本号对不对,然后再写入新的版本号

2.1.2 悲观锁的实现方式

悲观锁也叫作排它锁,在MySQL中是基于 for update 语法来实现加锁的,下面用伪代码来演示,例如:

 // 锁定的方法
 public boolean lock(){
     connection.setAutoCommit(false)
     while(true){
         result = 
         select * from t_pay where 
         id = 100 for update;
         if(result){
          // 结果不为空,
          // 则说明获取到了锁
             return true;
         }
         // 没有获取到锁,继续获取
         sleep(1000);
     }
     return false;
 }
 
 // 释放锁
 connection.commit(); 

上面的示例中,user表中,id是主键,通过 for update 操作,数据库在查询的时候就会给这条记录加上排它锁。(需要注意的是,在InnoDB中只有检索字段加了索引的,才会是行级锁,否者是表级锁,所以这个id字段要加索引),

当这条记录加上排它锁之后,其它线程是无法操作这条记录的。

那么,这样的话,我们就可以认为获得了排它锁的这个线程是拥有了分布式锁,然后就可以执行我们想要做的业务逻辑,当逻辑完成之后,再调用上述释放锁的语句即可。

2.1.3 数据库锁的优缺点

直接使用数据库,容易理解、操作简单。

但是会有各种各样的问题,在解决问题的过程中会使整个方案变得越来越复杂。操作数据库需要一定的开销,性能问题需要考虑,特别是高并发场景下。

使用数据库的行级锁并不一定靠谱,尤其是当我们的锁表并不大的时候。

2.2 基于Redis实现

2.2.1 基于缓存实现分布式锁

相比较于基于数据库实现分布式锁的方案来说,基于缓存来实现在性能方面会表现的更好一点。类似Redis可以多集群部署的,解决单点问题。

基于Redis实现的锁机制,主要是依赖redis自身的原子操作,例如:

# 判断是否存在,不存在设值,并提供自动过期时间
SET key value NX PX millisecond

# 删除某个key
DEL key [key …] 

NX:只在在键不存在时,才对键进行设置操作,SET key value NX 效果等同于 SETNX key value
PX millisecond:设置键的过期时间为millisecond毫秒,当超过这个时间后,设置的键会自动失效

如果需要把上面的支付业务实现,则需要改写如下:

# 设置账户Id为17124的账号的值为1,如果不存在的情况下,并设置过期时间为500ms
SET pay_id_17124 1 NX PX 500

# 进行删除
DEL pay_id_17124 

上述代码示例是指,

当redis中不存在pay_key这个键的时候,才会去设置一个pay_key键,键的值为 1,且这个键的存活时间为500ms。

当某个进程设置成功之后,就可以去执行业务逻辑了,等业务逻辑执行完毕之后,再去进行解锁。而解锁之前或者自动过期之前,其他进程是进不来的。

实现锁机制的原理是:这个命令是只有在某个key不存在的时候,才会执行成功。那么当多个进程同时并发的去设置同一个key的时候,就永远只会有一个进程成功。

解锁很简单,只需要删除这个key就可以了。

另外,针对redis集群模式的分布式锁,可以采用redis的Redlock机制。

需要注意的是,如何设置恰当的超时时间,如果设置的失效时间太短,方法没等执行完,锁就自动释放了,那么就会产生并发问题。如果设置的时间太长,其他获取锁的线程就要多等一段时间。这个问题使用数据库实现分布式锁同样存在。

总结:可以使用缓存来代替数据库来实现分布式锁,会提供更好的性能,同时,很多缓存服务都是集群部署的,可以避免单点问题。

并且很多缓存服务都提供了可以用来实现分布式锁的方法,比如redis的setnx方法。并且,缓存服务也都提供了对数据的过期自动删除的支持,可以直接设置超时时间来控制锁的释放。

2.2.2缓存实现分布式锁的优缺点

优点是性能好,实现起来较为方便。缺点是通过超时时间来控制锁的失效时间并不是十分的靠谱。

2.3 基于Zookeeper实现

2.3.1 如何实现?

基于zookeeper临时有序节点可以实现分布式锁。

其原理如下:

1、每个请求的客户端,都去Zookeeper上的某个指定节点的目录下(比如是对某个对象的操作),去生成一个唯一的临时有序节点。

2、然后判断自己是否是这些有序节点中序号最小的一个,如果是,则算是获取了锁。

3、如果不是最小序号,则说明没有获取到锁,那么就需要在序列中找到比自己小的那个节点,对其注册事件监听(调用exits()方法确认节点在不在)。比如下面图中,client-3 生成 node-3,并监听node-2。

4、当监听到这个节点被删除了,那就再去判断一次自己当初创建的节点是否变成了序列中最小的。如果是,则获取锁,如果不是,则重复上述步骤。

import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.recipes.locks.InterProcessMutex;
import org.apache.curator.retry.ExponentialBackoffRetry;

public class DistributedLockWithTemporarySequentialNodes {

    private static final String ZK_ADDRESS = "localhost:2181";
    private static final String LOCK_PATH = "/distributed_lock";

    public static void main(String[] args) {
        // 创建 CuratorFramework 实例
        CuratorFramework client = CuratorFrameworkFactory.builder()
                .connectString(ZK_ADDRESS)
                .retryPolicy(new ExponentialBackoffRetry(1000, 3))
                .build();
        client.start();

        try {
            // 确保根路径存在
            if (client.checkExists().forPath(LOCK_PATH) == null) {
                client.create().creatingParentsIfNeeded().forPath(LOCK_PATH);
            }

            // 创建临时顺序节点
            String lockNodePath = client.create().creatingParentsIfNeeded()
                    .withMode(CreateMode.EPHEMERAL_SEQUENTIAL)
                    .forPath(LOCK_PATH + "/lock_");

            // 获取当前节点的序列号
            long currentSeqNum = extractSequenceNumber(lockNodePath);

            // 获取所有子节点
            List<String> children = client.getChildren().forPath(LOCK_PATH);

            // 对子节点进行排序
            Collections.sort(children, (a, b) -> Long.compare(extractSequenceNumber(a), extractSequenceNumber(b)));

            // 找到当前节点的位置
            int index = children.indexOf(lockNodePath.substring(LOCK_PATH.length() + 1));

            // 如果当前节点是最小的,则获取锁
            if (index == 0) {
                System.out.println("Thread " + Thread.currentThread().getName() + " acquired the lock.");
                // 模拟业务逻辑
                Thread.sleep(5000); // 假设这里执行一些耗时操作
                System.out.println("Thread " + Thread.currentThread().getName() + " released the lock.");
            } else {
                // 否则,等待前一个节点被删除
                String prevNodePath = LOCK_PATH + "/" + children.get(index - 1);
                client.getData().usingWatcher((watchedEvent) -> {
                    if (watchedEvent.getType() == Watcher.Event.EventType.NodeDeleted) {
                        synchronized (DistributedLockWithTemporarySequentialNodes.class) {
                            notifyAll(); // 通知等待线程
                        }
                    }
                }).forPath(prevNodePath);

                // 等待前一个节点被删除
                synchronized (DistributedLockWithTemporarySequentialNodes.class) {
                    while (client.checkExists().forPath(prevNodePath) != null) {
                        wait();
                    }
                }

                // 重新检查是否获得锁
                if (checkAndAcquireLock(client, lockNodePath)) {
                    System.out.println("Thread " + Thread.currentThread().getName() + " acquired the lock.");
                    // 模拟业务逻辑
                    Thread.sleep(5000); // 假设这里执行一些耗时操作
                    System.out.println("Thread " + Thread.currentThread().getName() + " released the lock.");
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 关闭客户端
            client.close();
        }
    }

    private static long extractSequenceNumber(String path) {
        return Long.parseLong(path.substring(path.lastIndexOf('_') + 1));
    }

    private static boolean checkAndAcquireLock(CuratorFramework client, String lockNodePath) throws Exception {
        // 重新获取所有子节点
        List<String> children = client.getChildren().forPath(LOCK_PATH);
        Collections.sort(children, (a, b) -> Long.compare(extractSequenceNumber(a), extractSequenceNumber(b)));
        int index = children.indexOf(lockNodePath.substring(LOCK_PATH.length() + 1));
        return index == 0;
    }
}

根据上诉的步骤,Zookeeper实际解决了如下问题:

锁无法释放?使用Zookeeper可以有效的解决锁无法释放的问题,因为在创建锁的时候,客户端会在ZK中创建一个临时节点,一旦客户端获取到锁之后突然挂掉(Session连接断开),那么这个临时节点就会自动删除掉。其他客户端就可以再次获得锁。

非阻塞锁?使用Zookeeper可以实现阻塞的锁,客户端可以通过在ZK中创建顺序节点,并且在节点上绑定监听器,一旦节点有变化,Zookeeper会通知客户端,客户端可以检查自己创建的节点是不是当前所有节点中序号最小的,如果是,那么自己就获取到锁,便可以执行业务逻辑了。

不可重入?使用Zookeeper也可以有效的解决不可重入的问题,客户端在创建节点的时候,把当前客户端的主机信息和线程信息直接写入到节点中,下次想要获取锁的时候和当前最小的节点中的数据比对一下就可以了。如果和自己的信息一样,那么自己直接获取到锁,如果不一样就再创建一个临时的顺序节点,参与排队。

单点问题?使用Zookeeper可以有效的解决单点问题,ZK是集群部署的,只要集群中有半数以上的机器存活,就可以对外提供服务。

下面图例说明:
在这里插入图片描述

Locker Object 是对需要竞争的资源进行持久的节点,下面的node-1到node-n 就是上面说的有序子节点,由不同进程的client去创建。

当进来一个客户端需要去竞争资源的时候,就跑到持久化节点下去按顺序创建一个直接点,然后看一下是不是最小的一个。

如果是最小的就获取到锁,可以继续后面的资源操作了。如果不是则监听比自己序号小的节点,比如client-3 订阅的是 node-2。

如果node-2被删除,自己被唤醒,再次判断自己是不是序列中最小的,如果是,则获取锁。

2.3.2 zk实现分布式锁的优缺点

  • 优点:有效的解决单点问题,不可重入问题,非阻塞问题以及锁无法释放的问题。实现起来较为简单。

  • 缺点:性能上不如使用缓存实现分布式锁。 需要对ZK的原理有所了解。

3. 三种方案的对比总结

上面几种方式,并不是都能做到十全十美,就像CAP一样,在复杂性、可靠性、性能 三方面无法同时满足一样。所以,更多的是根据不同的应用场景选择最合适的方案。

下面是 Zookeeper、Redis 以及数据库三种实现分布式锁的对比表格。这个表格将从多个维度来比较这三种方式,包括一致性、性能、可靠性、复杂性等。

特性ZookeeperRedis数据库 (如 MySQL)
一致性强一致性,提供顺序和临时节点特性最终一致性(取决于客户端实现)最终一致性(取决于客户端实现)
性能性能较低,因为涉及网络通信和多节点协调高性能,单线程处理命令,适合高并发场景性能中等,受限于数据库的读写能力
可靠性高可靠性,通过多副本机制保证数据不丢失高可靠性,支持主从复制和哨兵模式可靠性取决于数据库配置,支持主从复制
复杂性实现较复杂,需要处理会话超时、节点监控等问题实现相对简单,但需要注意客户端重入和过期时间问题实现相对简单,但需要处理事务和死锁问题
自动释放支持,当客户端会话结束时自动删除临时节点支持,通过设置过期时间不支持,需要手动释放或使用事务
可重入性支持,通过检查当前节点是否为最小节点支持,通过客户端记录锁持有者信息支持,通过客户端记录锁持有者信息
公平性公平锁,基于节点顺序非公平锁,取决于客户端实现非公平锁,取决于客户端实现
适用场景适用于需要强一致性和高可靠性的场景适用于高性能、低延迟的场景适用于对性能要求不高且已有数据库基础设施的场景
扩展性良好的扩展性,可以通过增加节点来提高可用性良好的扩展性,可以通过分片和集群来提高性能扩展性较差,受限于单个数据库实例的性能
客户端库Curator, Zookeeper 官方库Jedis, Lettuce, RedissonJDBC, MyBatis, Hibernate 等
运维成本较高,需要维护多节点集群中等,需要维护主从复制和哨兵模式较低,依赖于现有的数据库运维
社区支持成熟,有广泛的社区支持成熟,有广泛的社区支持成熟,有广泛的社区支持

解释

  1. 一致性

    • Zookeeper:提供强一致性,通过顺序和临时节点特性确保所有节点看到的数据是一致的。
    • Redis:默认是最终一致性,但在某些客户端实现中可以达到接近强一致性。
    • 数据库:默认是最终一致性,依赖于客户端实现和事务机制。
  2. 性能

    • Zookeeper:性能较低,因为它涉及到网络通信和多节点协调。
    • Redis:高性能,单线程处理命令,适合高并发场景。
    • 数据库:性能中等,受限于数据库的读写能力和事务处理能力。
  3. 可靠性

    • Zookeeper:高可靠性,通过多副本机制保证数据不丢失。
    • Redis:高可靠性,支持主从复制和哨兵模式。
    • 数据库:可靠性取决于数据库配置,支持主从复制。
  4. 复杂性

    • Zookeeper:实现较复杂,需要处理会话超时、节点监控等问题。
    • Redis:实现相对简单,但需要注意客户端重入和过期时间问题。
    • 数据库:实现相对简单,但需要处理事务和死锁问题。
  5. 自动释放

    • Zookeeper:支持,当客户端会话结束时自动删除临时节点。
    • Redis:支持,通过设置过期时间。
    • 数据库:不支持,需要手动释放或使用事务。
  6. 可重入性

    • Zookeeper:支持,通过检查当前节点是否为最小节点。
    • Redis:支持,通过客户端记录锁持有者信息。
    • 数据库:支持,通过客户端记录锁持有者信息。
  7. 公平性

    • Zookeeper:公平锁,基于节点顺序。
    • Redis:非公平锁,取决于客户端实现。
    • 数据库:非公平锁,取决于客户端实现。
  8. 适用场景

    • Zookeeper:适用于需要强一致性和高可靠性的场景。
    • Redis:适用于高性能、低延迟的场景。
    • 数据库:适用于对性能要求不高且已有数据库基础设施的场景。
  9. 扩展性

    • Zookeeper:良好的扩展性,可以通过增加节点来提高可用性。
    • Redis:良好的扩展性,可以通过分片和集群来提高性能。
    • 数据库:扩展性较差,受限于单个数据库实例的性能。
  10. 客户端库

    • Zookeeper:Curator, Zookeeper 官方库。
    • Redis:Jedis, Lettuce, Redisson。
    • 数据库:JDBC, MyBatis, Hibernate 等。
  11. 运维成本

    • Zookeeper:较高,需要维护多节点集群。
    • Redis:中等,需要维护主从复制和哨兵模式。
    • 数据库:较低,依赖于现有的数据库运维。
  12. 社区支持

    • Zookeeper:成熟,有广泛的社区支持。
    • Redis:成熟,有广泛的社区支持。
    • 数据库:成熟,有广泛的社区支持。

根据这些对比,你可以根据具体需求选择最适合的分布式锁实现方式。例如,如果你需要强一致性和高可靠性,可以选择 Zookeeper;如果需要高性能和低延迟,可以选择 Redis;如果已有数据库基础设施且对性能要求不高,可以选择数据库。

在这里插入图片描述

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

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

相关文章

生信技能59 - 基于GATK CallingSNP变异检测及注释流程

1. 流程说明 使用BWA MEM比对,如果文件较大,可使用bwa-mem2进行比对,速度会有很大提升;使用GATK对BAM进行排序和标记重复,再使用GATK HaplotypeCaller + GATK GenotypeGVCFs进行变异检测,生产.g.vcf文件,提取SNP并使用annovar进行位点注释。 使用bwa-mem2进行比对,获…

python全栈开发《37.列表(元组)的count函数》

元组的count函数的功能和用法与列表的count函数完全一致。 1.count的功能 返回当前列表中某个成员的个数。 2.count的用法 fruits [苹果,西瓜,水蜜桃,西瓜,雪梨] count fruits.count(西瓜) print(count) 运行结果&#xff1a; 2 注意&#xff1a;列表的内置函数count拿到需要…

Qt系统相关——QThread

文章目录 QThread的API使用示例客户端多线程应用场景互斥锁QMutexQMutexLockerQReadWriteLocker、QReadLocker、QWriteLocker 条件变量和信号量 QThread的API Qt中的多线程和Linux中的线程&#xff0c;本质上是一个东西 Linux线程概念 Linux多线程——线程控制 Linux多线程——…

EEPROM手册阅读笔记

目录 一、特征描述二、功能描述三、总线特性四、设备寻址五、写入操作1.字节写入2.页写入 六、读取操作1.当前地址读取2.随机读取3.顺序读取 一、特征描述 1.Microchip Technology Inc. 24AA04/24LC04B &#xff08;24XX04*&#xff09; 是一款 4 Kbit 电气可擦除 PROM。该器件…

【4】AT32F437 OpenHarmony轻量系统移植教程(1)

开源地址&#xff1a;https://gitee.com/AT32437_OpenHarmony 1.学习本文档的意义 1.学习移植OpenHarmony轻量系统到AT32全系列mcu上&#xff0c;本文档移植的具体型号为AT32F437ZMT7 2.学习OpenHarmony轻量系统开发 2.移植前的准备工作 1.移植之前必须要先熟悉AT-START-F…

HTTP协议:发展、请求响应、状态码 等

文章目录 HTTP发展历程HTTP请求URL和URIHTTP协议版本HTTP请求方法GET 和 POST 区别HTTP状态码HTTP 请求与响应报文HTTP 请求流程 HTTP 超文本传输协议&#xff08;Hypertext Transfer Protocol&#xff0c;HTTP&#xff09;是一个简单的请求-响应协议&#xff0c;它通常运行在…

SAP-ABAP消息号 AD857

激活表时报错 返回表&#xff0c;找到报错的字段 维护参考表和字段

免费!大厂太卷了,又一款AI对口型神器,让照片开口说话唱歌,吊打阿里EMO?(附保姆级教程)

大家好&#xff0c;我是程序员X小鹿&#xff0c;前互联网大厂程序员&#xff0c;自由职业2年&#xff0c;也一名 AIGC 爱好者&#xff0c;持续分享更多前沿的「AI 工具」和「AI副业玩法」&#xff0c;欢迎一起交流~&#xff08;AI资料点文末卡片自取&#xff09; 之前分享了一款…

【小白向】怎么去除视频水印?HitPaw帮你轻松解决

序言 HitPaw是一款优秀的去除视频水印的工具。 特点&#xff1a;不仅仅能够去除图片、视频里的固定水印&#xff0c;还能去除移动水印。 尤其是它的AI去水印功能&#xff0c;效果非常好。 极简使用教程 下载安装 HitPaw需要在电脑上安装软件才能使用。 支持Windows系统和…

【AIGC】ChatGPT RAG提取文档内容,高效制作PPT、论文

目录 一、理解 RAG 技术 二、利用 ChatGPT 的 RAG 技术提取文档内容 三、高效制作 PPT 四、高效撰写论文 五、最佳实践与建议 六、工具推荐 随着人工智能生成内容&#xff08;AIGC&#xff09;的快速发展&#xff0c;利用先进的技术工具如 ChatGPT 的 RAG&#xff08;Ret…

kismet和war driving具体准备(仅供无线安全学习)

war driving准备 一台笔记本 一个最好是双频的网卡&#xff0c;单频搜集信号少 我自己买的是http://e.tb.cn/h.grI4EmkDLOqQXHG?tkKZ5g3RVeH6f 如果经济条件允许可以去买大功率天线&#xff08;我买的车载的 大概40db这样子 范围广&#xff09; http://e.tb.cn/h.grCM0CQ6L…

YOLOv5-水印检测

简介&#xff1a; YOLOv5在YOLOv4算法的基础上做了进一步的改进&#xff0c;检测性能得到进一步的提升。虽然YOLOv5算法并没有与YOLOv4算法进行性能比较与分析&#xff0c;但是YOLOv5在COCO数据集上面的测试效果还是挺不错的。 YOLOv5是一种单阶段目标检测算法&#xff0c;该算…

教材改版难道是假的?!24下半年软考怎么还是用旧版?何时启用新教材?

之前一直有消息说软考有几个科目的教材要改版&#xff0c;导致不少打算报名2024年下半年软考的同学都在担心是否会影响考试…… 但一直到现在都没看到有新教材出版的消息&#xff0c;所以很多人怀疑之前教材改版的消息是不是假的&#xff0c;要不然怎么还在用旧版教材…… 那…

C++11新特性和扩展(1)

个人主页&#xff1a;C忠实粉丝 欢迎 点赞&#x1f44d; 收藏✨ 留言✉ 加关注&#x1f493;本文由 C忠实粉丝 原创 C11新特性和扩展 收录于专栏 [C进阶学习] 本专栏旨在分享学习C的一点学习笔记&#xff0c;欢迎大家在评论区交流讨论&#x1f48c; 目录 1.C11简介 2. 列表初始…

Oracle 数据库安装和配置指南

目录 1. 什么是Oracle数据库&#xff1f; 2. 安装前的准备工作 2.1 硬件要求 2.2 软件要求 2.3 下载Oracle安装包 3. Oracle数据库的安装步骤 3.1 Windows系统安装步骤 3.2 Linux系统安装步骤 4. 配置Oracle数据库 4.1 设置环境变量&#xff08;Linux&#xff09; 4.…

828华为云征文 | 云服务器Flexus X实例,Docker集成搭建Halo博客平台

828华为云征文 | 云服务器Flexus X实例&#xff0c;Docker集成搭建Halo博客平台 Halo博客平台是一款基于Java的开源博客系统&#xff0c;以其简单易用、功能强大、美观大方等特点而受到广泛欢迎&#xff0c;采用了多种先进的技术框架&#xff0c;包括Freemarker模板引擎、Vue.j…

项目总结,路径匹配的业务逻辑

redisHelper.addzset(HitchConstants.STOKE_GEO_ZSET_PREFIX,hitchGeoBo.getTargetID()&#xff08;乘客ID&#xff09;,stroke.getId()->(司机的ID&#xff09;,getscore(hitchGeoBo); 如果他不这样乘客这里存储司机的ID,我们会发现假如再来一个司机&#xff0c;他是无法获…

通过 LabVIEW 正则表达式读取数值(整数或小数)

在LabVIEW开发中&#xff0c;字符串处理是一个非常常见的需求&#xff0c;尤其是在处理包含复杂格式的数字时。本文通过一个具体的例子来说明如何利用 Match Regular Expression Function 和 Match Pattern Function 读取并解析字符串中的数字&#xff0c;并重点探讨这两个函数…

书生·浦语作业集合

目录&#xff1a; 1. Linux基础知识 2.python基础知识 3.Git基础知识 4.书生大模型全链路开源体系 1.1-Linux基础知识 配置环境后&#xff0c;运行 hello_word.py 在本地终端中进行端口映射 映射成功后&#xff0c;访问 127.0.0.1&#xff1a;7860 1.2-python基础知识 任务…

【Python】生成dataframe的测试样例,用于测试一个或者多个dataframe

我们在处理dataframe测试时&#xff0c;发现&#xff0c;总需要重新构造一个新的dataframe&#xff0c;每次想找个现成的就想抓狂。 所以&#xff0c;为了方便随用随拿&#xff0c;我在这里直接保存一个直接生成dataframe 的方法。 1. 生成一个随机dataframe的方法&#xff1…