Go语言实现Redis分布式锁2

项目地址: https://github.com/liwook/Redislock

1.支持阻塞式等待获取锁

之前的是只尝试获取一次锁,要是获取失败就不再尝试了。现在修改为支持阻塞式等待获取锁。

添加LockOptions结构体

添加option.go文件。

在LockOptions中

  • isBlock表示是否是阻塞模式
  • blockWaitingTime是获取key的阻塞超时时间
  • expire表示key的过期时间(之前是在结构体RedisLock,现在保存在LockOptions中)

const (
	DefaultExpireTime = 20 * time.Second
	DefaultBlockWaitingTime = 8 * time.Second
)

type LockOptions struct {
	isBlock          bool
	blockWaitingTime time.Duration
	expire           time.Duration
}

 下面是LockOption的设置方法。

//option.go
type LockOptionFunc func(*LockOptions)

// 设置阻塞等待
func WithBlock() LockOptionFunc {
	return func(option *LockOptions) {
		option.isBlock = true
	}
}

//设置阻塞等待时间的上限
func WithBlockWaiting(waiting time.Duration) LockOptionFunc {
	return func(option *LockOptions) {
		option.blockWaitingTime = waiting
	}
}

//设置续期的时长,也是key过期的时长
func WithExpire(exprie time.Duration) LockOptionFunc {
	return func(option *LockOptions) {
		option.expire = exprie
	}
}

func setLock(o *LockOptions) {
	if o.isBlock && o.blockWaitingTime <= 0 {
		//默认阻塞等待时间上限是8
		o.blockWaitingTime = 8
	}

	if o.expire == 0 {
		o.expire = DefaultExpireTime
	}
}

修改创建锁的代码

//该结构体添加了LockOptions,去掉了expire成员
type RedisLock struct {
	LockOptions
	key      string
	Id       string //锁的标识
	redisCli *redis.Client
}

func NewRedisLock(cli *redis.Client, key string, opts ...LockOptionFunc) *RedisLock {
	id := strings.Join(strings.Split(uuid.New().String(), "-"), "")
	lock := RedisLock{
		key:      key,
		Id:       id,
		redisCli: cli,
	}
	//执行一些配置操作
	for _, optFunc := range opts {
		optFunc(&lock.LockOptions)
	}

	setLock(&lock.LockOptions)
	return &lock
}

//用法
	lock := redislock.NewRedisLock(client, key, redislock.WithBlock(), redislock.WithBlockWaiting(10*time.Second))


//之前的写法
// func NewRedisLock(cli *redis.Client, key string) *RedisLock {
// 	id := strings.Join(strings.Split(uuid.New().String(), "-"), "")
// 	return &RedisLock{
// 		key:      key,
// 		expire:   defaultExpireTime,
// 		Id:       id,
// 		redisCli: cli,
// 	}
// }

加锁

加锁主要分成了3步:

  1. 不管是不是阻塞的,都先尝试获取一次锁tryLock()
  2. 非阻塞加锁失败的话,就直接返回错误
  3. 之后基于阻塞模式轮询去获取锁
func (lock *RedisLock) Lock() (bool, error) {
	//不管是否是阻塞的,都是要先获取一次锁
	success, err := lock.tryLock()
	if success && err == nil {
		return success, err
	}

	//非阻塞加锁失败的话,直接返回错误
	if !lock.isBlock {
		return false, err
	}

	//基于阻塞模式轮询去获取锁
	return lock.blockingLock()
}

func (lock *RedisLock) tryLock() (bool, error) {
	return lock.redisCli.SetNX(lock.key, lock.Id, lock.expire).Result()
}

  阻塞模式中使用了定时器轮询去获取锁。

func (lock *RedisLock) blockingLock() (bool, error) {
	timeoutCh := time.After(lock.blockWaitingTime)
	//轮询ticker,定时器, 100ms循环一次去获取锁
	ticker := time.NewTicker(100 * time.Millisecond)
	defer ticker.Stop()

	for {
		select {
		case <-timeoutCh:
			return false, fmt.Errorf("block waiting timeout,err:%w", ErrLockAcquiredByOthers)
		case <-ticker.C:
			success, err := lock.tryLock() //尝试获取锁
			if success && err == nil {
				return success, nil
			}
		}
	}
}

测试使用

这样lock先后顺序可以获得锁了。

func main() {
	testBlockingLock()
}

func testBlockingLock() {
	client := NewClient()
	defer client.Close()

	val, _ := client.Ping().Result()
	fmt.Println(val)

	key := "blockLock"
	lock := redislock.NewRedisLock(client, key, redislock.WithBlock(), redislock.WithBlockWaiting(15*time.Second))

	var wg sync.WaitGroup
	wg.Add(1)

	go func() {
		//尝试获取锁
		if success, err := lock.Lock(); success && err == nil {
			fmt.Println("go BLOCKlock get..")
			time.Sleep(4 * time.Second)
			lock.Unlock()
		}
		wg.Done()
	}()

	//尝试获取锁
	if success, err := lock.Lock(); success && err == nil {
		fmt.Println("BLOCKlock get...")
		time.Sleep(7 * time.Second)
		lock.Unlock()
	}
	wg.Wait()
}

2.锁续期的看门狗实现

这里仍然存在一个问题:当锁的持有者任务未完成,但是锁的有效期已过,虽然持有者此时仍可以完成任务,并且也不会误删其他持有者的锁,但是此时可能会存在多个执行者同时执行临界区代码,使得数据的一致性难以保证,造成意外的后果,分布式锁就失去了意义。

因此,需要一个锁的自动续期机制,分布式锁框架Redission中就有这么一个看门狗,专门为将要到期的锁进行续期。这里我们也来实现一个简单的看门狗。

在LockOptions添加关于锁续期和看门狗标识

const (
	// 默认的分布式锁过期时间,也是默认的续期时长
	DefaultExpireTime = 20 * time.Second
	// 看门狗工作时间间隙
	WatchDogWorkStepTime    = 10 * time.Second
    ..........
)

type LockOptions struct {
    ................
    //强调,expire是key的过期时长,也是要进行续期时的续期时长
    expire           time.Duration
	wathchDogMode      bool
}

 下面是关于锁续期和看门狗标识的设置方法。

//设置续期的时长,也是key过期的时长,(在支持阻塞式等待获取锁的时候已展示过)
func WithExpire(exprie time.Duration) LockOptionFunc {
	return func(option *LockOptions) {
		option.expire = exprie
	}
}

func setLock(o *LockOptions) {
	if o.isBlock && o.blockWaitingTime <= 0 {
		//没有设置默认阻塞时间就使用默认阻塞时长
		o.blockWaitingTime = DefaultBlockWaitingTime
	}
	if o.watchDogWorkStepTime == 0 {
		o.watchDogWorkStepTime = DefaultWatchDogWorkStepTime
	}

	//简单起见,就设置是开启看门狗模式
	o.wathchDogMode = true

	if o.expire == 0 {
		o.expire = DefaultExpireTime
	}
	//比较续期时长和看门狗工作时间间隔
	if o.expire <= o.watchDogWorkStepTime {
		o.watchDogWorkStepTime = o.expire - 2
	}
}

添加watchDog方法

在watchDog内部开启新协程执行runWatchDog。把context.WithCancel的结果赋值给结构体RedisLock的stopDog,到时解锁的时候就可以调用RedisLock.stopDog就可以停止看门狗,回收看门狗协程。协程中调用runWatchDog方法。

type RedisLock struct {
    ....................
	// 停止看门狗
	stopDog context.CancelFunc //通过context.CancelFunc去停止看门狗
}

func (lock *RedisLock) watchDog() {
	if !lock.wathchDogMode {
		return
	}

	var ctx context.Context
	ctx, lock.stopDog = context.WithCancel(context.Background())
	//启动看门狗
	go func() {
		lock.runWatchDog(ctx)
	}()
}

runWatchDog方法中使用了go语言标准库中的Ticker实现定时查看锁是否过期。

在select 语句中,每隔WatchDogWorkStepTime秒就会触发一次 ticker进行续期,将key的过期时间重置。注意,这里也是使用Lua脚本封装了确认锁与锁续期的操作来用于原子化,以防止误续期了其他持有者的锁。

func (lock *RedisLock) runWatchDog(ctx context.Context) error {
	//开启一个定时器
	ticker := time.NewTicker(lock.watchDogWorkStepTime)
	defer ticker.Stop()
	script := redis.NewScript(LauCheckThenExpire)
	for {
		select {
		case <-ticker.C:
			result, err := script.Run(lock.redisCli, []string{lock.key}, lock.Id, lock.expire+3).Result()
			if err != nil {
				return err
			}
			if ret, _ := result.(int64); ret != 1 {
				return errors.New("can not expire lock without ownership of lock")
			}
		case <-ctx.Done():
			return nil
		}
	}
}

加锁时刻

相比起之前的,主要是添加了开头的defer函数。只要最终是获取了锁,就执行watchDog()。

func (lock *RedisLock) Lock() (success bool, err error) {
	defer func() {
		if success && err == nil {
			lock.watchDog()
		}
	}()

	//不管是否是阻塞的,都是要先获取一次锁
	success, err = lock.tryLock()
	if success && err == nil {
		return success, err
	}

	//非阻塞加锁失败的话,直接返回错误
	if !lock.isBlock {
		return false, err
	}

	//基于阻塞模式轮询去获取锁
	success, err = lock.blockingLock()
	return
}

 解锁时刻

相比之前的,也是添加了defer函数。这里就是用lock.stopDog()来停止看门狗,也规避潜在的协程泄漏问题.

func (lock *RedisLock) Unlock() error {
	defer func() {
		//停止看门狗
		if lock.stopDog != nil {
			lock.stopDog()
		}
	}()

	script := redis.NewScript(LauCheckAndDelete)
	result, err := script.Run(lock.redisCli, []string{lock.key}, lock.Id).Int64()
	if err != nil {
		return err
	}
	if result != 1 {
		return errors.New("can not unlock without ownership of lock")
	}
	return nil
}

3.RedLock实现

为什么需要RedLock

redis 的容错机制:为避免单点故障引起数据丢失问题,redis 会基于主从复制的方式实现数据备份增加服务的容错性.

以哨兵机制为例,哨兵会持续监听 master 节点的健康状况,倘若 master 节点发生故障,哨兵会负责扶持 slave 节点上位成为 master,以保证整个集群能够正常对外提供服务。

在分布式系统存在一个经典的 CAP 理论。

  • C:consistency,一致性

  • A:availability,可用性

  • P:Partition tolerance,分区容错性

 redis 走的是 AP 路线,为了保证服务的可用性和吞吐量,redis 在进行数据的主从同步时,采用的是异步执行机制

我们试想一种场景:

  • 时刻1:使用方 A 在 redis master 节点加锁成功,完成了锁数据的写入操作

  • 时刻2:redis master 宕机了,锁数据还没来得及同步到 slave 节点

  • 时刻3:未同步到锁数据的 slave 节点被哨兵升级为新的 master

  • 时刻4:使用方 B 前来取锁,由于新 master 中确实锁数据,所以使用方 B 加锁成功

这个时候可以使用redis红锁(redlock,全称 redis distribution lock)。redLock 的策略是通过增加锁的数量并基于多数派准则来解决这个问题

保证在 RedLock 下所有 redis 节点中达到半数以上节点可用时,整个红锁就能够正常提供服务。

规则的具体细节:

  • 获取当前的时间(毫秒)
  • 使用相同的key和随机值在N个master上获取锁。这里获取锁的尝试时间要远远小于锁的超时时间,是为了防止某个master挂了之后我们还在不停获取锁,导致被阻塞时间过长。比如:该锁20s过期,三个节点加锁花了21秒,那就是加锁失败。
  • 在大多数master上获取到了锁,并且中的获取时间小于锁的过期时间的情况下,才会被认为锁获取成功。
  • 如果锁获取成功,那锁的超时时间 = 最初的锁超时时间 - 获取锁的总耗时时间。
  • 如果锁获取失败,不管是因为获取成功的master的个数没有过半,还是因为获取锁的耗时超过了锁的过期时间,都会将已经设置了该key的master上的把该key删除。

 添加关于红锁的Option和结构体RedLock

添加结构体RedLockOptions。其内包括了单个节点的请求耗时的超时时间singleNodeTimeout和整个红锁的过期时间。

//option.go
//红锁的操作
type RedLockOptionFunc func(*RedLockOptions)

type RedLockOptions struct {
	singleNodeTimeout time.Duration //单个节点的请求耗时的超时时间
	exprie            time.Duration //整个红锁的过期时间
}

func WithSingleNodeTimeout(singleNodeTimeout time.Duration) RedLockOptionFunc {
	return func(opt *RedLockOptions) {
		opt.singleNodeTimeout = singleNodeTimeout
	}
}

func WithRedLockExpire(expire time.Duration) RedLockOptionFunc {
	return func(opt *RedLockOptions) {
		opt.exprie = expire
	}
}

func setRedLock(opt *RedLockOptions) {
	if opt.singleNodeTimeout <= 0 {
		opt.singleNodeTimeout = DefaultSingleLockTimeout
	}

	if opt.exprie <= 0 {
		opt.exprie = DefaultExpireTime
	}
}

新添redlock.go文件。添加结构体RedLock。

其是对多个节点进行加锁,锁的数量会增多,所以 RedLock中会存有*RedisLock的数组,还有RedLock的一些选项配置。

//redlock.go
//单个节点的请求锁的耗时时间上限
const DefaultSingleLockTimeout = 50 * time.Millisecond

type RedLock struct {
	locks []*RedisLock
	RedLockOptions
}

创建RedLock

因为是多个节点了,就会有多个节点的client,可以在option.go文件中创建结构体SingleNode,其中存有redis的地址和密码。

//option.go
type SingleNode struct {
	Address  string    //redis的地址
	Password string    //redis的密码
}

创建红锁主要分成4个步骤:

  1. 判断节点的个数,小于3个无意义
  2. 进行红锁的配置设置option
  3. 判断所有节点累计的加锁超时时间是否小于设定的分布式锁过期时间的1/10,这点是对应 获取锁的尝试时间要远远小于锁的超时时间。(不一定要1/10,可以自己设置)
  4. 对所有节点进行连接,并创建每个节点的redislock,并赋值给红锁的成员locks
func NewRedLock(key string, nodes []*SingleNode, opts ...RedLockOptionFunc) (*RedLock, error) {
	//步骤1 ,节点个数<3,没有意义
	if len(nodes) < 3 {
		return nil, errors.New("the number of node is less than 3")
	}

    //步骤2
	lock := RedLock{}
	for _, opt := range opts {
		opt(&lock.RedLockOptions)
	}
	setRedLock(&lock.RedLockOptions)

    //步骤3
	if lock.exprie > 0 && time.Duration(len(nodes))*lock.singleNodeTimeout*10 > lock.exprie {
		// 要求所有节点累计的超时阈值要小于分布式锁过期时间的十分之一
		return nil, errors.New("expire thresholds of single node is too long")
	}

    //步骤4
	lock.locks = make([]*RedisLock, 0, len(nodes))
	for _, node := range nodes {
		client := redis.NewClient(&redis.Options{
			Addr:     node.Address,
			Password: node.Password,
		})
		lock.locks = append(lock.locks, NewRedisLock(client, key, WithExpireSeconds(lock.exprie)))
	}

	return &lock, nil
}

加锁

对每个node进行加锁。并且对在singleNodeTimeout耗时时间内的加锁成功的锁进行计数。

要是加锁成功的个数超过一半,那即是加锁成功。

func (r *RedLock) Lock() (bool, error) {
	//成功加锁的个数
	successNum := 0
	//对每个node尝试加锁
	for _, lock := range r.locks {
		startTime := time.Now()
		success, err := lock.Lock()
		cost := time.Since(startTime)
		if err == nil && success && cost <= r.singleNodeTimeout {
			successNum++
		}
	}

	if successNum < (len(r.locks)>>1)+1 {
		return false, errors.New("lock failed,lock nodes are Not enough for half")
	}
	return true, nil
}

解锁

需要对所有节点进行解锁。其解锁是使用了(RedisLock).Unlock()。

// 解锁,需对所有节点解锁
func (r *RedLock) Unlock() error {
	var allErr error
	for _, lock := range r.locks {
		if err := lock.Unlock(); err != nil {
			if allErr == nil {
				allErr = err
			}
		}
	}
	return allErr
}

测试使用

func main() {
	testReadLock()
}

func testReadLock() {
	nodes := getNodes()
	key := "redLock"
	redLock, err := redislock.NewRedLock(key, nodes, redislock.WithRedLockExpire(10*time.Second), redislock.WithSingleNodeTimeout(100*time.Millisecond))
	if err != nil {
		return
	}

	var wg sync.WaitGroup
	wg.Add(1)

	go func() {
		//lock1尝试获取锁
		if success, err := redLock.Lock(); success && err == nil {
			fmt.Println("go redLock get..")
			time.Sleep(4 * time.Second)
			redLock.Unlock()
		}
		wg.Done()
	}()

	//lock2尝试获取锁
	if success, err := redLock.Lock(); success && err == nil {
		fmt.Println("redLock get...")
		time.Sleep(7 * time.Second)
		redLock.Unlock()
	}
	wg.Wait()
}

func getNodes() []*redislock.SingleNode {
	//三个节点
	addr1 := "127.0.0.1:10000"
	passwd1 := "okredis"

	addr2 := "127.0.0.1:10001"
	passwd2 := "okredis"

	addr3 := "127.0.0.1:10002"
	passwd3 := "okredis"

	return []*redislock.SingleNode{
		{
			Address:  addr1,
			Password: passwd1,
		},
		{
			Address:  addr2,
			Password: passwd2,
		},
		{
			Address:  addr3,
			Password: passwd3,
		},
	}
}

还是会存在的问题

在5台机器中(都是master),在代码中依次对这5台机器去加锁,只有成功的机器数大于一半就算加锁成功,其他机器也就没必要再去操作了,相反,如果大于一半的机器失败了,就算失败,其他机器也就没必要再去操作了。

 这时一样会出问题。

  • 线程A要加锁,对1,2,3,4,5这5个实例进行加锁。1,2,3成功,4,5加锁超时,那这时有三个master加锁成功,已超过一半,即是最终加锁成功了。
  • 而这时节点3挂了。很快运维人员把一个新节点顶替已挂的节点3。
  • 在新节点还没有该锁key时候,线程B来获取该锁,这时节点3,4,5就获取锁成功,也因为成功个数超过一半,也即是获取锁成功。这时就有两个线程同时获取同一把锁。

所以说红锁也是不能完全解决所有问题的。 

 Redis 官网关于红锁的描述,你能看到著名的关于红锁的神仙打架事件。即 Martin Kleppmann 和 Antirez 的 RedLock 辩论。一个是很有资历的分布式架构师,一个是 Redis 之父。

所以,使用红锁还是需要慎重。而且本文章实现的红锁是比较简单的,还有很多细节没有考虑到的。

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

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

相关文章

美团一面:说说synchronized的实现原理?问麻了。。。。

引言 在现代软件开发领域&#xff0c;多线程并发编程已经成为提高系统性能、提升用户体验的重要手段。然而&#xff0c;多线程环境下的数据同步与资源共享问题也随之而来&#xff0c;处理不当可能导致数据不一致、死锁等各种并发问题。为此&#xff0c;Java语言提供了一种内置…

Pots(DFS BFS)

//新生训练 #include <iostream> #include <algorithm> #include <cstring> #include <queue> using namespace std; typedef pair<int, int> PII; const int N 205; int n, m; int l; int A, B, C; int dis[N][N];struct node {int px, py, op…

谱重排变换和同步压缩变换的区别是什么?

谱重排方法能够得到非常高的时频分辨率&#xff0c;但是同样也存在一个问题&#xff0c;不能重构原始信号&#xff0c;2011 年 Daubechies 提出了一种基于相位的高分辨率时频分析方法—同步压缩小波变换&#xff0c;该方法也是一种谱重排的方法&#xff0c;能使非平稳非线性信号…

Mybatis报错:Unsupported conversion from LONG to java.sql.Timestamp

Mybatis在封装结果集的时候&#xff0c;如果方法返回的是对象&#xff0c;则会去调用这个对象的无参构造方法。 如果实体类标注了Builder注解&#xff0c;则此注解会把默认的构造方法全部改成私有的&#xff0c;则Mybatis在通过无参构造方法反射创建对象时&#xff0c;就会找不…

Redis中的集群(二)

节点 集群数据结构 redisClient结构和clusterLink结构的相同和不同之处 redisClient结构和clusterLink结构都有自己的套接字描述符和输入、输出缓冲区&#xff0c;这两个结构的区别在于&#xff0c;redisClient结构中的套接字和缓冲区是用于连接客户端的&#xff0c;而clust…

已解决:windows 下无法加载文件 xxx.ps1,因为在此系统上禁止运行脚本

目录 1&#xff0c;问题描述2&#xff0c;问题解决 1&#xff0c;问题描述 当通过 npm 全局安装依赖后&#xff08;比如 ts 对应的 tsc 命令&#xff0c;还有 pnpm&#xff09;&#xff0c;想直接使用安装的命令&#xff0c;就会报错&#xff1a; 2&#xff0c;问题解决 以管…

2024年AI带来的革命性变革与创新

大家好&#xff01;相信大家对于AI&#xff08;人工智能&#xff09;的发展已经有了一定的了解&#xff0c;但你是否意识到&#xff0c;到了2024年&#xff0c;AI已经变得如此强大和普及&#xff0c;带来了我们从未想象过的便利和创新呢&#xff1f;让我们一起来看看AI在这个时…

Python学习笔记11 - 列表

1. 列表的创建与删除 2. 列表的查询操作 3. 列表的增、删、改操作 4. 列表元素的排序 5. 列表生成式

负荷预测 | Matlab基于TCN-GRU-Attention单输入单输出时间序列多步预测

目录 效果一览基本介绍程序设计参考资料 效果一览 基本介绍 1.Matlab基于TCN-GRU-Attention单输入单输出时间序列多步预测&#xff1b; 2.单变量时间序列数据集&#xff0c;采用前12个时刻预测未来96个时刻的数据&#xff1b; 3.excel数据方便替换&#xff0c;运行环境matlab20…

QT drawPixmap和drawImage处理图片模糊问题

drawPixmap和drawImage显示图片时&#xff0c;如果图片存在缩放时&#xff0c;会出现模糊现象&#xff0c;例如将一个100x100 的图片显示到30x30的区域&#xff0c;这个时候就会出现模糊。如下&#xff1a; 实际图片&#xff1a; 这个问题就是大图显示成小图造成的像素失真。 当…

蓝桥杯刷题-16-买瓜-DFS+剪枝优化⭐⭐

蓝桥杯2023年第十四届省赛真题-买瓜 该如何剪枝呢&#xff1f;⭐⭐ 如果当前方案的切的刀数&#xff0c;已经大于等于了之前已知合法方案的最优解&#xff0c;那么就没必要 往后搜了。如果后面的瓜的总和加起来&#xff0c;再加上当前已有的重量&#xff0c;都不到m,那么也没…

Flask Web框架的使用-安装Flask

Flask Web框架的使用-安装Flask 一、前言二、安装Flask 一、前言 个人主页: ζ小菜鸡大家好我是ζ小菜鸡&#xff0c;让我们一起来学习Flask Web框架的使用-安装Flask如果文章对你有帮助、欢迎关注、点赞、收藏(一键三连) 二、安装Flask 大多数Python 包都是使用pip 实用工具安…

《看漫画学C++》第9章 直达记忆深处的数据类型——指针类型

C中最难的主题之一莫过于指针&#xff0c;《看漫画学C》通过漫画形式介绍知识。 上述知识点摘录于&#xff1a;《看漫画学C》第9章 直达记忆深处的数据类型——指针类型

机器学习(五) -- 监督学习(2) -- k近邻

系列文章目录及链接 目录 前言 一、K近邻通俗理解及定义 二、原理理解及公式 1、距离度量 四、接口实现 1、鸢尾花数据集介绍 2、API 3、流程 3.1、获取数据 3.2、数据预处理 3.3、特征工程 3.4、knn模型训练 3.5、模型评估 3.6、结果预测 4、超参数搜索-网格搜…

VRRP虚拟路由实验(思科)

一&#xff0c;技术简介 VRRP&#xff08;Virtual Router Redundancy Protocol&#xff09;是一种网络协议&#xff0c;用于实现路由器冗余&#xff0c;提高网络可靠性和容错能力。VRRP允许多台路由器共享一个虚拟IP地址&#xff0c;其中一台路由器被选为Master&#xff0c;负…

杨笛一新作:社恐有救了,AI大模型一对一陪聊,帮i人变成e人

ChatGPT狂飙160天&#xff0c;世界已经不是之前的样子。 新建了免费的人工智能中文站https://ai.weoknow.com 新建了收费的人工智能中文站ai人工智能工具 更多资源欢迎关注 在社交活动中&#xff0c;大语言模型既可以是你的合作伙伴&#xff08;partner&#xff09;&#xff0…

链表的中间结点——每日一题

题目链接&#xff1a; OJ链接 题目&#xff1a; 给你单链表的头结点 head &#xff0c;请你找出并返回链表的中间结点。 如果有两个中间结点&#xff0c;则返回第二个中间结点。 示例 1&#xff1a; 输入&#xff1a;head [1,2,3,4,5] 输出&#xff1a;[3,4,5] 解释&…

【二分查找】Leetcode 点名

题目解析 LCR 173. 点名 算法讲解 1. 哈希表 class Solution { public:int takeAttendance(vector<int>& nums) {map<int, int> Hash;for(auto n : nums) Hash[n];for(int i 0; i < nums[nums.size() - 1]; i){if(Hash[i] 0)return i;}return nums.si…

Java设计模式—组合模式(Composite Pattern)

组合模式&#xff08;Composite&#xff09;&#xff0c;将对象组合成树形结构以表示部分-整体的层次结构。组合模式使得用户对单个对象和组合对象的使用具有一致性。 public class CompositeTest {public static void main(String[] args){// 创建主菜单MenuComponent mainMen…

【Unity添加远程桌面】使用Unity账号远程控制N台电脑

设置地址&#xff1a; URDP终极远程桌面&#xff1b;功能强大&#xff0c;足以让开发人员、设计师、建筑师、工程师等等随时随地完成工作或协助别人https://cloud-desktop.u3dcloud.cn/在网站登录自己的Unity 账号上去 下载安装被控端安装 保持登录 3.代码添加当前主机 "…