Redis --- 分布式锁的使用

我们在上篇博客高并发处理 --- 超卖问题+一人一单解决方案讲述了两种锁解决业务的使用方法,但是这样不能让锁跨JVM也就是跨进程去使用,只能适用在单体项目中如下图:

为了解决这种场景,我们就需要用一个锁监视器对全部集群进行监视:

 这就引出了分布式锁的概念。


什么是分布式锁?


分布式锁是一种在分布式系统中用于控制多个实例(如多个微服务节点)对共享资源的并发访问的机制。分布式锁的核心目标是避免在并发情况下出现数据不一致的问题,比如多个线程同时对同一数据进行操作时,可能会导致数据竞争、脏数据或者业务逻辑错误。

总而言之:分布式锁就是满足分布式系统或者集群模式下多进程可见并且互斥的锁。


为什么要使用分布式锁?


在单机环境中,使用常规的同步机制(如 Java 中的 synchronized)可以避免并发问题,但在分布式系统中,多个服务或应用实例可能同时操作共享的资源。传统的同步方法无法跨机器或进程工作,因此需要引入分布式锁来确保在多台机器中,只有一个节点可以操作共享资源,其他节点必须等待

分布式锁的核心实现多进程之间互斥

分布式锁的实现方式有很多种,主要使用分布式系统中能共享的工具和技术。常见的实现方式包括基于 数据库RedisZookeeper 等的分布式锁。

那么接下来我们就对基于Redis实现分布式锁进行讲解:


Redis 是目前使用最广泛的分布式锁实现方式之一,因为 Redis 提供了高性能的存储和锁机制,适合高并发的场景。 

我们在SimpleRedisLock类去继承ILock接口,以此来实现获取释放锁的过程:

import org.springframework.data.redis.core.StringRedisTemplate;
import java.util.concurrent.TimeUnit;

public class SimpleRedisLock implements ILock{

    private static final String KEY_PREFIX = "lock:";
    private String keyName;
    private StringRedisTemplate stringRedisTemplate;

    public SimpleRedisLock(String keyName, StringRedisTemplate stringRedisTemplate) {
        this.keyName = keyName;
        this.stringRedisTemplate = stringRedisTemplate;
    }

    @Override
    public boolean tryLock(long timeoutSec) {
        // 获取当前线程标识
        long threadId = Thread.currentThread().getId();
        // 获取锁
        Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + keyName, threadId + "", timeoutSec, TimeUnit.SECONDS);
        return Boolean.TRUE.equals(success);
    }

    @Override
    public void unlock() {
        // 释放锁
        stringRedisTemplate.delete(KEY_PREFIX + keyName);
    }
}

 随后我们通过自定义的锁工具类进行使用:

首先new一个工具类对象,通过对象调用获取锁方法,随后判断是否获得锁,之后使用动态代理获得代理对象后,使用代理对象调用事务方法。


业务内容请点击下面地址:高并发处理 --- 超卖问题+一人一单解决方案

@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {

    @Autowired
    private ISeckillVoucherService seckillVoucherService;
    @Autowired
    private RedisIdWorker redisIdWorker;
    @Resource
    private StringRedisTemplate stringRedisTemplate;

    public Result seckillVoucherBySelf(Long voucherId) {
        // 业务逻辑...
        Long userId = UserHolder.getUser().getId();
//        synchronized (userId.toString().intern()){
//            IVoucherOrderService proxy = (IVoucherOrderService)AopContext.currentProxy(); // 获取当前类的代理对象  (需要引入aspectjweaver依赖,并且在实现类加入@EnableAspectJAutoProxy(exposeProxy = true)以此来暴露代理对象)
//            return proxy.createVoucherOrder(voucherId);
//        }
        SimpleRedisLock lock = new SimpleRedisLock("order" + userId, stringRedisTemplate);
        boolean isLock = lock.tryLock(1200);
        // 判断是否获取锁成功
        if (!isLock) {
            // 获取锁失败,返回错误或重试
            return Result.fail("不允许重复下单");
        }
        try {
            IVoucherOrderService proxy = (IVoucherOrderService)AopContext.currentProxy(); // 获取当前类的代理对象  (需要引入aspectjweaver依赖,并且在实现类加入@EnableAspectJAutoProxy(exposeProxy = true)以此来暴露代理对象)
            return proxy.createVoucherOrder(voucherId);
        } finally {
            lock.unlock();
        }
    }
    @Transactional
    public Result createVoucherOrder(Long voucherId) {
        // 业务逻辑...    
    }
}

 但是这样会出现一种情况:虽然我们的线程1还没有结束,但是由于锁设定的时间到期而被释放销毁,此时线程2就能够开始获取锁,过一段时间后线程1结束就会要释放锁,这个时候释放的锁就是线程2刚加上去的锁,所以导致线程安全问题。

解决方案:在获取锁的过程存入线程标识(可用UUID表示),以便于在释放锁去判断这个锁是不是自己的,如果是则释放,如果不是则不释放。(总而言之:设置线程标识的目的是判断释放锁是不是同一个线程获取的,以此来避免类似线程2创建的锁被线程1释放引发的线程安全问题

 

 那么我们就需要再释放锁的方法中加入判断逻辑:

public class SimpleRedisLock implements ILock{

    private static final String KEY_PREFIX = "lock:"; // 锁的前缀
    private static final String ID_PREFIX = UUID.randomUUID().toString() + "-"; // 线程标识的前缀
    private String keyName;
    private StringRedisTemplate stringRedisTemplate;

    public SimpleRedisLock(String keyName, StringRedisTemplate stringRedisTemplate) {
        this.keyName = keyName;
        this.stringRedisTemplate = stringRedisTemplate;
    }

    @Override
    public boolean tryLock(long timeoutSec) {
        // ...
    }

    @Override
    public void unlock() {
        // 获取线程标识
        String threadId = ID_PREFIX + Thread.currentThread().getId();
        // 获取锁中标识
        String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + keyName);
        // 判断标识是否一致
        if(threadId.equals(id)){
            // 如果一致则释放锁
            stringRedisTemplate.delete(KEY_PREFIX + keyName);
        }
    }
}

 但是还是有问题,请看下图:

如果出现一种情况,在线程1事务结束后释放锁,这时按照我们上述逻辑,会判断锁key与线程标识是否一致,随后假如非常凑巧,这个时候发生了线程阻塞并且锁还正好到期而释放,那么这个时候线程2就会获取锁,随后线程1阻塞结束后就会执行释放锁的代码,那么这个时候刚好就会将线程2创建的锁释放掉。 

所以这个时候我们就必须保证判断锁标识与释放锁是原子性的。

这个时候我们就可以使用 lua 脚本编写多条Redis命令,从而确保多条命令执行时的原子性

可以参考下面网站学习:Lua 教程 | 菜鸟教程



什么是Lua脚本?


  LUA 是一种轻量级的脚本语言,具有简单、易于嵌入等特点。在 Redis 中,LUA 脚本用于实现服务器端的逻辑操作,它可以在 Redis 服务器上执行,并且具有 原子性,即 Redis 在执行 LUA 脚本时,会确保脚本中的所有操作都要么全部执行成功,要么全部不执行

  Redis 支持通过 EVAL 命令执行 LUA 脚本,使用这种方式可以实现一些复杂的操作,避免了在客户端执行多个 Redis 命令时可能发生的网络延迟和操作不一致问题。


LUA 脚本如何确保原子性?


在 Redis 中,LUA 脚本是原子性执行的,即在脚本执行过程中,其他 Redis 命令不会被插入。这是因为 Redis 会一次性地加载、解析并执行整个脚本,直到脚本执行完毕,Redis 才会处理其他命令。因此,在一个脚本中,所有的操作都在 Redis 服务器端执行,不会中断,也不会被其他命令打断。

这就意味着在执行 LUA 脚本时,Redis 会确保以下几点:

  1. 脚本中的所有命令要么都执行,要么都不执行:例如,如果一个脚本修改了多个 Redis 键值,并且中途遇到了错误,那么整个脚本的操作将会失败,Redis 会保证在执行过程中没有部分操作成功,部分操作失败的情况。
  2. 不需要担心并发问题:多个客户端同时执行相同的 LUA 脚本时,它们会按顺序依次执行,不会出现竞争条件。
  3. 性能提升:将多个操作通过一个 LUA 脚本提交给 Redis 执行,避免了多次网络请求,提高了性能。

LUA 脚本在 Redis 中的使用方式


Redis 通过 EVALEVALSHA 命令执行 LUA 脚本:

  1. EVAL 命令:直接执行脚本。
  2. EVALSHA 命令:执行已加载的脚本(通过 SCRIPT LOAD 命令加载脚本),使用脚本的 SHA1 校验和来标识脚本。

一个 LUA 脚本的示例:

local current = redis.call('GET', KEYS[1])
if current then
    redis.call('SET', KEYS[1], current + 1)
else
    redis.call('SET', KEYS[1], 1)
end
return redis.call('GET', KEYS[1])

假设我们有一个 Lua 脚本要将一个键的值更新为某个参数:

return redis.call('set', KEYS[1], ARGV[1])

在 Redis Lua 脚本中,KEYS[]ARGV[] 是两个特殊的数组,用来传递键和值。它们的内容由 execute 方法的 keysargs 参数提供。

  1. KEYS[] 数组用于传递 Redis 中的键,通常在 Lua 脚本中用于表示数据库中需要操作的 Redis 键。你可以在 Lua 脚本中通过 KEYS[1]KEYS[2] 等方式来引用这些键。
  2. ARGV[] 数组用于传递非键值的参数。这些参数通常用于脚本中的计算、条件判断等,而不是 Redis 键。例如,ARGV 可以用来传递数字、字符串等作为参数传给 Redis 命令。

 那么我们将java代码提取改造成Lua脚本:

// 获取线程标识
String threadId = ID_PREFIX + Thread.currentThread().getId();
// 获取锁中标识
String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + keyName);
// 判断标识是否一致
if(threadId.equals(id)){
    // 如果一致则释放锁
    stringRedisTemplate.delete(KEY_PREFIX + keyName);
}

那么改造后的Lua脚本:

-- 锁的key
local key = KEYS[1]
-- 当前线程标识
local threadId = ARGV[1]

-- 获取锁中的线程标识  get key
local id = redis.call('get', key)
-- 比较线程标识与锁中的标识是否一致
if(id == threadId) then
    -- 释放锁 del key
    return redis.call('del', key)
end
return 0

我们下载EmmyLua插件,在/resource/unlock.lua文件中将上面代码粘贴进入即可。

随后我们在unlock方法内进行修改,按住Ctrl+H在右面即可查看类型的全部继承类。

为了调用Lua脚本文件,我们可以使用 RedisTemplateexecute 方法,这样可以直接执行 Lua 脚本,并传递相应的参数。RedisTemplateexecute 方法允许你执行自定义的 Redis 命令,包括 Lua 脚本。

在Java底层,execute()方法的源码如下:

public <T> T execute(RedisScript<T> script, List<K> keys, Object... args) {
    return this.scriptExecutor.execute(script, keys, args);
}

1.RedisScript<T> script: RedisScript 是一个用于表示 Redis 脚本(通常是 Lua 脚本)的对象。T 是脚本执行结果的返回类型。你可以将 Lua 脚本作为 RedisScript 的参数传入,执行该脚本后,它会返回一个类型为 T 的结果。

2.List<K> keyskeys 参数是 Lua 脚本的 KEYS[] 数组,对应传入 Redis 的键。这些键是你在 Lua 脚本中使用的 KEYS[] 部分。Redis 脚本允许你使用多个键(最多可以传递 16 个键),这些键是传递给 Lua 脚本的。

3.Object... argsargs 是可变参数,表示 Lua 脚本中的 ARGV[] 数组。在 Lua 脚本中,ARGV[] 用于传递参数,这些参数通常是值(字符串、数字等),而不是 Redis 键。args 可以是任意类型的参数。

public class SimpleRedisLock implements ILock{

    private static final String KEY_PREFIX = "lock:"; // 锁的前缀
    private static final String ID_PREFIX = UUID.randomUUID().toString() + "-"; // 线程标识的前缀
    private String keyName;
    private StringRedisTemplate stringRedisTemplate;

    public SimpleRedisLock(String keyName, StringRedisTemplate stringRedisTemplate) {
        this.keyName = keyName;
        this.stringRedisTemplate = stringRedisTemplate;
    }

    // Lua代码
    private static final DefaultRedisScript<Long> UNLOCK_SCRIPT; // 泛型内填入返回值类型
    static { // 静态属性要使用静态代码块进行初始化
        UNLOCK_SCRIPT = new DefaultRedisScript<>();
        UNLOCK_SCRIPT.setResultType(Long.class);
        UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
    }
    // 调用Lua脚本
    @Override
    public void unlock() {
        // 调用Lua脚本
        stringRedisTemplate.execute(
                UNLOCK_SCRIPT,
                Collections.singletonList(KEY_PREFIX + keyName),
                ID_PREFIX + Thread.currentThread().getId()); 
    }
}

但是基于我们自己创建的锁有以下几个方面的问题:

  1. 不可重入:同一个线程无法多次获取同一把锁。
  2. 不可重试:获取锁只尝试一次就返回false,没有重试机制。
  3. 超时释放:锁超时释放虽然可以避免死锁,但是如果是业务执行耗时较长,也会导致锁的释放,存在安全隐患问题。
  4. 主从一致性:如果Redis提供了主从集群,主从同步存在延迟,当主宕机时,如果从并同步主中的锁数据,则会出现锁实现。

那么这个时候我们就需要使用Redission来实现分布式锁:


下面是官方文档: Redission --- 快速入门


Redisson 是一个基于 Redis 的客户端,它提供了分布式锁的功能,适用于解决分布式系统中资源的竞争问题。我们可以通过 Redisson 来实现分布式锁,确保同一时刻只有一个实例能处理某个资源或任务。

首先引入依赖:

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.32.0</version>
</dependency>

之后配置Redission的客户端:

import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RedissonConfig {

    @Bean
    public RedissonClient redissonClient() {
        // 配置 Redisson 客户端
        Config config = new Config();
        // 添加redis的地址,也可使用config.useClusterServers()添加集群地址
        config.useSingleServer()
              .setAddress("redis://localhost:6379")
              .setPassword("password");  // 如果有密码
        return Redisson.create(config);
    }
}

之后是使用Redission实现的分布式锁的案例

Redisson 提供了 RLock 对象,允许在业务逻辑中加锁和释放锁。

例如,假设有一个需要保证只允许一个实例执行的业务操作,可以这样做:

@Service
public class BusinessService {
    @Autowired
    private RedissonClient redissonClient;
    public void performBusinessLogic() {
        // 获取锁,锁名为 "myLock",设置等待时间和锁的持有时间
        RLock lock = redissonClient.getLock("myLock");
        try {
            // 尝试获取锁,最多等待 10 秒,锁的持有时间为 30 秒
            // tryLock(long waitTime, long leaseTime, TimeUnit unit) 
            if (lock.tryLock(10, 30, TimeUnit.SECONDS)) {
                try {
                    // 执行需要加锁的业务逻辑
                    System.out.println("Executing business logic...");
                    // 例如处理任务,更新数据库等
                } finally {
                    // 释放锁
                    lock.unlock();
                }
            } else {
                System.out.println("Could not acquire the lock, try again later.");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

那么上面代码使用Redission实现的分布式锁为:

@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {

    @Autowired
    private ISeckillVoucherService seckillVoucherService;
    @Autowired
    private RedisIdWorker redisIdWorker;
    @Resource
    private StringRedisTemplate stringRedisTemplate;
    @Resource
    private RedissonClient redissonClient;

    public Result seckillVoucherBySelf(Long voucherId) {
        // ...
        Long userId = UserHolder.getUser().getId();
//        SimpleRedisLock lock = new SimpleRedisLock("order" + userId, stringRedisTemplate);
//        boolean isLock = lock.tryLock(1200);
        RLock lock = redissonClient.getLock("lock:order:" + userId);
        boolean isLock = lock.tryLock();
        // 判断是否获取锁成功
        if (!isLock) {
            // 获取锁失败,返回错误或重试
            return Result.fail("不允许重复下单");
        }
        try {
            IVoucherOrderService proxy = (IVoucherOrderService)AopContext.currentProxy(); // 获取当前类的代理对象  (需要引入aspectjweaver依赖,并且在实现类加入@EnableAspectJAutoProxy(exposeProxy = true)以此来暴露代理对象)
            return proxy.createVoucherOrder(voucherId);
        } finally {
            lock.unlock();
        }
    }
}

下图讲述了Redission实现分布式锁的原理: 

 

那么如何实现可重入锁呢?


我们先了解什么是可重入锁

Redis 的 可重入锁 是指同一个线程可以多次获得锁,而不会导致死锁。Redisson 提供了内置的可重入锁(RLock)功能,能够自动支持锁的可重入性。它的原理是通过记录每个线程对锁的持有次数来实现的,每当一个线程重新获取锁时就会增加持有次数释放锁时会检查持有次数,直到持有次数为 0 才会真正释放锁

我们可以按照下面逻辑进行分析:

当然,我们这里的读写锁的操作同样要保证原子性,那么这个时候就需要写Lua脚本:

(1)判断锁是否存在:①不存在:获取锁,设置有效期   ②存在且是自己的:重入次数+1,设置有效期

local key = KEYS[1]; -- 锁的key
local threadId = ARGV[1]; -- 线程唯一标识
local releaseTime = ARGV[2]; -- 锁的自动释放时间
-- 判断是否存在
if(redis.call('exist',key) == 0) then
    -- 不存在,获取锁
    redis.call('hset',key,threadId,'1');
    -- 设置有效期
    redis.call('expire',key,releaseTime);
end;
-- 锁已经存在,判断threadId是否是自己
if(redis.call('hexists',key,threadId) == 1) then
    -- 不存在,获取锁,重入次数+1
    redis.call('hincrby',key,threadId,1);
    -- 设置有效期
    redis.call('expire',key,releaseTime);
    return 1; -- 返回结果
end;
return 0; -- 代码走到这里,则说明获取锁的不是自己,获取锁失败

 (2)判断锁是否是自己的:

  1.  不是就直接返回 nil
  2.  是自己锁就 -1 并判断重入次数是否为 0:① >0则重置有效期   ② =0则直接删除锁
local key = KEYS[1]; -- 锁的key
local threadId = ARGV[1]; -- 线程唯一标识
local releaseTime = ARGV[2]; -- 锁的自动释放时间
-- 判断当前锁是否还是被自己持有
if(redis.call('hexists',key,threadId) == 0) then
    return nil; -- 如果已经不是自己,则直接返回
end;
-- 是自己的锁,则冲入次数-1
local count = redis.call('hincrby',key,threadId,-1);
-- 判断重入次数是否已经=0
if(count > 0) then
    -- 不为0,说明不能释放锁,则重置有效期然后返回
    redis.call('expire',key,releaseTime);
    return nil;
else -- =0说明可以释放锁,直接删除锁
    redis.call('del',key);
    return nil;
end;

在使用分布式锁时,尤其是可重入锁,务必注意以下几点:

  • 锁的自动释放:设置 releaseTime 时间确保锁在指定时间内被释放,防止线程因为异常或超时没有释放锁而导致死锁。
  • 及时释放锁:在业务执行完后,一定要确保调用 unlock() 方法释放锁。尤其在复杂业务场景下,确保每个分支都有对应的释放逻辑,避免丢锁。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

ML基础——分类模型的评估指标

以鸢尾花的分类分析为例&#xff1a; 首先我们对于3类分类做了个数字编码&#xff0c;012对应3个分类&#xff0c; 所以这就是3个分类下的预测结果指标&#xff1a; 分类为0的准确率、召回率、F1-score&#xff0c;以及support 因为前面我们数据集划分为了105 train&#xff…

安装Office自定义项,安装期间出错

个人博客地址&#xff1a;安装Office自定义项&#xff0c;安装期间出错 | 一张假钞的真实世界 卸载PowerDesigner后&#xff0c;打开“WPS文字”时出现下图错误&#xff1a; 解决方法&#xff1a; 按“WinR”快捷键&#xff0c;打开【运行】框&#xff0c;在对话框中输入“re…

Arouter详解・常见面试题

前言&#xff1a;ARouter是一个用于 Android App 进行组件化改造的路由框架 —— 支持模块间的路由、通信、解耦。 一、路由简介&#xff1a; 路由&#xff1a;就是通过互联的网络把信息从源地址传输到目的地址的活动。完成路由这个操作的实体设备就是 路由器&#xff08;Rout…

能量提升法三:赞美

前情回顾&#xff1a; 《能量提升法二&#xff1a;感恩》 片段&#xff1a;“感恩&#xff0c;就像是在跟世界说&#xff1a;谢谢你&#xff0c;我收到了&#xff0c;我很喜欢&#xff0c;请多来点” 把它归还人海&#xff0c;就当作每一个人&#xff0c;都有可能是曾经帮助…

翼星求生服务器搭建【Icarus Dedicated Server For Linux】

一、前言 本次搭建的服务器为Steam平台一款名为Icarus的沙盒、生存、建造游戏,由于官方只提供了Windows版本服务器导致很多热爱Linux的小伙伴无法释怀,众所周知Linux才是专业服务器的唯一准则。虽然Github上已经有大佬制作了容器版本但是容终究不够完美,毕竟容器无法与原生L…

Java编程语言:辉煌的历史与未来前景

如果将软件开发世界比喻成一个宇宙&#xff0c;Java 无疑是其中最亮的星星之一。它从诞生起就改变了软件开发世界的格局。发展到今天&#xff0c;Java仍然是这个世界上最重要的编程语言之一。当然&#xff0c;它也面临着新的挑战。 Java的诞生 回溯到 1991 年&#xff0c;在 …

为什么噪声不是过拟合的原因?

直观解释&#xff1a;为什么噪声不是过拟合的原因?又什么只要没有过拟合就一定有噪声?_哔哩哔哩_bilibili

【JavaWeb06】Tomcat基础入门:架构理解与基本配置指南

文章目录 &#x1f30d;一. WEB 开发❄️1. 介绍 ❄️2. BS 与 CS 开发介绍 ❄️3. JavaWeb 服务软件 &#x1f30d;二. Tomcat❄️1. Tomcat 下载和安装 ❄️2. Tomcat 启动 ❄️3. Tomcat 启动故障排除 ❄️4. Tomcat 服务中部署 WEB 应用 ❄️5. 浏览器访问 Web 服务过程详…

C语言【基础篇】之流程控制——掌握三大结构的奥秘

流程控制 &#x1f680;前言&#x1f99c;顺序结构&#x1f4af; 定义&#x1f4af;执行规则 &#x1f31f;选择结构&#x1f4af;if语句&#x1f4af;switch语句&#x1f4af;case穿透规则 &#x1f914;循环结构&#x1f4af;for循环&#x1f4af;while循环&#x1f4af;do -…

sunrays-framework配置重构

文章目录 1.common-log4j2-starter1.目录结构2.Log4j2Properties.java 新增两个属性3.Log4j2AutoConfiguration.java 条件注入LogAspect4.ApplicationEnvironmentPreparedListener.java 从Log4j2Properties.java中定义的配置读取信息 2.common-minio-starter1.MinioProperties.…

【Elasticsearch】内置分词器和IK分词器

&#x1f9d1; 博主简介&#xff1a;CSDN博客专家&#xff0c;历代文学网&#xff08;PC端可以访问&#xff1a;https://literature.sinhy.com/#/?__c1000&#xff0c;移动端可微信小程序搜索“历代文学”&#xff09;总架构师&#xff0c;15年工作经验&#xff0c;精通Java编…

简易版RAG实现

之前玩过一次RAG&#xff08;检索增强生成&#xff09;&#xff0c;链接&#xff0c;十分简陋&#xff0c;现在已经无用了&#xff0c;时隔1年半以后又再需要实现一次。其实现在做RAG已经算相对成熟了&#xff0c;要求不高的话甚至可以直接跑langchain-chatchat这类现成的。因为…

指针的介绍2后

1.二级指针 1.1二级指针的介绍 二级指针是指向指针的指针 #define _CRT_SECURE_NO_WARNINGS 1 #include <stdio.h>int main() {int a 100;int* pa &a;int** ppa &pa;printf("a %d\n", a);printf("&a(pa) %p\n", pa);prin…

Android开发基础知识

1 什么是Android&#xff1f; Android&#xff08;读音&#xff1a;英&#xff1a;[ndrɔɪd]&#xff0c;美&#xff1a;[ˈnˌdrɔɪd]&#xff09;&#xff0c;常见的非官方中文名称为安卓&#xff0c;是一个基于Linux内核的开放源代码移动操作系统&#xff0c;由Google成立…

【狂热算法篇】探秘图论之Dijkstra 算法:穿越图的迷宫的最短路径力量(通俗易懂版)

羑悻的小杀马特.-CSDN博客羑悻的小杀马特.擅长C/C题海汇总,AI学习,c的不归之路,等方面的知识,羑悻的小杀马特.关注算法,c,c语言,青少年编程领域.https://blog.csdn.net/2401_82648291?typebbshttps://blog.csdn.net/2401_82648291?typebbs 在本篇文章中&#xff0c;博主将带…

一个基于Python+Appium的手机自动化项目~~

本项目通过PythonAppium实现了抖音手机店铺的自动化询价&#xff0c;可以直接输出excel&#xff0c;并带有详细的LOG输出。 1.excel输出效果: 2. LOG效果: 具体文件内容见GitCode&#xff1a; 项目首页 - douyingoods:一个基于Pythonappium的手机自动化项目&#xff0c;实现了…

【Unity3D】实现Decal贴花效果,模拟战旗游戏地形效果

目录 一、基础版 二、Post Process 辉光Bloom效果 矩形渐隐 涉及知识点&#xff1a;Decal贴花、屏幕后处理Bloom、屏幕空间构建世界空间、ChracterController物体移动、Terrain地形创建 一、基础版 Unity 2019.4.0f1 普通渲染管线&#xff08;非URP、非HDRP&#xff09; UR…

03:Heap代码的分析

Heap代码的分析 1、内存对齐2、Heap_1.c文件代码分析3、Heap_2.c文件代码分析4、Heap_4.c文件代码分析5、Heap_5.c文件代码分析 1、内存对齐 内存对齐的作用是为了CPU更快的读取数据。对齐存储与不对齐存储的情况如下&#xff1a; 计算机读取内存中的数据时是一组一组的读取的…

javascript-es6 (一)

作用域&#xff08;scope&#xff09; 规定了变量能够被访问的“范围”&#xff0c;离开了这个“范围”变量便不能被访问 局部作用域 函数作用域&#xff1a; 在函数内部声明的变量只能在函数内部被访问&#xff0c;外部无法直接访问 function getSum(){ //函数内部是函数作用…

自动驾驶中的多传感器时间同步

目录 前言 1.多传感器时间特点 2.统一时钟源 2.1 时钟源 2.2 PPSGPRMC 2.3 PTP 2.4 全域架构时间同步方案 3.时间戳误差 3.1 硬件同步 3.2 软件同步 3.2.3 其他方式 ① ROS 中的 message_filters 包 ② 双端队列 std::deque 参考&#xff1a; 前言 对多传感器数据…