五、分布式锁-redission

news2024/11/18 23:36:30

源码仓库地址:git@gitee.com:chuangchuang-liu/hm-dingping.git

1、redission介绍

目前基于redis的setnx特性实现的自定义分布式锁仍存在的问题:

问题描述
重入问题同一个线程无法多次获取统一把锁。当方法A成功获取锁后,调用方法B,方法B也要获取锁,此时由于锁是不可重入的,也就是被方法A占用着,此时就产生了死锁的问题
不可重试自定义分布式锁无失败重试机制
超时释放锁的超时释放虽然可以避免死锁问题,但确实也可能存在业务执行时间比较长的情况,那这种情况下就仍存在安全隐患问题
主从一致性如果Redis提供了主从集群,当我们向集群写数据时,主机需要异步的将数据同步给从机,而万一在同步过去之前,主机宕机了,就会出现死锁问题。

什么是Redission?
Redission是一个用于Java的Redis客户端,它提供了丰富的特性,包括内存数据网格的功能。它支持同步/异步/RxJava/Reactive API,拥有超过50种基于Redis的Java对象和服务。Redission的使用非常简单,没有学习曲线,您不需要了解任何Redis命令就可以开始使用。(GitHub - redisson/redisson, Redisson官网)
Redission可以让Java应用更方便地访问和操作Redis数据存储,适合于需要高性能和高并发的应用场景。

2、快速开始

  1. 导入依赖
<!--redission-->
<dependency>
  <groupId>org.redisson</groupId>
  <artifactId>redisson</artifactId>
  <version>3.13.6</version>
</dependency>
  1. Redission配置客户端
@Configuration
public class RedisConfig {

    @Bean
    public RedissonClient redisClient(){

        Config config = new Config();
        // 可以用"rediss://"来启用SSL连接
        config.useSingleServer().setAddress("redis://192.168.224.128:6379").setPassword("123456");
        return Redisson.create(config);
    }
}
  1. 使用Redission分布式锁
@Resource
private RedissionClient redissonClient;

@Test
void testRedisson() throws Exception{
    //获取锁(可重入),指定锁的名称
    RLock lock = redissonClient.getLock("anyLock");
    //尝试获取锁,参数分别是:获取锁的最大等待时间(期间会重试),锁自动释放时间,时间单位
    boolean isLock = lock.tryLock(1,10,TimeUnit.SECONDS);
    //判断获取锁成功
    if(isLock){
        try{
            System.out.println("执行业务");          
        }finally{
            //释放锁
            lock.unlock();
        }
    }
}

3、redission可重入锁原理

3.1、原理介绍

在Lock锁中,他是借助于底层的一个voaltile的一个state变量来记录重入的状态的,比如当前没有人持有这把锁,那么state=0,假如有人持有这把锁,那么state=1,如果持有这把锁的人再次持有这把锁,那么state就会+1 ,如果是对于synchronized而言,他在c语言代码中会有一个count,原理和state类似,也是重入一次就加一,释放一次就-1 ,直到减少成0 时,表示当前这把锁没有被人持有。 、

而在redission中,也支持这种可重入锁原理,是通过redis的hash数据结构实现的。其中key表示这把锁是否存在,field判断锁是被哪个线程持有,value则记录锁被持有次数。
image.png

3.2、源码剖析

  • 获取锁

其中各参数解释:
KEYS[1]:锁的名称
ARGV[1]:锁过期时间
ARGV[2]:id + “:” + threadId

"if (redis.call('exists', KEYS[1]) == 0) then " +
"redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"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]);"

判断锁是否存在
如果不存在,则设置当前线程标识,计数器+1;设置过期时间;
如果存在。做二次判断,判断锁的持有线程是不是自己?
如果是,计数器+1,重置锁的过期时间;
如果不是,获取锁失败,返回锁的剩余过期时间

  • 释放锁

其中各参数解释:
KEYS[1]:锁的名称
KEYS[2]:订阅频道
ARGV[1]:是要发布的消息内容
ARGV[2]:锁过期时间
ARGV[3]:id + “:” + threadId

"if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +
"return nil;" +
"end; " +
"local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +
"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;"

判断锁是不是当前线程?
不是==>直接返回
是==>计数器–
二次判断,判断计数器是否大于0
大于0==>重置锁过期时间
否则==>真正释放锁

4、redission锁重试和WatchDog机制

4.1、redission是如何解决不可重试的?

源码剖析:
用户调用tryLock方法时,指定waitTime最大等待时间

public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {
    long time = unit.toMillis(waitTime);
    long current = System.currentTimeMillis();
    long threadId = Thread.currentThread().getId();
    Long ttl = tryAcquire(waitTime, leaseTime, unit, threadId);
    // lock acquired
    if (ttl == null) {
        return true;
    }

    time -= System.currentTimeMillis() - current;
    if (time <= 0) {
        acquireFailed(waitTime, unit, threadId);
        return false;
    }

    current = System.currentTimeMillis();
    RFuture<RedissonLockEntry> subscribeFuture = subscribe(threadId);
    if (!subscribeFuture.await(time, TimeUnit.MILLISECONDS)) {
        if (!subscribeFuture.cancel(false)) {
            subscribeFuture.onComplete((res, e) -> {
                if (e == null) {
                    unsubscribe(subscribeFuture, threadId);
                }
            });
        }
        acquireFailed(waitTime, unit, threadId);
        return false;
    }

    try {
        time -= System.currentTimeMillis() - current;
        if (time <= 0) {
            acquireFailed(waitTime, unit, threadId);
            return false;
        }

        while (true) {
            long currentTime = System.currentTimeMillis();
            ttl = tryAcquire(waitTime, leaseTime, unit, threadId);
            // lock acquired
            if (ttl == null) {
                return true;
            }

            time -= System.currentTimeMillis() - currentTime;
            if (time <= 0) {
                acquireFailed(waitTime, unit, threadId);
                return false;
            }

            // waiting for message
            currentTime = System.currentTimeMillis();
            if (ttl >= 0 && ttl < time) {
                subscribeFuture.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
            } else {
                subscribeFuture.getNow().getLatch().tryAcquire(time, TimeUnit.MILLISECONDS);
            }

            time -= System.currentTimeMillis() - currentTime;
            if (time <= 0) {
                acquireFailed(waitTime, unit, threadId);
                return false;
            }
        }
    } finally {
        unsubscribe(subscribeFuture, threadId);
    }
    //        return get(tryLockAsync(waitTime, leaseTime, unit));
}
  1. 计算等待时间和获取当前时间:将用户指定的等待时间转换为毫秒,并记录方法调用时的当前时间。
  2. 尝试获取锁。如果ttl为空,则获取锁成功;否则,返回的是其他线程占用锁的剩余有效时间
  3. 检查剩余等待时间。如果time小于等于0,调用acquireFailed方法返回false
  4. 订阅锁。通过subscribe方法订阅相关的锁。如果在剩余时间内未能订阅成功,处理取消订阅并调用acquireFailed方法,返回false。
  5. 循环等待锁释放消息。等待过程中会调用tryAcquire方法获取锁,如果获取成功返回true
  6. 处理锁的ttl。如果ttl大于0,返回锁被其他线程占用的剩余过期时间(ttl)。更新剩余等待时间(time)。以time和ttl中较小的值继续等待再次尝试。
  7. 再次检查等于剩余等待时间。如果小于0,调用acquireFailed方法返回false
  8. 循环结束后(要么获取锁成功,要么超过最大等待时间了),最终调用unsubscribe方法取消订阅

结论1:redission不是获取锁失败后立即进行重试,而是等待“一定时间”后再进行重试,节省了一定的CPU资源,对服务器性能有一定提升;
结论2:一定要采取调用tryLock方法携带参数waitTime的重载方法,其他重载的tryLock方法底层是不具备重试机制的。

4.2、redission是如何解决锁超时释放的-看门狗机制?

自定义分布式锁仍存在的一个问题是:锁的超时释放虽然可以避免死锁问题,但确实也可能存在业务执行时间比较长的情况,那这种情况下业务还未执行完毕,锁就被释放了,存在一定的安全隐患。
源码剖析:

private RFuture<Boolean> tryAcquireOnceAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
    if (leaseTime != -1) {
        return tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_NULL_BOOLEAN);
    }
    RFuture<Boolean> ttlRemainingFuture = tryLockInnerAsync(waitTime,
                                                            commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(),
                                                            TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_NULL_BOOLEAN);
    ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
        if (e != null) {
            return;
        }

        // lock acquired
        if (ttlRemaining) {
            // 开启任务更新过期时间
            scheduleExpirationRenewal(threadId);
        }
    });
    return ttlRemainingFuture;
}
private void renewExpiration() {
    ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());
    if (ee == null) {
        return;
    }

    Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
        @Override
        public void run(Timeout timeout) throws Exception {
            ExpirationEntry ent = EXPIRATION_RENEWAL_MAP.get(getEntryName());
            if (ent == null) {
                return;
            }
            Long threadId = ent.getFirstThreadId();
            if (threadId == null) {
                return;
            }

            RFuture<Boolean> future = renewExpirationAsync(threadId);
            future.onComplete((res, e) -> {
                if (e != null) {
                    log.error("Can't update lock " + getName() + " expiration", e);
                    return;
                }

                if (res) {
                    // reschedule itself
                    renewExpiration();
                }
            });
        }
    }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);

    ee.setTimeout(task);
}
protected RFuture<Boolean> renewExpirationAsync(long threadId) {
        return evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
                "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
                        "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                        "return 1; " +
                        "end; " +
                        "return 0;",
                Collections.singletonList(getName()),
                internalLockLeaseTime, getLockName(threadId));
    }
  1. 如果没有指定leaseTime,那么底层会默认传入commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout()看门狗时间
  2. 在leasetime的1/3处时间,会创建一个任务renewExpirationAsync方法来异步地更新重置锁过期时间
  3. 递归地调用自身来更新锁过期时间,直到业务处理完毕。

至此redission解决了因业务阻塞而导致锁提前释放的问题

业务执行完毕,释放锁源码剖析:

public RFuture<Void> unlockAsync(long threadId) {
    RPromise<Void> result = new RedissonPromise<Void>();
    // 释放锁
    RFuture<Boolean> future = unlockInnerAsync(threadId);

    future.onComplete((opStatus, e) -> {
        // 取消锁失效时间更新重置任务
        cancelExpirationRenewal(threadId);

        if (e != null) {
            result.tryFailure(e);
            return;
        }

        if (opStatus == null) {
            IllegalMonitorStateException cause = new IllegalMonitorStateException("attempt to unlock lock, not locked by current thread by node id: "
                    + id + " thread-id: " + threadId);
            result.tryFailure(cause);
            return;
        }

        result.trySuccess(null);
    });

    return result;
}



void cancelExpirationRenewal(Long threadId) {
    ExpirationEntry task = EXPIRATION_RENEWAL_MAP.get(getEntryName());
    if (task == null) {
        return;
    }
    
    if (threadId != null) {
        task.removeThreadId(threadId);
    }

    if (threadId == null || task.hasNoThreads()) {
        Timeout timeout = task.getTimeout();
        if (timeout != null) {
            timeout.cancel();
        }
        // 删除递归更新锁时间任务
        EXPIRATION_RENEWAL_MAP.remove(getEntryName());
    }
}

当业务执行完毕且锁正常释放后,删除递归更新锁时间任务,避免redission一直递归创建任务更新锁过期时间

5、redission锁的MultiLock原理

为了提高redis的可用性,我们会搭建集群或者主从,现在以主从为例
此时我们去写命令,写在主机上, 主机会将数据同步给从机,但是假设在主机还没有来得及把数据写入到从机去的时候,此时主机宕机,哨兵会发现主机宕机,并且选举一个slave变成master,而此时新的master中实际上并没有锁信息,此时锁信息就已经丢掉了。
1653553998403.png
为了解决该问题,redission的方案是去掉redis集群主从关系,每一个节点都是平等的。加锁逻辑是需要写入到每一个节点上才算加锁成功。这样,当某一台机器宕机了,这台机器的slave节点变为master节点,此时另一个线程趁虚而入,虽然可以正常写入,但其它机器仍会写入失败,最终结果仍是获取锁失败,从而保证了获取锁的可靠性。
1653554055048.png
MulitLock源码剖析:

public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {
    //        try {
    //            return tryLockAsync(waitTime, leaseTime, unit).get();
    //        } catch (ExecutionException e) {
    //            throw new IllegalStateException(e);
    //        }
    long newLeaseTime = -1;
    if (leaseTime != -1) {
        if (waitTime == -1) {
            newLeaseTime = unit.toMillis(leaseTime);
        } else {
            newLeaseTime = unit.toMillis(waitTime)*2;
        }
    }

    long time = System.currentTimeMillis();
    long remainTime = -1;
    if (waitTime != -1) {
        remainTime = unit.toMillis(waitTime);
    }
    long lockWaitTime = calcLockWaitTime(remainTime);

    int failedLocksLimit = failedLocksLimit();
    List<RLock> acquiredLocks = new ArrayList<>(locks.size());
    for (ListIterator<RLock> iterator = locks.listIterator(); iterator.hasNext();) {
        RLock lock = iterator.next();
        boolean lockAcquired;
        try {
            if (waitTime == -1 && leaseTime == -1) {
                lockAcquired = lock.tryLock();
            } else {
                long awaitTime = Math.min(lockWaitTime, remainTime);
                lockAcquired = lock.tryLock(awaitTime, newLeaseTime, TimeUnit.MILLISECONDS);
            }
        } catch (RedisResponseTimeoutException e) {
            unlockInner(Arrays.asList(lock));
            lockAcquired = false;
        } catch (Exception e) {
            lockAcquired = false;
        }

        if (lockAcquired) {
            acquiredLocks.add(lock);
        } else {
            if (locks.size() - acquiredLocks.size() == failedLocksLimit()) {
                break;
            }

            if (failedLocksLimit == 0) {
                unlockInner(acquiredLocks);
                if (waitTime == -1) {
                    return false;
                }
                failedLocksLimit = failedLocksLimit();
                acquiredLocks.clear();
                // reset iterator
                while (iterator.hasPrevious()) {
                    iterator.previous();
                }
            } else {
                failedLocksLimit--;
            }
        }

        if (remainTime != -1) {
            remainTime -= System.currentTimeMillis() - time;
            time = System.currentTimeMillis();
            if (remainTime <= 0) {
                unlockInner(acquiredLocks);
                return false;
            }
        }
    }

    if (leaseTime != -1) {
        List<RFuture<Boolean>> futures = new ArrayList<>(acquiredLocks.size());
        for (RLock rLock : acquiredLocks) {
            RFuture<Boolean> future = ((RedissonLock) rLock).expireAsync(unit.toMillis(leaseTime), TimeUnit.MILLISECONDS);
                futures.add(future);
            }
            
            for (RFuture<Boolean> rFuture : futures) {
                rFuture.syncUninterruptibly();
            }
        }
        
        return true;
    }
  1. 遍历锁集合,调用lock.tryLock尝试获取锁。将获取结果传给变量lockAcquired
  2. 如果获取成功,将当前锁存放到acquiredLocks集合中
  3. 获取成功后,如果此时的剩余等待时间小于等于0,释放自己已获取的锁,返回false
  4. 如果获取失败,判断是否具备重试机制
    1. 没有重试,则直接返回false
    2. 有重试机制,将acquiredLocks集合清空,将iterator指针前移,重新遍历尝试。

6、结论

目前已接触的分布式锁有:

  • 可不重入锁/自定义分布式锁:

原理: 利用setnx特性、expire避免死锁、添加线程标识避免锁误删
缺点: 仍存在不可重入、失败不可重试、锁超时失效等问题

  • 可重入锁:

原理: 利用hash数据结构存储线程标识和重入次数、利用看门狗机制延续锁失效时间、利用信号量机制控制等待重试时间
缺点: 仍存在集群模式下redis宕机导致的锁失效问题

  • MulitLock

原理: 利用多个平等的redis节点,所有redis都写入才算获取锁成功
缺点: 维护成本高,实现相对复杂

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

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

相关文章

【C++】如何用一个哈希表同时封装出unordered_set与unordered_map

&#x1f440;樊梓慕&#xff1a;个人主页 &#x1f3a5;个人专栏&#xff1a;《C语言》《数据结构》《蓝桥杯试题》《LeetCode刷题笔记》《实训项目》《C》《Linux》《算法》 &#x1f31d;每一个不曾起舞的日子&#xff0c;都是对生命的辜负 目录 前言 1.哈希桶源码 2.哈希…

19.删除链表的倒数第N个结点 92.反转链表II

给你一个链表&#xff0c;删除链表的倒数第 n 个结点&#xff0c;并且返回链表的头结点。 示例 1&#xff1a; 输入&#xff1a;head [1,2,3,4,5], n 2 输出&#xff1a;[1,2,3,5]示例 2&#xff1a; 输入&#xff1a;head [1], n 1 输出&#xff1a;[]示例 3&#xff1a; …

模拟-算法

文章目录 替换所有的问号提莫攻击Z字形变换外观数列数青蛙 替换所有的问号 算法思路&#xff1a; 从前往后遍历整个字符串&#xff0c;找到问号之后&#xff0c;就遍历 a ~ z 去尝试替换即可。 class Solution {public String modifyString(String s) {char[] ss s.toCharA…

删除字符串--给你一个字符串S,要求你将字符串中出现的所有“gzu“子串删除,输出删除之后的S。

输入描述: 输入一行字符串S&#xff0c;长度不超过100。 输出描述: 输出进行删除操作之后的S。 #include <stdio.h> #include <stdlib.h> #include <string.h>//结合了串的模式匹配算法思路int main(){char s[100];char a[3]{g,z,u};gets(s);int nstrlen…

数据库语言一些基本操作

1&#xff0c;消除取值重复的行。 例如&#xff1a;查成绩不及格的学号&#xff1a;SELECT DISTINCT sno FROM SC WHERE grade<60. 这里使用DISTINCT表示取消取值重复的行。 2&#xff0c;比较。 例如&#xff1a;查计算机系全体学生的姓名&#xff1a;SELECT Sname FROM…

C++一维数组练习oj(3)

为什么C的一维数组练习要出要做那么多的题目&#xff1f;因为我们是竞赛学生&#xff01;想要将每个知识点灵活运用的话就必须刷大量的题目来锻炼思维。 我使用的是jsswoj.com这个刷题网站&#xff0c;当然要钱... C一维数组练习oj(2)-CSDN博客这是上一次的题目讲解 这道题有…

java每日一题——买啤酒(递归经典问题)

前言&#xff1a; 非常喜欢的一道题&#xff0c;经典中的经典。打好基础&#xff0c;daydayup!!!啤酒问题&#xff1a;一瓶啤酒2元&#xff0c;4个盖子可以换一瓶&#xff0c;2个空瓶可以换一瓶&#xff0c;请问10元可以喝几瓶 题目如下&#xff1a; 啤酒问题&#xff1a;一瓶…

[Halcon学习笔记]在Qt上实现Halcon窗口的字体设置颜色设置等功能

1、 Halcon字体大小设置在Qt上的实现 在之前介绍过Halcon窗口显示文字字体的尺寸和样式&#xff0c;具体详细介绍可回看 &#xff08;一&#xff09;Halcon窗口界面上显示文字的字体尺寸、样式修改 当时介绍的设定方法 //Win下QString Font_win "-Arial-10-*-1-*-*-1-&q…

传输层——UDP协议

端口号(Port) 端口号标识了一个主机上进行通信的不同的应用程序&#xff0c;准确来说&#xff0c;端口号标识了主机上唯一的一个进程。 在TCP/IP协议中, 用 "源IP", "源端口号", "目的IP", "目的端口号", "协议号" 这样一个…

罗德与施瓦茨联合广和通全面验证RedCap模组FG132系列先进性能

近日&#xff0c;罗德与施瓦茨联合广和通完成Redcap(Reduce Capability)功能和性能验证。本次测试使用R&SCMX500 OBT(One Box Tester)无线通信测试仪&#xff0c;主要验证广和通RedCap模组FG132系列射频性能以及IP层吞吐量&#xff0c;包括RedCap上下行吞吐量和射频指标如矢…

Java 自定义线程池实现

自定义线程池 简介任务图示阻塞队列 BlockingQueue<T>ReentrantLock代码 线程池 ThreadPool工作线程类 Worker 拒绝策略接口代码测试类 TestThreadPool为什么需要j i&#xff1f;&#xff08;lambad表达式相关&#xff09; 测试结果拒绝策略&#xff1a;让调用者自己执行…

c++常考基础知识(2)

二.c关键字 关键字汇总 c中共有63个关键字&#xff0c;其中包括int&#xff0c;char&#xff0c;double等类型关键字&#xff0c;if&#xff0c;else&#xff0c;while&#xff0c;do&#xff0c;等语法关键字&#xff0c;还有sizeof等函数关键字。 三.数据结构 1.数组&#x…

Navicat 干货 | 探索 PostgreSQL 的外部数据包装器和统计函数

PostgreSQL 因其稳定性和可扩展性而广受青睐&#xff0c;为开发人员和数据管理员提供了许多有用的函数。在这些函数中&#xff0c;file_fdw_handler、file_fdw_validator、pg_stat_statements、pg_stat_statements_info 以及 pg_stat_statements_reset 是其中的重要函数&#x…

浩哥带你做项目,纯免费教学

浩哥带你做项目 一、YiYi-Web项目开发1. 简介2. 技术栈2.1 后端开发环境2.2 前端开发环境 3.项目截图 二、计算机游戏程序设计&#xff08;基础篇&#xff09;三、RuoYi-Cloud项目学习1.功能介绍2.项目截图 四、鸿蒙应用开发五、软考六、Linux基础知识学习 最近浩哥社区群涌进大…

【Python从入门到进阶】51、电影天堂网站多页面下载实战

接上篇《50、当当网Scrapy项目实战&#xff08;三&#xff09;》 上一篇我们讲解了使用Scrapy框架在当当网抓取多页书籍数据的效果&#xff0c;本篇我们来抓取电影天堂网站的数据&#xff0c;同样采用Scrapy框架多页面下载的模式来实现。 一、抓取需求 打开电影天堂网站&…

VMware Workstation Pro 17虚拟机超级详细搭建(含redis,nacos,docker, rabbitmq,sentinel,elasticsearch....)(一)

今天从零搭建一下虚拟机的环境&#xff0c;把nacos&#xff0c;redis等微服务组件还有数据库搭建到里面&#xff0c;首先看到的是我们最开始下载VMware Workstation Pro 17 之后的样子&#xff0c;总共一起应该有三部分因为篇幅太长了 下载地址 : VMware - Delivering a Digit…

echarts睡眠分期

效果 echarts核心配置 option {tooltip: {trigger: axis // 触发方式为axis&#xff0c;表示数据项图形触发&#xff0c;此时坐标轴上的刻度也会显示提示信息。},xAxis: {show: false,type: category,data: [2024-02-02 12:00:01,2024-02-02 12:00:02,2024-02-02 12:00:03,20…

stm32启动文件里面的__main和主函数main()

一、__main和main()之间的关系 先来对stm32启动过程简单学习 启动文件里面的Reset_Handler&#xff1a; 调用过程&#xff1a; stm32在启动后先进入重启中断函数Reset_Handler&#xff0c;其中会先后调用SystemInit和__main函数&#xff0c; __main函数属于c库函数&…

3GPP 协议资料学习和文档下载

一、登录3GPP官网 3GPP – The Mobile Broadband Standard 二、选择Specifications Per TSG Round 三、选择ftp下载路径 四、选择不同阶段的3GPP协议 包含了从1999年到R18,甚至更新到当前最新的协议。 五、查看对应版本的LTE或者5G NR协议 其中LTE射频相关章节为36.521系列&…

【黄啊码】使用cloudflare搭建OpenAI的接口

现在&#xff0c;我们可以使用cloudflare自己搭建一个OpenAI代理服务&#xff0c;使用我们自己的转发代理 第一步&#xff1a;注册cloudflare账号 前往官方网站注册一个账户 第二步&#xff1a;创建worker&#xff0c;进行请求中转 名字可以自己随便取一个&#xff0c;点击快…