Redis:原理速成+项目实战——Redis实战8(基于Redis的分布式锁及优化)

👨‍🎓作者简介:一位大四、研0学生,正在努力准备大四暑假的实习
🌌上期文章:Redis:原理速成+项目实战——Redis实战7(优惠券秒杀+细节解决超卖、一人一单问题)
📚订阅专栏:Redis:原理速成+项目实战
希望文章对你们有所帮助

上一篇文章已经通过代码的调优,用Redis实现了单个JVM下的秒杀并保证了线程安全问题,但是通过测试发现,在集群分布下,JVM之间依旧会存在线程安全问题,解决这个问题的方法就是分布式锁。

因为是速成,所以这一篇涉及到的底层的原理(Redisson的锁重试和WatchDog机制、Redisson的multiLock原理)只能讲个大概,但是他们的源码真的得太久了。。。把源码的实现做个总结也不太现实,还是需要大家自己去啃。(我从晚上11点啃到凌晨3点。。。)
另外这篇文章的最后一部分测试,我配置了多个Redis结点,自己去配置是很繁琐的,所以我会用Docker来进行配置,有关于Docker的文章可以看这:
一文快速学会Docker软件部署

Redis实现分布式锁

  • 分布式锁
    • 基本原理
    • 不同实现方式对比
  • 基于Redis的分布式锁
  • 实现Redis分布式锁初级
  • Redis分布式锁误删问题
  • 解决Redis分布式锁误删问题
  • 分布式锁的原子性问题
  • Lua脚本
  • Java调用Lua脚本改造分布式锁
  • Redisson
    • Redisson快速入门
    • Redisson的可重入锁原理
    • Redisson的锁重试和WatchDog机制
    • Redisson的multiLock原理

分布式锁

基本原理

JVM内的线程之间可以用锁实现互斥,是因为一个他们的锁只有一个锁监视器,每个JVM都有一个锁监视器,但是多个JVM就会有多个锁监视器,导致发生线程安全问题。
因此,要实现互斥,可以让多个JVM都共用一个锁监视器,这样让JVM与JVM之间、每个JVM的线程之间都共用这个锁,就不会发生线程安全问题了。
由此引出分布式锁的定义:满足分布式系统或集群模式下多进程可见并且互斥的锁。
需要满足的特点:多进程可见、互斥、高可用、高性能、安全性

不同实现方式对比

MySQLRedisZookeeper
互斥本身的互斥锁机制利用互斥命令setnx利用节点的唯一性和有序性实现互斥
高可用
高性能一般一般
安全性断开连接,自动释放锁利用锁超时时间,到时释放临时节点,断开连接自动释放

基于Redis的分布式锁

之前讨论过,我们的方式就是用Redis中的setnx去设置一个锁,而为了解决锁释放前出现以外,我们会给锁增加一个超时释放expire,这样即便出现异常,也不会一直不释放,其他线程也能正常获得锁并执行操作。
获取锁:set lock thread1 NX EX 10(这里的expire就不要单独写一行了,要保持原子性,不然有可能expire还没执行Redis就宕机,照样会造成锁无法释放的情况)
释放锁:del key

需要讨论一下,其他线程获取锁失败以后该怎么办,我们选用非阻塞式的方式,当获取锁失败了以后,不再等待(成功返回true,否则返回false)

容易总结出流程:
在这里插入图片描述

实现Redis分布式锁初级

直接在utils包下创建ILock接口与SimpleRedisLock 类,这个内容和之前的差不多,用stringRedisTemplate完成的流程就那一套:
在这里插入图片描述

public class SimpleRedisLock implements ILock{

    public static final String KEY_PREFIX = "lock:";

    private String name;//不同业务有不同的锁,业务name即为锁的name
    private StringRedisTemplate stringRedisTemplate;

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

    @Override
    public boolean tryLock(long timeoutSec) {
        //获取线程表示
        long threadId = Thread.currentThread().getId();
        //获取锁
        Boolean success = stringRedisTemplate.opsForValue().
                setIfAbsent(KEY_PREFIX + name, threadId + "", timeoutSec, TimeUnit.SECONDS);
        //防止拆箱操作,不能直接返回success
        return Boolean.TRUE.equals(success);
    }

    @Override
    public void unLock() {
        stringRedisTemplate.delete(KEY_PREFIX + name);
    }
}

接着修改我们的下单业务的impl,改变之前的加锁逻辑:

        //创建锁对象,key需要加上用户id,因为不同的用户无所谓,只有同一个用户才要锁起来,因此要指定好用户id
        SimpleRedisLock lock = new SimpleRedisLock("order:" + userId, stringRedisTemplate);
        //获取锁
        boolean isLock = lock.tryLock(1200);
        //判断是否获取锁成功
        if(!isLock){
            //获取锁失败
            return Result.fail("不允许重复下单");
        }
        //获取代理对象
        try {
            IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
            return proxy.createVoucherOrder(voucherId);
        } finally {
            //手动释放锁
            lock.unLock();
        }

在锁那打断点,并利用postman发请求就可以看到锁起到作用了,这都是基本功了。

Redis分布式锁误删问题

上面的锁已经可以解决大多数的情况了,但是遇到一些极端情况还是会出问题:
当一个线程的业务阻塞了,甚至到达了key的TTL,这时候就会被强制释放锁,因此其他的线程就可以成功获取锁并执行自己的业务,而一旦之前被阻塞的业务完成了自己的业务,并且去unLock,这时候就会释放了其它业务的锁,这时候就会导致本来在执行的业务没有了锁,再次引发安全问题。
在这里插入图片描述

这个情况出现的情况相对没有那么大,但是一旦出现就可能会大量出现并发安全问题,因此需要解决问题。
如上图,归根结底,发生大量线程并发问题的原因是线程1误删了线程2的锁,因此我们可以尝试进行一个资格判断,判断线程1此时有没有资格释放锁,这是解决误删问题的一个思路:
在这里插入图片描述
我们需要修改一下业务流程:
在这里插入图片描述

解决Redis分布式锁误删问题

根据上述的分析,我们需要修改一下分布式锁,使得满足:
1、在获取锁时存入线程标识

在这里增加了UUID来作为线程的标识,不再使用线程自己的ID了,这是因为虽然每个JVM的线程都是递增的,每个JVM内部之间的都会维护线程的唯一ID,但是不同的JVM之间还是会产生冲突,因此让JVM自己去维护线程的ID,会导致不同JVM之间的ID冲突。
事实上,也可以用UUID来表示不同的JVM,用线程ID来区分JVM内部的线程,两者拼接在一块。

2、在释放锁时限获取锁中的线程标识,判断是否与当前线程标识一致(一致才可释放)

业务内部,需要增加线程标识的prefix:
在这里插入图片描述
接着修改tryLock与unLock的逻辑,线程的标识变成UUID+线程ID
在这里插入图片描述
这样就可以解决不同JVM之间锁的误删问题,可自行DEBUG。
但这样做依旧不是完美方案。

分布式锁的原子性问题

上述的方式已经可以解决业务阻塞导致的误删操作,但是还会有一些问题:

如果我们阻塞的不是业务,而是业务执行完了,并且判断锁标识成功,即将释放锁的时候发生的阻塞(这种阻塞不是业务阻塞,而可能是JVM内部的垃圾回收机制异常导致阻塞),这时候还会发生新的问题。
如果被阻塞的时间足够长,导致锁的TTL到期了,一旦释放,其他线程又开始乘虚而入,成功获取锁,执行业务。
这时候,被阻塞的线程恢复正常了,但是因为已经进行锁标识的逻辑判断了,这时候被阻塞的线程就可以完成这个释放锁的操作,再次造成误删问题。

可以看下图:
在这里插入图片描述
分析一下问题发生的原因,之所以会出现这种情况,主要原因是锁标识的逻辑判断与锁的释放操作,是两个不同的操作,不满足原子性,所以当在两个操作之间发生了阻塞,那么线程并发问题依旧会出现。
所以,我们必须要保证判断锁标识的动作与释放锁的动作必须得保证原子性。

Lua脚本

想到原子性,我们很容易就想到MySQL中的事务,但是Redis中的事务却不太一样,Redis事务虽然能保障原子性,但是无法保证事务的一致性。Redis事务的操作是一系列的批处理,是在最终的一致性执行的,必须要有乐观锁来做判断,会麻烦很多。
Lua语言能够保证原子性,是因为它在执行原子操作时会将其他线程或进程阻塞,直到该操作完成。
而Redis提供了Lua脚本,在一个脚本中编写多条Redis命令,确保多条命令执行时的原子性。Lua是一种变成语言,基本语法可以参考:
Lua语法教程
重点介绍Lua中Redis提供的调用函数:

redis.call(‘命令名称’, ‘key’, ‘其它参数’, …)

例如,执行set name jack,脚本写法如下:

redis.call(‘set’, ‘name’, ‘jack’)

在我们编写完脚本,使得多条命令的操作满足了原子性,我们还需要用Redis命令来调用脚本:

EVAL script numkeys key… arg…

例如,要执行redis.call(‘set’, ‘name’, ‘jack’)这个脚本:

EVAL “return redis.call(‘set’, ‘name’, ‘jack’)” 0

0表示key类型的参数的个数

脚本中的key、value不要写死,那可以作为参数传递。key类型参数会放入KEYS数组,其它参数会放入ARGV十足,在脚本中可以从KEYS和ARGV数组获取这些参数:

EVAL “return redis.call(‘set’, KEYS[1], ARGV[1])” 1 name Rose
1代表key类型的参数有一个,也就是紧接着的name,会放入KEYS[1]
而Rose则放入ARGV[1]中

Java调用Lua脚本改造分布式锁

在resources下新建Lua文件:

if(redis.call('get', KEYS[1]) == ARGV[1]) then
    -- 释放锁
    return redis.call('del', KEYS[1])
end
return 0

在impl中增加静态变量,防止每次调用unLock函数都要重新调用Lua脚本:

	//DefaultRedisScript是RedisScript的实现类
    public static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
    static {
        UNLOCK_SCRIPT = new DefaultRedisScript<>();
        UNLOCK_SCRIPT.setLocation(new ClassPathResource("unLock.lua"));//设置脚本位置
        UNLOCK_SCRIPT.setResultType(Long.class);//配置返回值
    }

修改unLock函数,调用Lua脚本:

	public void unLock() {
        //调用Lua脚本
        stringRedisTemplate.execute(
                UNLOCK_SCRIPT,
                Collections.singletonList(KEY_PREFIX + name), //转成List类型
                ID_PREFIX + Thread.currentThread().getId());
    }

Redisson

基于setnx的分布式锁存在下面的问题:
1、不可重入:同一个线程无法多次获取同一把锁(当同一个线程内,方法A获取了锁,然后调用方法B,方法B中没办法获取同一把锁,就无法执行)
2、不可重试:获取锁只尝试一次就返回false,没有重试机制
3、超时释放:虽然可以避免死锁,但如果业务耗时很长,也会导致锁释放,会再次发生线程安全问题
4、主从一致性问题:若Redis提供了主从集群,主从同步存在延迟,当主宕机时,如果从并同步主中的锁数据,则会出现锁实现
Redisson是一个在Redis基础上实现的分布式工具集合,提供了很多分布式服务,包含了各种分布式锁的实现。

Redisson快速入门

1、引入依赖:

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

2、配置Redisson客户端:

@Configuration
public class RedissonConfig {

    @Bean
    public RedissonClient redissonClient(){
        //配置
        Config config = new Config();
        //添加Redis地址,这里添加的是单点的地址,也可以使用config.userClusterServer()来添加集群的地址
        config.useSingleServer().setAddress("redis://192.168.177.130:6379").setPassword("123456");
        //创建客户端
        return Redisson.create(config);
    }

}

3、使用Redisson的分布式锁:
redissonClient注入后,只需要将之前的订单impl的锁的定义换成下面的代码就行了

RLock lock = redissonClient.getLock("lock:order:" + userId);

运行代码,做两个测试:
(1)使用postman发送请求,查看下单是否正常:
在这里插入图片描述
(2)jmeter进行多线程测试,测试一人一单功能:
在这里插入图片描述

Redisson的可重入锁原理

我们用如下代码片段就可以解决不可重入问题:

//创建锁对象
RLock lock = redissonClient.getLock("lock");
@Test
void method1() {
	boolean isLock = lock.tryLock();
	if(!isLock){
		log.error("获取锁失败,1");
		return;
	}
	try{
		log.info("获取锁成,1");
		method2();
	} finally {
		log.info("释放锁,1");
		lock.unlock();
	}
}
void method2() {
	boolean isLock = lock.tryLock();
	if(!isLock){
		log.error("获取锁失败,2");
		return;
	}
	try{
		log.info("获取锁成,2");
		method2();
	} finally {
		log.info("释放锁,2");
		lock.unlock();
	}
}

可以发现,如果我们使用之前的加锁与释放锁的方法,我们执行method1方法,获取锁成功以后,method1又去执行了method2方法,这时候因为他们是同一个线程,key就是相同的,就会出现method2无法获得锁,导致method2无法执行,从而造成阻塞。
所以,String类型的结构显然就不行了。我们需要找到一种数据结构,能够在一个key里面获取多个东西——Hash:

Hash结构(hset)的KEY对应的VALUE包含了field与value,因此我们可以让KEY对应锁名称,让field对应线程标识,让value位置记录锁的重入次数(初始为0)。

因此,发生上述情况的时候,虽然线程的标识是相同的,但我们可以将重入次数+1,代表第二次获取锁,这时候整体的VALUE是不相同的。
需要注意的是,method2执行完毕以后不能直接释放这个key对应的锁,因为这样的话会导致method1没有执行完毕就被删掉了,解决的方法是让重入次数-1,只有所有业务都执行完了(重入次数=0)的时候才能真正释放。
这样我们的流程就会发生变化(哈希结构没有直接的EX来设置有效期):
在这里插入图片描述
这样的代码就很长了,我们肯定要用Lua脚本来保证代码的原子性,而Lua代码获取锁与释放锁的逻辑已经是保存到RedissonLock类中了,我们只需要直接调用tyrLock与unlock方法就行。
总结:Redisson的可重入原理的核心就是因为我们使用了hash结构,记录了获取锁的线程以及可重用的次数

Redisson的锁重试和WatchDog机制

这里的底层逻辑非常的复杂,都得自己去啃一遍,啃半天都是很有可能的。
在这里插入图片描述
Redisson分布式锁原理:
1、可重入:利用hash结构记录线程id和重入次数
2、可重试:利用信号量和PubSub功能实现等待、唤醒,获取锁失败的重试机制
3、超时续约:利用watchDog,每隔一段时间(releaseTime/3),重置超时时间

Redisson的multiLock原理

到此,Redisso解决了不可重入、不可重试、超时释放问题,而主从一致性问题还没解决。
也就是当我们的java对Redis集群的主结点进行获取锁的操作之后,主结点要与从结点保持主从同步,而就在主从同步还未完成的时候,主结点宕机了,需要选出一个从结点来替代成为主结点,但因为主从同步没完成,锁失效了,这样就会发生线程并发问题。

既然产生问题的原因是主从一致,那么就可以考虑不再设置主结点,所有结点一视同仁,获取锁的操作同步对所有的结点进行,并且只有所有的结点都获取锁了,才算获取锁成功。这样即便有结点宕机了也不会产生上述的问题。

当然我们也可以对所有的结点都配备从结点,也就是依旧保持主从同步,也就是说这时候的主结点不再只有一个了,那么主结点宕机后,选出这个主结点的其中一个从结点来替代,也不会发生并发安全问题,因为即便有线程对这台Redis乘虚而入了,也没有办法操作,只有在所有结点都获取锁了,才算成功。

这一套方案就叫做连锁,在这边我配置了3台Redis结点,用于后续测试:
在这里插入图片描述
配置很麻烦,但是用Docker就会方便很多,直接在Redis中输入如下命令:

docker pull redis:6.2
docker run -id --name=r1 -p 6380:6379 redis:6.2
docker run -id --name=r2 -p 6381:6379 redis:6.2

创建好以后记得配置Redis是开机自启动的:
Redis:原理速成+项目实战——初识Redis、Redis的安装及启动、Redis客户端

连接的时候要注意端口号分别是6380与6381(我没配置密码,不用填):
在这里插入图片描述
1、先在RedissonConfig中配置好另外2个结点:
在这里插入图片描述
2、把三个独立的锁连接在一起,变成连锁:

@Slf4j
@SpringBootTest
public class RedissonTest {

    @Resource
    private RedissonClient redissonClient;

    @Resource
    private RedissonClient redissonClient2;

    @Resource
    private RedissonClient redissonClient3;

    private RLock lock;

    @BeforeEach
    void setUp(){
        RLock lock1 = redissonClient.getLock("order");
        RLock lock2 = redissonClient2.getLock("order");
        RLock lock3 = redissonClient3.getLock("order");

        //创建连锁
        lock = redissonClient.getMultiLock(lock1, lock2, lock3);
    }

    @Test
    void method1() throws InterruptedException {
        //尝试获取锁
        boolean isLock = lock.tryLock(1L, TimeUnit.SECONDS);
        if(!isLock){
            log.error("获取锁失败,1");
            return;
        }
        try{
            log.info("获取锁成,1");
            method2();
        } finally {
            log.info("释放锁,1");
            lock.unlock();
        }
    }
    void method2() {
        boolean isLock = lock.tryLock();
        if(!isLock){
            log.error("获取锁失败,2");
            return;
        }
        try{
            log.info("获取锁成,2");
            log.info("开始执行业务2");
        } finally {
            log.info("释放锁,2");
            lock.unlock();
        }
    }
}

3、打断点:
在这里插入图片描述
debug运行method1,成功获取锁:
在这里插入图片描述
可以发现三个Redis都有同一把锁,且value为1:
在这里插入图片描述
method2中打断点调试:
在这里插入图片描述

value变为2:
在这里插入图片描述
unlock,value变回1:
在这里插入图片描述

在这里插入图片描述
再unlock,锁被释放(不再演示)

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

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

相关文章

C语言数组基础知识

目录 一维数组&#xff1a; 一维数组的创建&#xff1a; 一维数组的访问&#xff1a; 一维数组在内存中的存储&#xff1a; 二维数组&#xff1a; 二维数组的创建&#xff1a; 二维数组的初始化&#xff1a; 二维数组的使用&#xff1a; 二维数组在内存中的存储&#x…

UE5 将类修改目录

有个需求&#xff0c;需要修改ue里面类的位置&#xff0c;默认在Public类下面&#xff0c;我想创建一个二级目录&#xff0c;将所有的类分好位置&#xff0c;方便查看。 上图为创建一个类所在的默认位置。 接下来&#xff0c;将其移动到一个新的目录中。 首先在资源管理器中找…

kubeSphere DevOps自定义容器环境JDK11

kubeSphere DevOps自定义容器环境JDK11 &#x1f342;前言&#x1f342;增加JDK11容器环境&#x1f341;检查是否成功 &#x1f342;不生效的原因排查&#x1f341;按步骤执行如下命令 &#x1f342;前言 kubeSphere 版本v3.1.1 遇到问题:kubeSphere默认支持容器只有JDK8,目前…

[足式机器人]Part3 机构运动学与动力学分析与建模 Ch00-3(1) 刚体的位形 Configuration of Rigid Body

本文仅供学习使用&#xff0c;总结很多本现有讲述运动学或动力学书籍后的总结&#xff0c;从矢量的角度进行分析&#xff0c;方法比较传统&#xff0c;但更易理解&#xff0c;并且现有的看似抽象方法&#xff0c;两者本质上并无不同。 2024年底本人学位论文发表后方可摘抄 若有…

接口测试管理续集

今天应大家需要&#xff0c;接着谈app端数据返回层面的用例设计方法。第二部分给大家安利一个“接口管理平台”&#xff0c;以帮助大家解决接口文档维护、接口测试数据Mock、接口自动化测试等问题。希望对小伙伴们有用。 言归正传&#xff0c;进入今天的话题。 一、用例设计 …

【大数据OLAP引擎】StartRocks存算分离

存算分离的原因 降低存储成本&#xff1a;同样的存储大小对象存储价格只有SSD的1/10&#xff0c;所以号称存储成本降低80%不是吹的。 存算一体到存算分离 存算一体 作为 MPP 数据库的典型代表&#xff0c;StarRocks 3.0 版本之前使用存算一体 (shared-nothing) 架构&#xf…

Flink任务实战优化

前言&#xff1a;一个好产品&#xff0c;功能应该尽量包装在服务内部&#xff1b;对于Flink而言&#xff0c;无疑是做到了这一点。但是用户在使用Flink的时候&#xff0c;依然可以从版本的选择、代码逻辑、资源参数、业务的数据情况等方面做任务级的定制化优化&#xff1b;用最…

OpenMv颜色识别

本文旨在分享OpenMv实现数字识别并通过串口打印出来的工程源码。如果大家想将识别的结果传给单片机&#xff0c;即OpenMv与单片机之间的通信&#xff0c;可以参考以下文章&#xff1a; OpenMV与STM32之间的通信&#xff08;附源码&#xff09;_openmv与stm32串口-CSDN博客 ​​…

webpack学习笔记

为什么要使用Webpack webpack是一个用于现代JavaScript应用程序的静态模块打包工具。在webpack里一切文件皆模块&#xff0c;通过loader转换文件&#xff0c;通过plugin注入钩子&#xff0c;最后输出由多个模块组合成的文件&#xff0c;webpack专注构建模块化项目。 webPack可以…

HTTPS详解及openssl简单使用

本文介绍https传输协议中涉及的概念&#xff0c;流程&#xff0c;算法&#xff0c;如何实现等相关内容。 HTTP传输过程 HTTP 之所以被 HTTPS 取代&#xff0c;最大的原因就是不安全&#xff0c;至于为什么不安全&#xff0c;看了下面这张图就一目了然了 HTTP 在传输数据的过程…

pulsar的架构与特性记录

一、什么是云原生 云原生的概念是2013年Matt Stine提出的,到目前为止&#xff0c;云原生的概念发生了多次变更&#xff0c;目前最新对云原生定义为: Devps持续交付微服务容器 而符合云原生架构的应用程序是: 采用开源堆栈(K8SDocker)进行容器化&#xff0c;基于微服务架构提高灵…

java.net.ConnectException: Connection refused: connect已解决

&#x1f95a;今日鸡汤&#x1f95a; 要有最朴素的生活和最遥远的梦想&#xff0c;即使明天天寒地冻&#xff0c;山高水远&#xff0c;路远马亡。 —— 《枫》 遇见问题莫着急&#xff0c;着急也没用~&#x1f636;‍&#x1f32b;️ 目录 &#x1f9c2;1.令人发麻的问题 &am…

vue/vue3/js来动态修改我们的界面浏览器上面的文字和图标

前言&#xff1a; 整理vue/vue3项目中修改界面浏览器上面的文字和图标的方法。 效果&#xff1a; vue2/vue3: 默认修改 public/index.html index.html <!DOCTYPE html> <html lang"en"><head><link rel"icon" type"image/sv…

Logstash:迁移数据到 Elasticsearch

在生产环境中&#xff0c;不使用 Apache Kafka 等流平台进行数据迁移并不是一个好的做法。 在这篇文章中&#xff0c;我们将详细探讨 Apache Kafka 和 Logstash 的关系。 但首先让我们简单了解一下 Apache Kafka 的含义。 Apache Kafka 是分布式流平台&#xff0c;擅长实时数据…

Qt QRadioButton单选按钮控件

文章目录 1 属性和方法1.1 文本1.2 选中状态1.3 自动排他1.4 信号和槽 2 实例2.1 布局2.2 代码实现 Qt中的单选按钮类是QRadioButton它是一个可以切换选中&#xff08;checked&#xff09;或未选中&#xff08;unchecked&#xff09;状态的单选按钮单选按钮常用在“多选一”的场…

【Python学习】Python学习12-字典

目录 【Python学习】Python学习12-字典 前言创建语法访问列表中的值修改与新增字典删除字典元素Python字典内置函数&方法参考 文章所属专区 Python学习 前言 本章节主要说明Python的字典&#xff0c;是可变的容器&#xff0c;每个字典由键值对组成用冒号隔开&#xff0c;…

预训练中文GPT2(包括重新训练tokenizer)

训练数据 1.json后缀的文件 2.数据是json line格式&#xff0c;一行一条json 3. json结构如下 {"content": "①北京和上海户籍的游客可获得韩国多次签证&#xff1b;②“整容客”可以不经由韩国使领馆、直接在网上申请签证&#xff1b;③中泰免签的实施日期…

SpringBoot中使用单例模式+ScheduledExecutorService实现异步多线程任务(若依源码学习)

场景 若依前后端分离版手把手教你本地搭建环境并运行项目&#xff1a; 若依前后端分离版手把手教你本地搭建环境并运行项目_本地运行若依前后端分离-CSDN博客 设计模式-单例模式-饿汉式单例模式、懒汉式单例模式、静态内部类在Java中的使用示例&#xff1a; 设计模式-单例模…

Android SDK环境搭建[图解]; 解决问题Done. Nothing was installed.

安装SDK Android SDK环境搭建 依赖java环境,需要自备Java环境 (100%实操成功) 目录 1. 解压&#xff1a;解压到非中文无特殊字符的目录 2. 双击&#xff1a;SDK Manager.exe&#xff0c;不要选全部!不要选全部!不要选全部!(会下很久) 3. 然后勾选组件​ 4. 设置环境变量 …

安装ubuntu22.04系统,GPU驱动,cuda,cudnn,python环境,pycharm

需要准备一个u盘&#xff0c;需要格式化&#xff0c;且内存不小于8g 1 下载ubuntu镜像 下载链接&#xff1a; https://cn.ubuntu.com/download/desktop 2下载rufus Rufus - 轻松创建 USB 启动盘Rufus: Create bootable USB drives the easy wayhttps://rufus.ie/zh/ 准备好这…