Spring Cloud Alibaba - Sentinel源码分析(二)

news2024/11/26 0:22:51

目录

一、Sentinel源码分析

1、时间窗算法

2、滑动时间窗算法

3、Sentinel滑动时间窗口算法源码解析

4、Sentinel滑动窗口数据统计源码解析


一、Sentinel源码分析

1、时间窗算法

时间窗算法,也可以称之为:固定时间窗算法

概念:固定时间窗口计数器算法思想:在固定的时间窗口内,可以允许固定数量的请求进入。超过数量就拒绝或者排队,等下一个时间段进入。

具体分析一下:
1. 将当前的时间分为10t大小的几个时间窗
2. 规则是阈值为100个请求数,每个时间窗里面的请求数量不能超过阈值100
3. 10t到16t进入请求10个,16t到20t进入请求50个,总数60个请求,没有超过阈值100
4. 20t到26t进入请求60个,26t到30t进入请求20个,总数80个请求,没有超过阈值100
5. 30t到40t之间进入请求120个,超过阈值20个,所以20个请求无法进入
存在问题:16t到26t之间也是10t大小的一个时间窗,但是请求总数为110,超过阈值,这种固定时间窗无法处理这部分超出的请求,解决办法就是使用滑动时间窗

2、滑动时间窗算法

        使用滑动时间窗的原因,就在于虽然以上提到超出阈值的部分分别在两个时间窗中,但是实际上我们要清楚,我们系统限流的目的是要在任意时间都要能应对突然的流量暴增,如果使用以上的算法,就会造成在16t和26t之间的请求无法限流,从而严重会导致服务雪崩。
        要解决的话,我们就需要使用滑动时间窗算法,具体原理如下:
        滑动时间窗限流算法解决了固定时间窗限流算法的问题。其没有划分固定的时间窗起点与终点,而是将每一次请求的到来时间点作为统计时间窗的终点,起点则是终点向前推时间窗长度的时间点。这种时间窗称为“滑动时间窗”

实际上当前的时间窗不再是固定的,而是可以从时间的起始位置一直向右滑动

 这样的话就可以解决固定时间窗带来的问题

其原理就是:
1. 当前时间窗口为滑动窗口,可以从左向右按照时间顺序进行滑动,并且大小为10t,同时此时的阈值为100
2. 红色线的位置进入一个请求,此时想要判断这个请求是否能够正常通过,就要看当前滑动窗口中的请求数量是否达到阈值,如果当前没有达到阈值100,就可以正常通过,但是如果一旦超过阈值,就会被进行限流。

没有超过阈值:

 超过阈值:

但是此时滑动时间窗还是有问题的,问题就是会出现大量的重复统计,造成系统效率下降,如下图所示:

在此图中我们就可以看出,这个蓝色的区域就是重复统计的区域,也就是说每一次移动时间窗口,都需要重新统计重复区域的请求数量,从而导致浪费大量的系统资源。

滑动时间窗口算法-改进

想要解决以上的问题,我们就需要更加细粒度话的计算,增加多个子时间窗口:样本窗口

概念:
1. 样本窗口的长度必须小于滑动窗口长度,如果等于滑动窗口长度就会变成固定时间窗口
2. 一般滑动窗口长度是样本窗口的整数倍,比如:4*样本窗口=1个滑动窗口
3. 每个样本窗口在到达终点时间时,会统计本样本窗口中的流量数据并且记录下来。
4. 当一个请求达到时,会统计当前请求时间点所在的样本窗口中的流量数据,然后在获取当前请求时间的样本窗口以外的同一个滑动窗口中的样本窗口的统计数据,进行求和,如果没有超出阈值,则通过,否则就会被限流。

原理图:

下一个进入请求的时间点
此时这个请求将不会被限流,因为本次请求的时间的对应的样本窗口只有5个请求加上之前重复的样本窗口统计的流量值,没有超过阈值100,所以本次请求会通过。

3、Sentinel滑动时间窗口算法源码解析

首先看StatisticSlot.entry方法中node.addPassRequest(count)方法

//StatisticSlot.entry  node.addPassRequest(count)
// 增加通过请求的数量(这里涉及到滑动窗口算法)
node.addPassRequest(count);

进入方法DefaultNode.addPassRequest

//DefaultNode.addPassRequest
    @Override
    public void addPassRequest(int count) {
        super.addPassRequest(count);
        this.clusterNode.addPassRequest(count);
    }
//StatisticNode.addPassRequest
    @Override
    public void addPassRequest(int count) {
        // 为滑动计数器增加本次的访问数据
        rollingCounterInSecond.addPass(count);
        rollingCounterInMinute.addPass(count);
    }

最后进入ArrayMetric.addPass,这是一个使用数组保存数据的计量器类

//ArrayMetric.addPass
    @Override
    public void addPass(int count) {
        // 获取当前时间点所在的样本窗口
        WindowWrap<MetricBucket> wrap = data.currentWindow();
        // 将当前请求的计数量添加到当前样本窗口的统计数据中
        wrap.value().addPass(count);
    }

先来跟踪data.currentWindow();

//LeapArray:环形数组
    public WindowWrap<T> currentWindow() {
        // 获取当前时间所在的样本窗口
        return currentWindow(TimeUtil.currentTimeMillis());
    }

这里就会进入LeapArray(环形数组)中的currentWindow方法中,这个环形数组,其实就是Sentinel官方提供的原理图中的环形数组WindowLeapArray

 

// 环形数组
public abstract class LeapArray<T> {
    // 样本窗口长度
    protected int windowLengthInMs;
    // 一个时间窗中包含的时间窗数量
    protected int sampleCount;
    // 时间窗长度
    protected int intervalInMs;
    private double intervalInSecond;
 
    // 这个一个数组,元素为WindowWrap样本窗口
    // 注意,这里的泛型 T 实际为 MetricBucket 类型
    protected final AtomicReferenceArray<WindowWrap<T>> array;
 ......   
}  

这里要注意这个数组,这个数组里面所存放的类型就是WindowWrap窗口类型,泛型T是MetricBucket这里我们来看一下这个类型

public WindowWrap(long windowLengthInMs, long windowStart, T value) {
    //样本窗口长度
    this.windowLengthInMs = windowLengthInMs;
    //样本窗口的起始时间戳
    this.windowStart = windowStart;
    //当前样本窗口的统计数据 其类型为MetricBucket
    this.value = value;
}
//..LeapArray
public WindowWrap<T> currentWindow() {
    // 获取当前时间所在的样本窗口
    return currentWindow(TimeUtil.currentTimeMillis());
}
//------------------------------------------------------------
public WindowWrap<T> currentWindow(long timeMillis) {
        if (timeMillis < 0) {
            return null;
        }
        // 计算当前时间所在的样本窗口id,即在计算数组LeapArray中的索引
        int idx = calculateTimeIdx(timeMillis);
        // Calculate current bucket start time.
     	// 计算当前样本窗口的开始时间点
        long windowStart = calculateWindowStart(timeMillis);
     .....
 }

在这里我们先分析calculateTimeIdx方法

private int calculateTimeIdx(/*@Valid*/ long timeMillis) {
    // 计算当前时间在那个样本窗口(样本窗口下标),当前时间/样本窗口长度
    long timeId = timeMillis / windowLengthInMs;
    // Calculate current index so we can map the timestamp to the leap array.
    // 计算具体索引,这个array就是装样本窗口的数组
    return (int)(timeId % array.length());
}

timeId(样本窗口下标)原理如下:

 具体索引原理:

 接着分析计算当前样本窗口的起点

// 计算当前样本窗口的开始时间点
long windowStart = calculateWindowStart(timeMillis);
//------------------------------
protected long calculateWindowStart(/*@Valid*/ long timeMillis) {
    // 计算当前样本窗口的起点 当前时间点-(当前时间点%样本窗口长度)
    return timeMillis - timeMillis % windowLengthInMs;
}

原理如下:

 继续向下分析

//LeapArray.currentWindow
...
        while (true) {
            // 获取到当前时间所在的样本窗口
            WindowWrap<T> old = array.get(idx);
            // 如果获取不到,表示没有创建
            if (old == null) {
                /*
                 *     B0       B1      B2    NULL      B4
                 * ||_______|_______|_______|_______|_______||___
                 * 200     400     600     800     1000    1200  timestamp
                 *                             ^
                 *                          time=888
                 *            bucket is empty, so create new and update
                 *
                 * If the old bucket is absent, then we create a new bucket at {@code windowStart},
                 * then try to update circular array via a CAS operation. Only one thread can
                 * succeed to update, while other threads yield its time slice.
                 */
                // 创建新的时间窗口
                WindowWrap<T> window = new WindowWrap<T>(windowLengthInMs, windowStart, newEmptyBucket(timeMillis));
                // 通过CAS方式将新建窗口放入Array
                if (array.compareAndSet(idx, null, window)) {
                    // Successfully updated, return the created bucket.
                    return window;
                } else {
                    // Contention failed, the thread will yield its time slice to wait for bucket available.
                    Thread.yield();
                }
            // 若当前样本窗口的起始时间点与计算出的样本窗口起始点相同,则说明两个是同一个样本窗口
            } else if (windowStart == old.windowStart()) {
                /*
                 *     B0       B1      B2     B3      B4
                 * ||_______|_______|_______|_______|_______||___
                 * 200     400     600     800     1000    1200  timestamp
                 *                             ^
                 *                          time=888
                 *            startTime of Bucket 3: 800, so it's up-to-date
                 *
                 * If current {@code windowStart} is equal to the start timestamp of old bucket,
                 * that means the time is within the bucket, so directly return the bucket.
                 */
                return old;
             // 若当前样本窗口的起始时间点 大于 计算出的样本窗口起始时间点,说明计算出的样本窗口已经过时了,
            // 需要将原来的样本窗口替换
            } else if (windowStart > old.windowStart()) {
                /*
                 *   (old)
                 *             B0       B1      B2    NULL      B4
                 * |_______||_______|_______|_______|_______|_______||___
                 * ...    1200     1400    1600    1800    2000    2200  timestamp
                 *                              ^
                 *                           time=1676
                 *          startTime of Bucket 2: 400, deprecated, should be reset
                 *
                 * If the start timestamp of old bucket is behind provided time, that means
                 * the bucket is deprecated. We have to reset the bucket to current {@code windowStart}.
                 * Note that the reset and clean-up operations are hard to be atomic,
                 * so we need a update lock to guarantee the correctness of bucket update.
                 *
                 * The update lock is conditional (tiny scope) and will take effect only when
                 * bucket is deprecated, so in most cases it won't lead to performance loss.
                 */
                if (updateLock.tryLock()) {
                    try {
                        // Successfully get the update lock, now we reset the bucket.
                        // 替换掉老的样本窗口
                        return resetWindowTo(old, windowStart);
                    } finally {
                        updateLock.unlock();
                    }
                } else {
                    // Contention failed, the thread will yield its time slice to wait for bucket available.
                    Thread.yield();
                }
            // 当前样本窗口的起始时间点 小于 计算出的样本窗口起始时间点,
            // 这种情况一般不会出现,因为时间不会倒流。除非人为修改了系统时钟
            } else if (windowStart < old.windowStart()) {
                // Should not go through here, as the provided time is already behind.
                return new WindowWrap<T>(windowLengthInMs, windowStart, newEmptyBucket(timeMillis));
            }
        }

替换分析

// 替换掉老的样本窗口
return resetWindowTo(old, windowStart);
//------------------------------------------------------------
// BucketLeapArray.resetWindowTo
@Override
protected WindowWrap<MetricBucket> resetWindowTo(WindowWrap<MetricBucket> w, long startTime) {
    // Update the start time and reset value.
    // 更新窗口起始时间
    w.resetTo(startTime);
    // 将多维度统计数据清零
    w.value().reset();
    return w;
}

更新数据分析

public MetricBucket reset() {
    // 将每个维度的统计数据清零
    for (MetricEvent event : MetricEvent.values()) {
        counters[event.ordinal()].reset();
    }
    initMinRt();
    return this;
}

通过维度

//ArrayMetric.addPass
@Override
public void addPass(int count) {
    // 获取当前时间点所在的样本窗口
    WindowWrap<MetricBucket> wrap = data.currentWindow();
    // 将当前请求的计数量添加到当前样本窗口的统计数据中
    wrap.value().addPass(count);
}
//----------------------------------------
//MetricBucket.addPass
public void addPass(int n) {
    add(MetricEvent.PASS, n);
}

4、Sentinel滑动窗口数据统计源码解析

从FlowSlot的entry进入

//FlowSlot
@Override
public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count,
                  boolean prioritized, Object... args) throws Throwable {
    //检测并且应用流控规则
    checkFlow(resourceWrapper, context, node, count, prioritized);
    //触发下一个Slot
    fireEntry(context, resourceWrapper, node, count, prioritized, args);
}

void checkFlow(ResourceWrapper resource, Context context, DefaultNode node, int count, boolean prioritized)
    throws BlockException {
    // 从这里进入
    checker.checkFlow(ruleProvider, resource, context, node, count, prioritized);
}

checkFlow方法里面找到遍历所有规则的canPassCheck方法然后在进入canPass方法,找到DefaultController对应实现,快速失败的流控效果,我们从这里来看,这里我们要关注的是avgUsedTokens方法,这个方法实际上就是获取当前时间窗里面的已经统计的数据

//DefaultController
@Override
public boolean canPass(Node node, int acquireCount, boolean prioritized) {
    //获取当前node节点的线程数或者请求通过的qps总数
    // 获取当前时间窗已经统计的数据
    int curCount = avgUsedTokens(node);
    //当前请求数(请求的令牌)+申请总数是否(已经消耗的令牌)>该资源配置的总数(阈值)
    // 以前的数据+新的数据
    if (curCount + acquireCount > count) {
        if (prioritized && grade == RuleConstant.FLOW_GRADE_QPS) {
            long currentTime;
            long waitInMs;
            currentTime = TimeUtil.currentTimeMillis();
            waitInMs = node.tryOccupyNext(currentTime, acquireCount, count);
            if (waitInMs < OccupyTimeoutProperty.getOccupyTimeout()) {
                node.addWaitingRequest(currentTime + waitInMs, acquireCount);
                node.addOccupiedPass(acquireCount);
                sleep(waitInMs);

                // PriorityWaitException indicates that the request will pass after waiting for {@link @waitInMs}.
                throw new PriorityWaitException(waitInMs);
            }
        }
        return false;
    }
    return true;
}

跟进一下,看如何获取已经统计的数据

//DefaultController
private int avgUsedTokens(Node node) {
    // 如果没有选出node,代表没有做统计工作,直接返回0
    if (node == null) {
        return DEFAULT_AVG_USED_TOKENS;
    }
    // 判断阈值类型,如果为QPS,则返回当前统计的QPS
    // 如果为线程数,则返回当前的线程数总量
    return grade == RuleConstant.FLOW_GRADE_THREAD ? node.curThreadNum() : (int)(node.passQps());
}

实际上就是判断阈值类型,看QPS类型的统计

//StatisticNode.passQps
    @Override
    public double passQps() {
            // rollingCounterInSecond.pass() 当前时间窗中统计的通过请求数量
            // rollingCounterInSecond.getWindowIntervalInSec() 时间窗口长度
            // 这两个数相除,计算出的就是QPS
        return rollingCounterInSecond.pass() / rollingCounterInSecond.getWindowIntervalInSec();
    }

那么这里就需要查看pass方法,看是如何统计通过请求总量的

//ArrayMetric
    @Override
    public long pass() {    
        // 更新array中当前时间点所在样本窗口实例中的数据
        data.currentWindow();
        long pass = 0;
        // 将当前时间窗口中的所有样本窗口统计的value读取出来,并且记录
        List<MetricBucket> list = data.values();

        for (MetricBucket window : list) {
            pass += window.pass();
        }
        return pass;
    }

那么这里我们先跟踪values(),其实这里就是拿出没有过时有效的样本窗口数据

//LeapArray
    public List<T> values() {
        return values(TimeUtil.currentTimeMillis());
    }
// ----------------------------
public List<T> values(long timeMillis) {
    if (timeMillis < 0) {
        return new ArrayList<T>();
    }
    int size = array.length();
    List<T> result = new ArrayList<T>(size);
    // 这个遍历array中的每一个样本窗口实例
    for (int i = 0; i < size; i++) {
        WindowWrap<T> windowWrap = array.get(i);
        // 若当前遍历实例为空或者已经过时,则继续下一个
        if (windowWrap == null || isWindowDeprecated(timeMillis, windowWrap)) {
            continue;
        }
        result.add(windowWrap.value());
    }
    return result;
}

判断位置过时判断逻辑:

//LeapArray
    public boolean isWindowDeprecated(long time, WindowWrap<T> windowWrap) {
        // 当前时间-样本窗口起始时间>时间窗口  说明过时了
        return time - windowWrap.windowStart() > intervalInMs;
    }

那么这里分析完成以后我们再回到ArrayMetric中的pass方法中,看遍历方法,就是将所有的有效并且是通过维度的数据统计出来进行求和,看是否超过阈值。

//ArrayMetric
@Override
public long pass() {
    // 更新array中当前时间点所在样本窗口实例中的数据
    data.currentWindow();
    long pass = 0;
    // 将当前时间窗口中的所有样本窗口统计的value读取出来,并且记录
    List<MetricBucket> list = data.values();
	
    // 将List中所有pass维度的统计数据并取出求和
    for (MetricBucket window : list) {
        // 通过维度
        pass += window.pass();
    }
    return pass;
}

Spring Cloud Alibaba - Sentinel源码分析(一)

干我们这行,啥时候懈怠,就意味着长进的停止,长进的停止就意味着被淘汰,只能往前冲,直到凤凰涅槃的一天!

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

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

相关文章

015:vue项目中常用的正则表达式

第015个 查看专栏目录: VUE — element UI 专栏目标 在vue和element UI联合技术栈的操控下&#xff0c;本专栏提供行之有效的源代码示例和信息点介绍&#xff0c;做到灵活运用。 &#xff08;1&#xff09;提供vue2的一些基本操作&#xff1a;安装、引用&#xff0c;模板使用…

【RV1126】使用gpiolib框架

文章目录 史上最简单&#xff1a;增加GPIO控制功能是如何实现的呢&#xff1f;GPIOLIB框架Linux 驱动实现 控制引脚输出高低电平综合测试 这一套非常方便&#xff01; 史上最简单&#xff1a;增加GPIO控制功能 如果是想增加GPIO控制只需要修改设备树就可以做到&#xff01; …

谷粒商城第二天-项目环境搭建

目录 一、前言 二、学习的内容 一、虚拟平台的安装&#xff0c;远程连接虚拟机的工具的安装 二、Docker以及常用软件的安装 一、安装Docker&#xff1a; 二、安装相关软件 三、开发环境的统一 1. 这里就是调整Maven的下载依赖的地址&#xff0c;改用阿里云镜像地址 2. …

11.vue3医疗在线问诊项目 - _药品订单 ==> 支付页面、支付详情、支付结果、订单详情、物流信息、高德地图工具

11.vue3医疗在线问诊项目 - _药品订单 &#xff1e; 支付页面、支付详情、支付结果、订单详情、物流信息、高德地图工具 药品订单-支付页面-路由 目标&#xff1a;配置路由&#xff0c;分析药品支付组件结构 1&#xff09;路由与组件 {path: /medicine/pay,component: () >…

系列十一、MongoDB副本集

一、概述 MongoDB副本集&#xff08;Replica Set&#xff09;是有自动故障恢复功能的主从集群&#xff0c;有一个Primary节点和一个或者多个Secondary节点组成。副本集没有固定的主节点&#xff0c;当主节点发生故障时&#xff0c;整个集群会选举一个主节点 为系统提供服务以保…

大数据测试基本知识

常用大数据框架结构 1.大数据测试常用到的软件工具 工具推荐&#xff0c;对于测试数据构造工具有&#xff1a;Datafaker、DbSchema、Online test data generator等&#xff1b;ETL测试工具有&#xff1a;RightData、QuerySurge等&#xff1b;数据质量检查工具&#xff1a;great…

SpringBoot医药管理系统设计+第三稿+文档

博主介绍&#xff1a;✌在职Java研发工程师、专注于程序设计、源码分享、技术交流、专注于Java技术领域和毕业设计✌ 项目名称 SpringBoot医药管理系统设计第三稿文档 视频演示 SpringBoot医药管理系统设计第三稿中期检查表ppt外文文献翻译文献综述开题任务书查重报告安装视频讲…

【计算机网络】第五章数据链路层-电子科技大学2023期末考试

第五章 数据链路层 学习目的 目的1&#xff1a;理解链路层服务的主要功能 差错检查、纠错 共享广播信道&#xff1a;多点接入问题(multiple access) 链路层寻址(link layer addressing) 局域网技术&#xff1a;Ethernet, VLANs 目的2&#xff1a;链路层技术的实现 点到点…

【Java入门】-- Java基础详解之【程序控制结构】

目录 1.程序流程控制介绍 2.顺序控制 3.分支控制if-else 4.嵌套分支 5.switch分支语句 6.for循环控制&#xff08;重点&#xff09; 7.while循环控制 8.do...while循环控制 9.多重循环控制&#xff08;重难点&#xff01;&#xff09; 10.跳转控制语句break 11.跳转…

计算机课程个性化内容推荐系统的设计与实现+文档等

博主介绍&#xff1a;✌在职Java研发工程师、专注于程序设计、源码分享、技术交流、专注于Java技术领域和毕业设计✌ 项目名称 计算机课程个性化内容推荐系统的设计与实现文档等 视频演示 计算机课程个性化内容推荐系统的设计与实现 系统介绍 计算机课程个性化内容推荐系统的设…

Car Guide

文章目录 科目一第一章 机动车驾驶证申领和使用规定第一节 驾驶证的许可&#xff1f;种类和有效期第二节 驾驶证的申领第三节 驾驶证的使用第四节 驾驶考试第五节 违法记分制度 第二章 交通信号第一节 交通信号灯第二节 交通标志第三节 交通标线第四节 交警手势 第三章 道路交通…

Ansible之playbooks剧本

目录 一、playbooks1、playbooks简述2、playbooks剧本格式3、playbooks组成部分4、playbooks启动 二、playbook编写 apache 的yum安装部署剧本三、playbook编写 nginx 的yum安装并且能修改其监听端口的剧本四、 playbook的定义、引用变量1、基础变量的定义与引用2、引用fact信息…

Stable Diffusion webui 基础参数学习

哈喽&#xff0c;各位小伙伴们大家好&#xff0c;最近一直再研究人工智能类的生产力&#xff0c;不得不说随着时代科技的进步让人工智能也得到了突破性的发展。而小编前段时间玩画画也是玩的不可自拔&#xff0c;你能想想得到&#xff0c;一个完全不会画画的有一天也能创作出绘…

【计算机网络】第一章概论-电子科技大学2023期末考试

相关术语 URI&#xff1a;Uniform Resource Identifier 统一资源标识符&#xff0c;指的是一个资源 URL&#xff1a;Uniform Resource Location 统一资源定位符&#xff0c;URI的子集&#xff0c;用地址定为的方式指定一个资源 URN&#xff1a;Uniform Resource Name 统一资…

MySQL事务相关笔记

杂项 InnoDB最大特点&#xff1a;支持事务和行锁&#xff1b; MyISAM不支持事务 介绍 一个事务是由一条或者多条对数据库操作的SQL语句所组成的一个不可分割的单元&#xff0c;只有当事务中的所有操作都正常执行完了&#xff0c;整个事务才会被提交给数据库。事务有如下特性…

【使用指导】wifi蓝牙二合一模块LCS2028与服务器的数据收发功能测试指导

在物联网智能家居、智能照明、智能楼宇、智慧工厂、智能制造等领域的数据透传、智能控制应用中&#xff0c;支持UART串口通信的低功耗WiFi蓝牙二合一模块应用极为广泛。模块性能测试环节中会测试模块与服务器的数据收发功能&#xff0c;确保功能性能够满足项目应用需求。本篇就…

深入理解 SpringBoot 日志框架:从入门到高级应用——(四)Logback 输出日志到 QQ邮箱

文章目录 获取 QQ 邮箱授权码添加依赖编写 SMTPAppender运行结果 要将 Logback 输出日志到 QQ 邮箱&#xff0c;需要执行以下步骤&#xff1a; 在 QQ 邮箱中获取授权码。在你的 SpringBoot 项目中添加 Logback 依赖和 SMTP 协议实现库&#xff0c;例如 Email 依赖。在 Logback…

NOTA-Me-Tetrazine,NOTA-甲基四嗪,大环化合物具有良好的配位和鳌合能力

文章关键词&#xff1a;甲基四嗪修饰大环配体&#xff0c;双功能螯合剂&#xff0c;大环化合物 ●中文名&#xff1a;NOTA-甲基四嗪 ●英文名&#xff1a;NOTA-Me-Tetrazine ●外观以及性质&#xff1a; 西安凯新生物科技有限公司供应的​NOTA-Me-Tetrazine中四嗪修饰大环配体&…

LaTeX插入参考文献

接着上一篇&#xff0c;用EndNote组织参考文献&#xff0c;然后再导入到LeTex中感觉不太好用&#xff0c;然后就学习了一下BibTeX来管理参考文献&#xff0c;发现还可以&#xff0c;这里记录一下&#xff0c;方便以后查阅。 LaTeX插入参考文献 thebibliographyBibTeX参考资料 t…

总结901

目标规划&#xff1a; 月目标&#xff1a;6月&#xff08;线性代数强化9讲&#xff0c;考研核心词过三遍&#xff09; 周目标&#xff1a;线性代数强化5讲&#xff0c;英语背3篇文章并回诵&#xff0c;检测 每日规划 今日已做 1.回环复习之前背过的文章。 2.背单词&#xf…