Sentinel源码—3.ProcessorSlot的执行过程一
大纲
1.NodeSelectorSlot构建资源调用树
2.LogSlot和StatisticSlot采集资源的数据
3.Sentinel监听器模式的规则对象与规则管理
4.AuthoritySlot控制黑白名单权限
5.SystemSlot根据系统保护规则进行流控
1.NodeSelectorSlot构建资源调用树
(1)Entry的处理链的执行入口
(2)NodeSelectorSlot的源码
(3)Context对象中存储的资源调用树总结
(1)Entry的处理链的执行入口
每当一个线程处理包含某些资源的接口请求时,会调用SphU的entry()方法去创建并管控该接口中涉及的Entry资源访问对象。
在创建Entry资源访问对象的期间,会创建一个ResourceWrapper对象、一个Context对象、以及根据ResourceWrapper对象创建或获取一个ProcessorSlotChain对象,也就是把ProcessorSlotChain对象、Context对象与ResourceWrapper对象绑定到Entry对象中。
public class SphU {private static final Object[] OBJECTS0 = new Object[0];...public static Entry entry(String name) throws BlockException {//调用CtSph.entry()方法创建一个Entry资源访问对象,默认的请求类型为OUTreturn Env.sph.entry(name, EntryType.OUT, 1, OBJECTS0);}
}public class Env {//创建一个CtSph对象public static final Sph sph = new CtSph();static {InitExecutor.doInit();}
}public class CtSph implements Sph {//Same resource will share the same ProcessorSlotChain}, no matter in which Context.//Same resource is that ResourceWrapper#equals(Object).private static volatile Map<ResourceWrapper, ProcessorSlotChain> chainMap = new HashMap<ResourceWrapper, ProcessorSlotChain>();...@Overridepublic Entry entry(String name, EntryType type, int count, Object... args) throws BlockException {//StringResourceWrapper是ResourceWrapper的子类,且StringResourceWrapper的构造方法默认了资源类型为COMMONStringResourceWrapper resource = new StringResourceWrapper(name, type);return entry(resource, count, args);}//Do all {@link Rule}s checking about the resource.public Entry entry(ResourceWrapper resourceWrapper, int count, Object... args) throws BlockException {//调用CtSph.entryWithPriority()方法,执行如下处理://初始化Context -> 将Context与线程绑定 -> 初始化Entry -> 将Context和ResourceWrapper放入Entry中return entryWithPriority(resourceWrapper, count, false, args);}private Entry entryWithPriority(ResourceWrapper resourceWrapper, int count, boolean prioritized, Object... args) throws BlockException {//从当前线程中获取ContextContext context = ContextUtil.getContext();if (context instanceof NullContext) {return new CtEntry(resourceWrapper, null, context);}//如果没获取到Contextif (context == null) {//Using default context.//创建一个名为sentinel_default_context的Context,并且与当前线程绑定context = InternalContextUtil.internalEnter(Constants.CONTEXT_DEFAULT_NAME);}//Global switch is close, no rule checking will do.if (!Constants.ON) {return new CtEntry(resourceWrapper, null, context);}//调用CtSph.lookProcessChain()方法初始化处理链(处理器插槽链条)ProcessorSlot<Object> chain = lookProcessChain(resourceWrapper);if (chain == null) {return new CtEntry(resourceWrapper, null, context);}//创建出一个Entry对象,将处理链(处理器插槽链条)、Context与Entry绑定//其中会将Entry的三个基础属性(封装在resourceWrapper里)以及当前Entry所属的Context作为参数传入CtEntry的构造方法Entry e = new CtEntry(resourceWrapper, chain, context);try {//处理链(处理器插槽链条)入口,负责采集数据,规则验证//调用DefaultProcessorSlotChain.entry()方法执行处理链每个节点的逻辑(数据采集+规则验证)chain.entry(context, resourceWrapper, null, count, prioritized, args);} catch (BlockException e1) {//规则验证失败,比如:被流控、被熔断降级、触发黑白名单等e.exit(count, args);throw e1;} catch (Throwable e1) {RecordLog.info("Sentinel unexpected exception", e1);}return e;}...private final static class InternalContextUtil extends ContextUtil {static Context internalEnter(String name) {//调用ContextUtil.trueEnter()方法创建一个Context对象return trueEnter(name, "");}static Context internalEnter(String name, String origin) {return trueEnter(name, origin);}}//Get ProcessorSlotChain of the resource. //new ProcessorSlotChain will be created if the resource doesn't relate one.//Same resource will share the same ProcessorSlotChain globally, no matter in which Context.//Same resource is that ResourceWrapper#equals(Object).ProcessorSlot<Object> lookProcessChain(ResourceWrapper resourceWrapper) {ProcessorSlotChain chain = chainMap.get(resourceWrapper);if (chain == null) {synchronized (LOCK) {chain = chainMap.get(resourceWrapper);if (chain == null) {//Entry size limit.if (chainMap.size() >= Constants.MAX_SLOT_CHAIN_SIZE) {return null;}//调用SlotChainProvider.newSlotChain()方法初始化处理链(处理器插槽链条)chain = SlotChainProvider.newSlotChain();//写时复制Map<ResourceWrapper, ProcessorSlotChain> newMap = new HashMap<ResourceWrapper, ProcessorSlotChain>(chainMap.size() + 1);newMap.putAll(chainMap);newMap.put(resourceWrapper, chain);chainMap = newMap;}}}return chain;}
}public class StringResourceWrapper extends ResourceWrapper {public StringResourceWrapper(String name, EntryType e) {//调用父类构造方法,且默认资源类型为COMMONsuper(name, e, ResourceTypeConstants.COMMON);}...
}//Utility class to get or create Context in current thread.
//Each SphU.entry() should be in a Context.
//If we don't invoke ContextUtil.enter() explicitly, DEFAULT context will be used.
public class ContextUtil {//Store the context in ThreadLocal for easy access.//存放线程与Context的绑定关系//每个请求对应一个线程,每个线程绑定一个Context,所以每个请求对应一个Contextprivate static ThreadLocal<Context> contextHolder = new ThreadLocal<>();//Holds all EntranceNode. Each EntranceNode is associated with a distinct context name.//以Context的name作为key,EntranceNode作为value缓存到HashMap中private static volatile Map<String, DefaultNode> contextNameNodeMap = new HashMap<>();private static final ReentrantLock LOCK = new ReentrantLock();private static final Context NULL_CONTEXT = new NullContext();...//ContextUtil.trueEnter()方法会尝试从ThreadLocal获取一个Context对象//如果获取不到,再创建一个Context对象然后放入ThreadLocal中//入参name其实一般就是默认的Constants.CONTEXT_DEFAULT_NAME=sentinel_default_context//由于当前线程可能会涉及创建多个Entry资源访问对象,所以trueEnter()方法需要注意并发问题protected static Context trueEnter(String name, String origin) {//从ThreadLocal中获取当前线程绑定的Context对象Context context = contextHolder.get();//如果当前线程还没绑定Context对象,则初始化Context对象并且与当前线程进行绑定if (context == null) {//首先要获取或创建Context对象所需要的EntranceNode对象,EntranceNode会负责统计名字相同的Context下的指标数据//将全局缓存contextNameNodeMap赋值给一个临时变量localCacheNameMap//因为后续会对contextNameNodeMap的内容进行修改,所以这里需要将原来的contextNameNodeMap复制一份出来//从而避免后续对contextNameNodeMap的内容进行修改时,可能造成对接下来读取contextNameNodeMap内容的影响Map<String, DefaultNode> localCacheNameMap = contextNameNodeMap;//从缓存副本localCacheNameMap中获取EntranceNode//这个name其实一般就是默认的sentinel_default_contextDefaultNode node = localCacheNameMap.get(name);//如果获取的EntranceNode为空if (node == null) {//为了防止缓存无限制地增长,导致内存占用过高,需要设置一个上限,只要超过上限,就直接返回NULL_CONTEXTif (localCacheNameMap.size() > Constants.MAX_CONTEXT_NAME_SIZE) {setNullContext();return NULL_CONTEXT;} else {//如果Context还没创建,缓存里也没有当前Context名称对应的EntranceNode,并且缓存数量尚未达到2000//那么就创建一个EntranceNode,创建EntranceNode时需要加锁,否则会有线程不安全问题//毕竟需要修改HashMap类型的contextNameNodeMap//通过加锁 + 缓存 + 写时复制更新缓存,避免并发情况下创建出多个EntranceNode对象//一个线程对应一个Context对象,多个线程对应多个Context对象//这些Context对象会使用ThreadLocal进行隔离,但它们的name默认都是sentinel_default_context//根据下面的代码逻辑://多个线程(对应多个Context的name默认都是sentinel_default_context)会共用同一个EntranceNode//于是可知,多个Context对象会共用一个EntranceNode对象LOCK.lock();try {//从缓存中获取EntranceNodenode = contextNameNodeMap.get(name);//对node进行Double Check//如果没获取到EntranceNodeif (node == null) {if (contextNameNodeMap.size() > Constants.MAX_CONTEXT_NAME_SIZE) {setNullContext();return NULL_CONTEXT;} else {//创建EntranceNode,缓存到contextNameNodeMap当中node = new EntranceNode(new StringResourceWrapper(name, EntryType.IN), null);//Add entrance node.//将新创建的EntranceNode添加到ROOT中,ROOT就是每个Node的根结点Constants.ROOT.addChild(node);//写时复制,将新创建的EntranceNode添加到缓存中Map<String, DefaultNode> newMap = new HashMap<>(contextNameNodeMap.size() + 1);newMap.putAll(contextNameNodeMap);newMap.put(name, node);contextNameNodeMap = newMap;}}} finally {//解锁LOCK.unlock();}}}//此处可能会有多个线程同时执行到此处,并发创建多个Context对象//但这是允许的,因为一个请求对应一个Context,一个请求对应一个线程,所以一个线程本来就需要创建一个Context对象//初始化Context,将刚获取到或刚创建的EntranceNode放到Context的entranceNode属性中context = new Context(node, name);context.setOrigin(origin);//将创建出来的Context对象放入ThreadLocal变量contextHolder中,实现Context对象与当前线程的绑定contextHolder.set(context);}return context;}...
}public final class SlotChainProvider {private static volatile SlotChainBuilder slotChainBuilder = null;//The load and pick process is not thread-safe, //but it's okay since the method should be only invoked via CtSph.lookProcessChain() under lock.public static ProcessorSlotChain newSlotChain() {//如果存在,则直接返回if (slotChainBuilder != null) {return slotChainBuilder.build();}//Resolve the slot chain builder SPI.//通过SPI机制初始化SlotChainBuilderslotChainBuilder = SpiLoader.of(SlotChainBuilder.class).loadFirstInstanceOrDefault();if (slotChainBuilder == null) {//Should not go through here.RecordLog.warn("[SlotChainProvider] Wrong state when resolving slot chain builder, using default");slotChainBuilder = new DefaultSlotChainBuilder();} else {RecordLog.info("[SlotChainProvider] Global slot chain builder resolved: {}", slotChainBuilder.getClass().getCanonicalName());}return slotChainBuilder.build();}private SlotChainProvider() {}
}@Spi(isDefault = true)
public class DefaultSlotChainBuilder implements SlotChainBuilder {@Overridepublic ProcessorSlotChain build() {//创建一个DefaultProcessorSlotChain对象实例ProcessorSlotChain chain = new DefaultProcessorSlotChain();//通过SPI机制加载责任链的节点ProcessorSlot实现类//然后按照@Spi注解的order属性进行排序并进行实例化//最后将ProcessorSlot实例放到sortedSlotList中List<ProcessorSlot> sortedSlotList = SpiLoader.of(ProcessorSlot.class).loadInstanceListSorted();//遍历已排好序的ProcessorSlot集合for (ProcessorSlot slot : sortedSlotList) {//安全检查,防止业务系统也写了一个SPI文件,但没按规定继承AbstractLinkedProcessorSlotif (!(slot instanceof AbstractLinkedProcessorSlot)) {RecordLog.warn("The ProcessorSlot(" + slot.getClass().getCanonicalName() + ") is not an instance of AbstractLinkedProcessorSlot, can't be added into ProcessorSlotChain");continue;}//调用DefaultProcessorSlotChain.addLast()方法构建单向链表//将责任链的节点ProcessorSlot实例放入DefaultProcessorSlotChain中chain.addLast((AbstractLinkedProcessorSlot<?>) slot);}//返回单向链表return chain;}
}
在DefaultSlotChainBuilder的build()方法中,从其初始化ProcessorSlotChain的逻辑可知,Entry的处理链的执行入口就是DefaultProcessorSlotChain的entry()方法。
当一个线程调用SphU的entry()方法创建完与接口相关的Entry对象后,就会调用DefaultProcessorSlotChain的entry()方法执行处理链节点的逻辑。因为NodeSelectorSlot是Entry的处理链ProcessorSlotChain的第一个节点,所以接着会调用NodeSelectorSlot的entry()方法。由于处理链中紧接着NodeSelectorSlot的下一个节点是ClusterBuilderSlot,所以执行完NodeSelectorSlot的entry()方法后,会接着执行ClusterBuilderSlot的entry()方法。
public class DefaultProcessorSlotChain extends ProcessorSlotChain {...@Overridepublic void entry(Context context, ResourceWrapper resourceWrapper, Object t, int count, boolean prioritized, Object... args) throws Throwable {//默认情况下会调用处理链的第一个节点NodeSelectorSlot的transformEntry()方法first.transformEntry(context, resourceWrapper, t, count, prioritized, args);}...
}public abstract class AbstractLinkedProcessorSlot<T> implements ProcessorSlot<T> {...void transformEntry(Context context, ResourceWrapper resourceWrapper, Object o, int count, boolean prioritized, Object... args) throws Throwable {T t = (T)o;entry(context, resourceWrapper, t, count, prioritized, args);}...
}
(2)NodeSelectorSlot的源码
NodeSelectorSlot和ClusterBuilderSlot会一起构建Context的资源调用树,资源调用树的作用其实就是用来统计资源的调用数据。
在一个Context对象实例的资源调用树上主要会有如下三类节点:DefaultNode、EntranceNode、ClusterNode,分别对应于:单机里的资源维度、接口维度、集群中的资源维度。
其中DefaultNode会统计名字相同的Context下的某个资源的调用数据,EntranceNode会统计名字相同的Context下的全部资源的调用数据,ClusterNode会统计某个资源在全部Context下的调用数据。
在执行NodeSelectorSlot的entry()方法时,首先会从缓存(NodeSelectorSlot.map属性)中获取一个DefaultNode对象。如果获取不到,再通过DCL机制创建一个DefaultNode对象并更新缓存。其中缓存的key是Context的name,value是DefaultNode对象。由于默认情况下多个线程对应的Context的name都相同,所以多个线程访问同一资源时使用的DefaultNode对象也一样。
在执行ClusterBuilderSlot的entry()方法时,首先会判断缓存是否为null,若是则创建一个ClusterNode对象,然后再将ClusterNode对象设置到DefaultNode对象的clusterNode属性中。
由DefaultNode、EntranceNode、ClusterNode构成的资源调用树:因为DefaultNode是和资源ResourceWrapper以及Context挂钩的,所以DefaultNode应该添加到EntranceNode中。因为ClusterNode和资源挂钩,而不和Context挂钩,所以ClusterNode应该添加到DefaultNode中。
具体的资源调用树构建源码如下:
//This class will try to build the calling traces via:
//adding a new DefaultNode if needed as the last child in the context.
//the context's last node is the current node or the parent node of the context.
//setting itself to the context current node.//It works as follow:
// ContextUtil.enter("entrance1", "appA");
// Entry nodeA = SphU.entry("nodeA");
// if (nodeA != null) {
// nodeA.exit();
// }
// ContextUtil.exit();//Above code will generate the following invocation structure in memory:
// machine-root
// /
// /
// EntranceNode1
// /
// /
// DefaultNode(nodeA)- - - - - -> ClusterNode(nodeA);
//Here the EntranceNode represents "entrance1" given by ContextUtil.enter("entrance1", "appA").
//Both DefaultNode(nodeA) and ClusterNode(nodeA) holds statistics of "nodeA", which is given by SphU.entry("nodeA").
//The ClusterNode is uniquely identified by the ResourceId;
//The DefaultNode is identified by both the resource id and {@link Context}.
//In other words, one resource id will generate multiple DefaultNode for each distinct context,
//but only one ClusterNode.//the following code shows one resource id in two different context:
// ContextUtil.enter("entrance1", "appA");
// Entry nodeA = SphU.entry("nodeA");
// if (nodeA != null) {
// nodeA.exit();
// }
// ContextUtil.exit();
// ContextUtil.enter("entrance2", "appA");
// nodeA = SphU.entry("nodeA");
// if (nodeA != null) {
// nodeA.exit();
// }
// ContextUtil.exit();//Above code will generate the following invocation structure in memory:
// machine-root
// / \
// / \
// EntranceNode1 EntranceNode2
// / \
// / \
// DefaultNode(nodeA) DefaultNode(nodeA)
// | |
// +- - - - - - - - - - +- - - - - - -> ClusterNode(nodeA);
//As we can see, two DefaultNode are created for "nodeA" in two context,
//but only one ClusterNode is created.
//We can also check this structure by calling: http://localhost:8719/tree?type=root
@Spi(isSingleton = false, order = Constants.ORDER_NODE_SELECTOR_SLOT)
public class NodeSelectorSlot extends AbstractLinkedProcessorSlot<Object> {//DefaultNodes of the same resource in different context.//缓存map以Context的name为key,DefaultNode为value//由于默认情况下多个线程对应的Context的name都相同,所以多个线程访问资源时使用的DefaultNode也一样private volatile Map<String, DefaultNode> map = new HashMap<String, DefaultNode>(10);@Overridepublic void entry(Context context, ResourceWrapper resourceWrapper, Object obj, int count, boolean prioritized, Object... args) throws Throwable {//It's interesting that we use context name rather resource name as the map key.//Remember that same resource will share the same ProcessorSlotChain globally, no matter in which context. //Same resource is that ResourceWrapper#equals(Object).//So if code goes into entry(Context, ResourceWrapper, DefaultNode, int, Object...),//the resource name must be same but context name may not.//If we use SphU.entry(String resource)} to enter same resource in different context, //using context name as map key can distinguish the same resource.//In this case, multiple DefaultNodes will be created of the same resource name, //for every distinct context (different context name) each.//Consider another question. One resource may have multiple DefaultNode,//so what is the fastest way to get total statistics of the same resource?//The answer is all DefaultNodes with same resource name share one ClusterNode.//See ClusterBuilderSlot for detail.//先从缓存中获取DefaultNode node = map.get(context.getName());if (node == null) {//使用DCL机制,即Double Check + Lock机制synchronized (this) {node = map.get(context.getName());if (node == null) {//每个线程访问Entry时,都会调用CtSph.entry()方法创建一个ResourceWrapper对象//下面根据ResourceWrapper创建一个DefaultNode对象node = new DefaultNode(resourceWrapper, null);//写时复制更新缓存mapHashMap<String, DefaultNode> cacheMap = new HashMap<String, DefaultNode>(map.size());cacheMap.putAll(map);cacheMap.put(context.getName(), node);map = cacheMap;//Build invocation tree//首先会调用context.getLastNode()方法,获取到的是Context.entranceNode属性即一个EntranceNode对象//EntranceNode对象是在执行ContextUtil.trueEnter()方法创建Context对象实例时添加到Context对象中的//然后会将刚创建的DefaultNode对象添加到EntranceNode对象的childList列表中((DefaultNode) context.getLastNode()).addChild(node);}}}//设置Context的curNode属性为当前获取到或新创建的DefaultNode对象context.setCurNode(node);//触发执行下一个ProcessorSlot,即ClusterBuilderSlotfireEntry(context, resourceWrapper, node, count, prioritized, args);}@Overridepublic void exit(Context context, ResourceWrapper resourceWrapper, int count, Object... args) {fireExit(context, resourceWrapper, count, args);}
}//This slot maintains resource running statistics (response time, qps, thread count, exception),
//and a list of callers as well which is marked by ContextUtil.enter(String origin).
//One resource has only one cluster node, while one resource can have multiple default nodes.
@Spi(isSingleton = false, order = Constants.ORDER_CLUSTER_BUILDER_SLOT)
public class ClusterBuilderSlot extends AbstractLinkedProcessorSlot<DefaultNode> {//Remember that same resource will share the same ProcessorSlotChain globally, no matter in which context.//Same resource is that ResourceWrapper#equals(Object).//So if code goes into entry(Context, ResourceWrapper, DefaultNode, int, boolean, Object...),//the resource name must be same but context name may not.//To get total statistics of the same resource in different context, //same resource shares the same ClusterNode} globally.//All ClusterNodes are cached in this map.//The longer the application runs, the more stable this mapping will become. //so we don't concurrent map but a lock. //as this lock only happens at the very beginning while concurrent map will hold the lock all the time.private static volatile Map<ResourceWrapper, ClusterNode> clusterNodeMap = new HashMap<>();private static final Object lock = new Object();private volatile ClusterNode clusterNode = null;@Overridepublic void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count, boolean prioritized, Object... args) throws Throwable {if (clusterNode == null) {//使用DCL机制,即Double Check + Lock机制synchronized (lock) {if (clusterNode == null) {//Create the cluster node.//创建ClusterNode对象clusterNode = new ClusterNode(resourceWrapper.getName(), resourceWrapper.getResourceType());HashMap<ResourceWrapper, ClusterNode> newMap = new HashMap<>(Math.max(clusterNodeMap.size(), 16));newMap.putAll(clusterNodeMap);newMap.put(node.getId(), clusterNode);clusterNodeMap = newMap;}}}//设置DefaultNode的clusterNode属性为获取到的ClusterNode对象node.setClusterNode(clusterNode);//if context origin is set, we should get or create a new {@link Node} of the specific origin.if (!"".equals(context.getOrigin())) {Node originNode = node.getClusterNode().getOrCreateOriginNode(context.getOrigin());context.getCurEntry().setOriginNode(originNode);}//执行下一个ProcessorSlotfireEntry(context, resourceWrapper, node, count, prioritized, args);}@Overridepublic void exit(Context context, ResourceWrapper resourceWrapper, int count, Object... args) {fireExit(context, resourceWrapper, count, args);}...
}
(3)Context对象中存储的资源调用树总结
其实Context对象的属性entranceNode就代表了一棵资源调用树。
首先,在调用ContextUtil的trueEnter()方法创建Context对象实例时,便会创建一个EntranceNode对象并赋值给Context的entranceNode属性,以及调用Constants.ROOT的addChild()方法,将这个EntranceNode对象放入Constants.ROOT的childList列表中。
然后,执行NodeSelectorSlot的entry()方法时,便会创建一个DefaultNode对象。该DefaultNode对象会被添加到Context.entranceNode的childList列表中,也就是前面创建的EntranceNode对象的childList列表中。
接着,执行ClusterBuilderSlot的entry()方法时,便会创建一个ClusterNode对象,该ClusterNode对象会赋值给前面DefaultNode对象中的clusterNode属性。
至此,便构建完Context下的资源调用树了。Constants.ROOT的childList里会存放多个EntranceNode对象,每个EntranceNode对象的childList里会存放多个DefaultNode对象,而每个DefaultNode对象会指向一个ClusterNode对象。
//This class holds metadata of current invocation:
//the EntranceNode: the root of the current invocation tree.
//the current Entry: the current invocation point.
//the current Node: the statistics related to the Entry.
//the origin: The origin is useful when we want to control different invoker/consumer separately.
//Usually the origin could be the Service Consumer's app name or origin IP.//Each SphU.entry() or SphO.entry() should be in a Context,
//if we don't invoke ContextUtil.enter() explicitly, DEFAULT context will be used.
//A invocation tree will be created if we invoke SphU.entry() multi times in the same context.
//Same resource in different context will count separately, see NodeSelectorSlot.
public class Context {//Context name.private final String name;//The entrance node of current invocation tree.private DefaultNode entranceNode;//Current processing entry.private Entry curEntry;//The origin of this context (usually indicate different invokers, e.g. service consumer name or origin IP).private String origin = "";...public Context(DefaultNode entranceNode, String name) {this(name, entranceNode, false);}public Context(String name, DefaultNode entranceNode, boolean async) {this.name = name;this.entranceNode = entranceNode;this.async = async;}//Get the parent Node of the current.public Node getLastNode() {if (curEntry != null && curEntry.getLastNode() != null) {return curEntry.getLastNode();} else {return entranceNode;}}...
}public class ContextUtil {//以Context的name作为key,EntranceNode作为value缓存所有的EntranceNode到HashMap中private static volatile Map<String, DefaultNode> contextNameNodeMap = new HashMap<>();...protected static Context trueEnter(String name, String origin) {...//从缓存中获取EntranceNodeDefaultNode node = contextNameNodeMap.get(name);...//创建EntranceNode,缓存到contextNameNodeMap当中node = new EntranceNode(new StringResourceWrapper(name, EntryType.IN), null);//将新创建的EntranceNode添加到ROOT中,ROOT就是每个Node的根结点Constants.ROOT.addChild(node);...//初始化Context,将刚获取到或刚创建的EntranceNode放到Context的entranceNode属性中context = new Context(node, name);...}...
}public final class Constants {...//Global ROOT statistic node that represents the universal parent node.public final static DefaultNode ROOT = new EntranceNode(new StringResourceWrapper(ROOT_ID, EntryType.IN),new ClusterNode(ROOT_ID, ResourceTypeConstants.COMMON));...
}//A Node used to hold statistics for specific resource name in the specific context.
//Each distinct resource in each distinct Context will corresponding to a DefaultNode.
//This class may have a list of sub DefaultNodes.
//Child nodes will be created when calling SphU.entry() or SphO.entry() multiple times in the same Context.
public class DefaultNode extends StatisticNode {//The resource associated with the node.private ResourceWrapper id;//The list of all child nodes.private volatile Set<Node> childList = new HashSet<>();//Associated cluster node.private ClusterNode clusterNode;...//Add child node to current node.public void addChild(Node node) {if (node == null) {RecordLog.warn("Trying to add null child to node <{}>, ignored", id.getName());return;}if (!childList.contains(node)) {synchronized (this) {if (!childList.contains(node)) {Set<Node> newSet = new HashSet<>(childList.size() + 1);newSet.addAll(childList);newSet.add(node);childList = newSet;}}RecordLog.info("Add child <{}> to node <{}>", ((DefaultNode)node).id.getName(), id.getName());}}//Reset the child node list.public void removeChildList() {this.childList = new HashSet<>();}...
}@Spi(isSingleton = false, order = Constants.ORDER_NODE_SELECTOR_SLOT)
public class NodeSelectorSlot extends AbstractLinkedProcessorSlot<Object> {//DefaultNodes of the same resource in different context.//缓存map以Context的name为key,DefaultNode为value//由于默认情况下多个线程对应的Context的name都相同,所以多个线程访问资源时使用的DefaultNode也一样private volatile Map<String, DefaultNode> map = new HashMap<String, DefaultNode>(10);...@Overridepublic void entry(Context context, ResourceWrapper resourceWrapper, Object obj, int count, boolean prioritized, Object... args) throws Throwable {...//先从缓存中获取DefaultNode node = map.get(context.getName());...//下面根据ResourceWrapper创建一个DefaultNode对象node = new DefaultNode(resourceWrapper, null);...//Build invocation tree//首先会调用context.getLastNode()方法,获取到的是Context.entranceNode属性即一个EntranceNode对象//EntranceNode对象是在执行ContextUtil.trueEnter()方法创建Context对象实例时添加到Context对象中的//然后会将刚创建的DefaultNode对象添加到EntranceNode对象的childList列表中((DefaultNode) context.getLastNode()).addChild(node);...//执行下一个ProcessorSlotfireEntry(context, resourceWrapper, node, count, prioritized, args);}...
}@Spi(isSingleton = false, order = Constants.ORDER_CLUSTER_BUILDER_SLOT)
public class ClusterBuilderSlot extends AbstractLinkedProcessorSlot<DefaultNode> {...private volatile ClusterNode clusterNode = null;@Overridepublic void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count, boolean prioritized, Object... args) throws Throwable {...//创建ClusterNode对象clusterNode = new ClusterNode(resourceWrapper.getName(), resourceWrapper.getResourceType()); ...//设置DefaultNode的clusterNode属性为获取到的ClusterNode对象node.setClusterNode(clusterNode);...//执行下一个ProcessorSlotfireEntry(context, resourceWrapper, node, count, prioritized, args);}...
}//资源调用树的示例如下所示:
// machine-root
// / \
// / \
// EntranceNode1 EntranceNode2
// / \
// / \
// DefaultNode(nodeA) DefaultNode(nodeA)
// | |
// +- - - - - - - - - - +- - - - - - -> ClusterNode(nodeA);
//其中,machine-root中的childList里会有很多个EntranceNode对象
//EntranceNode对象中的childList里又会有很多个DefaultNode对象
//每个DefaultNode对象下都会指向一个ClusterNode对象
一些对应关系的梳理总结:
一个线程对应一个ResourceWrapper对象实例,一个线程对应一个Context对象实例。如果ResourceWrapper对象相同,则会共用一个ProcessorSlotChain实例。如果ResourceWrapper对象相同,则也会共用一个ClusterNode实例。如果Context对象的名字相同,则会共用一个EntranceNode对象实例。如果Context对象的名字相同,则也会共用一个DefaultNode对象实例。
//每个请求对应一个线程,每个线程绑定一个Context,所以每个请求对应一个Context
private static ThreadLocal<Context> contextHolder = new ThreadLocal<>();//以Context的name作为key,EntranceNode作为value缓存所有的EntranceNode到HashMap中
private static volatile Map<String, EntranceNode> contextNameNodeMap = new HashMap<>();//Same resource will share the same ProcessorSlotChain}, no matter in which Context.
//Same resource is that ResourceWrapper#equals(Object).
private static volatile Map<ResourceWrapper, ProcessorSlotChain> chainMap = new HashMap<ResourceWrapper, ProcessorSlotChain>();//DefaultNodes of the same resource in different context.
//以Context的name作为key,DefaultNode作为value
//由于默认情况下多个线程对应的Context的name都相同,所以多个线程访问资源时使用的DefaultNode也一样
private volatile Map<String, DefaultNode> map = new HashMap<String, DefaultNode>(10);//To get total statistics of the same resource in different context,
//same resource shares the same ClusterNode globally.
//All ClusterNodes are cached in this map.
private static volatile Map<ResourceWrapper, ClusterNode> clusterNodeMap = new HashMap<>();
2.LogSlot和StatisticSlot采集资源的数据
(1)LogSlot的源码
(2)StatisticSlot的源码
(3)记录资源在不同维度下的调用数据
(1)LogSlot的源码
LogSlot用于记录异常请求日志,以便于故障排查。也就是当出现BlockException异常时,调用EagleEyeLogUtil的log()方法将日志写到sentinel-block.log文件中。
//A ProcessorSlot that is response for logging block exceptions to provide concrete logs for troubleshooting.
@Spi(order = Constants.ORDER_LOG_SLOT)
public class LogSlot extends AbstractLinkedProcessorSlot<DefaultNode> {@Overridepublic void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode obj, int count, boolean prioritized, Object... args) throws Throwable {try {//调用下一个ProcessorSlotfireEntry(context, resourceWrapper, obj, count, prioritized, args);} catch (BlockException e) {//被流控或者熔断降级后打印log日志EagleEyeLogUtil.log(resourceWrapper.getName(), e.getClass().getSimpleName(), e.getRuleLimitApp(), context.getOrigin(), e.getRule().getId(), count);throw e;} catch (Throwable e) {RecordLog.warn("Unexpected entry exception", e);}}@Overridepublic void exit(Context context, ResourceWrapper resourceWrapper, int count, Object... args) {try {//调用下一个ProcessorSlotfireExit(context, resourceWrapper, count, args);} catch (Throwable e) {RecordLog.warn("Unexpected entry exit exception", e);}}
}public class EagleEyeLogUtil {public static final String FILE_NAME = "sentinel-block.log";private static StatLogger statLogger;static {String path = LogBase.getLogBaseDir() + FILE_NAME;statLogger = EagleEye.statLoggerBuilder("sentinel-block-log").intervalSeconds(1).entryDelimiter('|').keyDelimiter(',').valueDelimiter(',').maxEntryCount(6000).configLogFilePath(path).maxFileSizeMB(300).maxBackupIndex(3).buildSingleton();}public static void log(String resource, String exceptionName, String ruleLimitApp, String origin, Long ruleId, int count) {String ruleIdString = StringUtil.EMPTY;if (ruleId != null) {ruleIdString = String.valueOf(ruleId);}statLogger.stat(resource, exceptionName, ruleLimitApp, origin, ruleIdString).count(count);}
}
(2)StatisticSlot的源码
StatisticSlot用于统计资源的调用数据,如请求成功数、请求失败数、响应时间等。
注意:开始对请求进行规则验证时,需要调用SphU的entry()方法。完成对请求的规则验证后,也需要调用Entry的exit()方法。
//A processor slot that dedicates to real time statistics.
//When entering this slot, we need to separately count the following information:
//ClusterNode: total statistics of a cluster node of the resource ID.
//Origin node: statistics of a cluster node from different callers/origins.
//DefaultNode: statistics for specific resource name in the specific context.
//Finally, the sum statistics of all entrances.
@Spi(order = Constants.ORDER_STATISTIC_SLOT)
public class StatisticSlot extends AbstractLinkedProcessorSlot<DefaultNode> {@Overridepublic void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count, boolean prioritized, Object... args) throws Throwable {try {//Do some checking.//执行下一个ProcessorSlot,先进行规则验证等fireEntry(context, resourceWrapper, node, count, prioritized, args);//Request passed, add thread count and pass count.//如果通过了后面ProcessorSlot的验证//则将处理当前资源resourceWrapper的线程数 + 1 以及 将对当前资源resourceWrapper的成功请求数 + 1node.increaseThreadNum();node.addPassRequest(count);if (context.getCurEntry().getOriginNode() != null) {//Add count for origin node.context.getCurEntry().getOriginNode().increaseThreadNum();context.getCurEntry().getOriginNode().addPassRequest(count);}if (resourceWrapper.getEntryType() == EntryType.IN) {//Add count for global inbound entry node for global statistics.Constants.ENTRY_NODE.increaseThreadNum();Constants.ENTRY_NODE.addPassRequest(count);}//Handle pass event with registered entry callback handlers.for (ProcessorSlotEntryCallback<DefaultNode> handler : StatisticSlotCallbackRegistry.getEntryCallbacks()) {handler.onPass(context, resourceWrapper, node, count, args);}} catch (PriorityWaitException ex) {node.increaseThreadNum();if (context.getCurEntry().getOriginNode() != null) {//Add count for origin node.context.getCurEntry().getOriginNode().increaseThreadNum();}if (resourceWrapper.getEntryType() == EntryType.IN) {//Add count for global inbound entry node for global statistics.Constants.ENTRY_NODE.increaseThreadNum();}//Handle pass event with registered entry callback handlers.for (ProcessorSlotEntryCallback<DefaultNode> handler : StatisticSlotCallbackRegistry.getEntryCallbacks()) {handler.onPass(context, resourceWrapper, node, count, args);}} catch (BlockException e) {//捕获BlockException//Blocked, set block exception to current entry.context.getCurEntry().setBlockError(e);//Add block count.//如果规则验证失败,则将BlockQps+1node.increaseBlockQps(count);if (context.getCurEntry().getOriginNode() != null) {context.getCurEntry().getOriginNode().increaseBlockQps(count);}if (resourceWrapper.getEntryType() == EntryType.IN) {//Add count for global inbound entry node for global statistics.Constants.ENTRY_NODE.increaseBlockQps(count);}//Handle block event with registered entry callback handlers.for (ProcessorSlotEntryCallback<DefaultNode> handler : StatisticSlotCallbackRegistry.getEntryCallbacks()) {handler.onBlocked(e, context, resourceWrapper, node, count, args);}throw e;} catch (Throwable e) {//Unexpected internal error, set error to current entry.context.getCurEntry().setError(e);throw e;}}//开始对请求进行规则验证时,需要调用SphU.entry()方法//完成对请求的规则验证后,也需要调用Entry.exit()方法@Overridepublic void exit(Context context, ResourceWrapper resourceWrapper, int count, Object... args) {Node node = context.getCurNode();if (context.getCurEntry().getBlockError() == null) {//Calculate response time (use completeStatTime as the time of completion).//获取系统当前时间long completeStatTime = TimeUtil.currentTimeMillis();context.getCurEntry().setCompleteTimestamp(completeStatTime);//计算响应时间 = 系统当前事件 - 根据资源resourceWrapper创建Entry资源访问对象时的时间long rt = completeStatTime - context.getCurEntry().getCreateTimestamp();Throwable error = context.getCurEntry().getError();//Record response time and success count.//记录响应时间等信息recordCompleteFor(node, count, rt, error);recordCompleteFor(context.getCurEntry().getOriginNode(), count, rt, error);if (resourceWrapper.getEntryType() == EntryType.IN) {recordCompleteFor(Constants.ENTRY_NODE, count, rt, error);}}//Handle exit event with registered exit callback handlers.Collection<ProcessorSlotExitCallback> exitCallbacks = StatisticSlotCallbackRegistry.getExitCallbacks();for (ProcessorSlotExitCallback handler : exitCallbacks) {handler.onExit(context, resourceWrapper, count, args);}fireExit(context, resourceWrapper, count, args);}private void recordCompleteFor(Node node, int batchCount, long rt, Throwable error) {if (node == null) {return;}node.addRtAndSuccess(rt, batchCount);node.decreaseThreadNum();if (error != null && !(error instanceof BlockException)) {node.increaseExceptionQps(batchCount);}}
}
(3)记录资源在不同维度下的调用数据
一.如何统计单机里某个资源的调用数据
二.如何统计所有资源的调用数据即接口调用数据
三.如何统计集群中某个资源的调用数据
一.如何统计单机里某个资源的调用数据
由于DefaultNode会统计名字相同的Context下的某个资源的调用数据,它是按照单机里的资源维度进行调用数据统计的,所以在StatisticSlot的entry()方法中,会调用DefaultNode的方法来进行统计。
//A Node used to hold statistics for specific resource name in the specific context.
//Each distinct resource in each distinct Context will corresponding to a DefaultNode.
//This class may have a list of sub DefaultNodes.
//Child nodes will be created when calling SphU.entry() or SphO.entry() multiple times in the same Context.
public class DefaultNode extends StatisticNode {//The resource associated with the node.private ResourceWrapper id;//Associated cluster node.private ClusterNode clusterNode;...@Overridepublic void increaseThreadNum() {super.increaseThreadNum();this.clusterNode.increaseThreadNum();}@Overridepublic void addPassRequest(int count) {super.addPassRequest(count);this.clusterNode.addPassRequest(count);}@Overridepublic void increaseBlockQps(int count) {super.increaseBlockQps(count);this.clusterNode.increaseBlockQps(count);}@Overridepublic void addRtAndSuccess(long rt, int successCount) {super.addRtAndSuccess(rt, successCount);this.clusterNode.addRtAndSuccess(rt, successCount);}@Overridepublic void decreaseThreadNum() {super.decreaseThreadNum();this.clusterNode.decreaseThreadNum();}...
}public class StatisticNode implements Node {//The counter for thread count.private LongAdder curThreadNum = new LongAdder();//Holds statistics of the recent INTERVAL milliseconds. //The INTERVAL is divided into time spans by given sampleCount.private transient volatile Metric rollingCounterInSecond = new ArrayMetric(SampleCountProperty.SAMPLE_COUNT, IntervalProperty.INTERVAL);//Holds statistics of the recent 60 seconds. //The windowLengthInMs is deliberately set to 1000 milliseconds,//meaning each bucket per second, in this way we can get accurate statistics of each second.private transient Metric rollingCounterInMinute = new ArrayMetric(60, 60 * 1000, false);...@Overridepublic void increaseThreadNum() {curThreadNum.increment();}@Overridepublic void addPassRequest(int count) {rollingCounterInSecond.addPass(count);rollingCounterInMinute.addPass(count);}@Overridepublic void increaseBlockQps(int count) {rollingCounterInSecond.addBlock(count);rollingCounterInMinute.addBlock(count);}@Overridepublic void addRtAndSuccess(long rt, int successCount) {rollingCounterInSecond.addSuccess(successCount);rollingCounterInSecond.addRT(rt);rollingCounterInMinute.addSuccess(successCount);rollingCounterInMinute.addRT(rt);}@Overridepublic void decreaseThreadNum() {curThreadNum.decrement();}...
}
二.如何统计所有资源的调用数据即接口调用数据
由于EntranceNode会统计名字相同的Context下的全部资源的调用数据,它是按接口维度来统计调用数据的,即统计接口下所有资源的调用情况,所以可以通过遍历EntranceNode的childList来统计接口的调用数据。
//A Node represents the entrance of the invocation tree.
//One Context will related to a EntranceNode,
//which represents the entrance of the invocation tree.
//New EntranceNode will be created if current context does't have one.
//Note that same context name will share same EntranceNode globally.
public class EntranceNode extends DefaultNode {public EntranceNode(ResourceWrapper id, ClusterNode clusterNode) {super(id, clusterNode);}@Overridepublic double avgRt() {double total = 0;double totalQps = 0;for (Node node : getChildList()) {total += node.avgRt() * node.passQps();totalQps += node.passQps();}return total / (totalQps == 0 ? 1 : totalQps);}@Overridepublic double blockQps() {double blockQps = 0;for (Node node : getChildList()) {blockQps += node.blockQps();}return blockQps;}@Overridepublic long blockRequest() {long r = 0;for (Node node : getChildList()) {r += node.blockRequest();}return r;}@Overridepublic int curThreadNum() {int r = 0;for (Node node : getChildList()) {r += node.curThreadNum();}return r;}@Overridepublic double totalQps() {double r = 0;for (Node node : getChildList()) {r += node.totalQps();}return r;}@Overridepublic double successQps() {double r = 0;for (Node node : getChildList()) {r += node.successQps();}return r;}@Overridepublic double passQps() {double r = 0;for (Node node : getChildList()) {r += node.passQps();}return r;}@Overridepublic long totalRequest() {long r = 0;for (Node node : getChildList()) {r += node.totalRequest();}return r;}@Overridepublic long totalPass() {long r = 0;for (Node node : getChildList()) {r += node.totalPass();}return r;}
}
三.如何统计集群中某个资源的调用数据
由于ClusterNode会统计某个资源在全部Context下的调用数据,它是按照集群中的资源维度进行调用数据统计的,而StatisticSlot的entry()调用DefaultNode的方法统计单机下的资源时,会顺便调用ClusterNode的方法来统计集群下的资源调用,所以通过ClusterNode就可以获取集群中某个资源的调用数据。
//A Node used to hold statistics for specific resource name in the specific context.
//Each distinct resource in each distinct Context will corresponding to a DefaultNode.
//This class may have a list of sub DefaultNodes.
//Child nodes will be created when calling SphU.entry() or SphO.entry() multiple times in the same Context.
public class DefaultNode extends StatisticNode {//The resource associated with the node.private ResourceWrapper id;//Associated cluster node.private ClusterNode clusterNode;...@Overridepublic void increaseThreadNum() {super.increaseThreadNum();this.clusterNode.increaseThreadNum();}@Overridepublic void addPassRequest(int count) {super.addPassRequest(count);this.clusterNode.addPassRequest(count);}@Overridepublic void increaseBlockQps(int count) {super.increaseBlockQps(count);this.clusterNode.increaseBlockQps(count);}@Overridepublic void addRtAndSuccess(long rt, int successCount) {super.addRtAndSuccess(rt, successCount);this.clusterNode.addRtAndSuccess(rt, successCount);}@Overridepublic void decreaseThreadNum() {super.decreaseThreadNum();this.clusterNode.decreaseThreadNum();}...
}
相关文章:
Sentinel源码—3.ProcessorSlot的执行过程一
大纲 1.NodeSelectorSlot构建资源调用树 2.LogSlot和StatisticSlot采集资源的数据 3.Sentinel监听器模式的规则对象与规则管理 4.AuthoritySlot控制黑白名单权限 5.SystemSlot根据系统保护规则进行流控 1.NodeSelectorSlot构建资源调用树 (1)Entry的处理链的执行入口 (2…...
datagrip连接mysql问题5.7.26
1.Case sensitivity: plainmixed, delimitedexac Remote host terminated the handshake. 区分大小写:plain混合,分隔exac 远程主机终止了握手。 原因:usessl 参数用于指定是否使用 SSL(Secure Sockets Layer)加密来保护数据传…...
【电路笔记】-变压器构造
变压器构造 文章目录 变压器构造1、概述2、变压器铁芯的构造3、变压器叠片4、变压器绕组排列5、变压器点定位6、变压器铁芯损耗6.1 磁滞损耗6.2 涡流损耗6.3 铜损耗一个简单的双绕组变压器构造包括每个绕组分别缠绕在一个独立的软铁肢或磁芯上,这提供了必要的磁路。 1、概述 …...
阿里云集群开启debug
1、安装 kubectl Macos brew install kubectl Windows: https://kubernetes.io/zh-cn/docs/tasks/tools/install-kubectl-windows/ 下载后,放到任意目录 2、配置连接信息 mac 将以下内容复制到计算机 $HOME/.kube/config 文件下: windows 不同集…...
继承-C++
继承在我们日常中经常指我们的人伦关系中的父子关系,孩子继承父母的基因、习惯之类的,孩子也会有自己的个性等。然而在我们C计算机语言中的类也存在继承,我们将作为“父亲”的类称为父类,将作为“孩子”的类称为子类,父…...
Java并发-AQS框架原理解析与实现类详解
什么是AQS? AQS(AbstractQueuedSynchronizer)是Java并发包(JUC)的核心基础框架,它为构建锁和同步器提供了高效、灵活的底层支持。本文将从设计原理、核心机制及典型实现类三个维度展开,帮助读者…...
【FFmpeg从入门到精通】第一章-FFmpeg简介
1 FFmpeg的定义 FFmpeg既是一款音视频编解码工具,同时也是一组音视频编解码开发套件,作为编解码开发套件,它为开发者提供了丰富的音视频处理的调用接口。 FFmpeg提供了多种媒体格式的封装和解封装,包括多种音视频编码、多种协议…...
Mac屏幕共享怎么使用?
Mac电脑要实现远程桌面连接到的工功能,可以使用其自带的屏幕共享功能。Mac屏幕共享能从一台Mac电脑远程控制另一台Mac电脑,并且无需下载第三方远程控制软件。下面,将为您介绍Mac远程桌面连接在哪,以及使用方法。 步骤 1. Mac的远…...
探索亮数据Web Unlocker API:让谷歌学术网页科研数据 “触手可及”
本文目录 一、引言二、Web Unlocker API 功能亮点三、Web Unlocker API 实战1.配置网页解锁器2.定位相关数据3.编写代码 四、Web Scraper API技术亮点 五、SERP API技术亮点 六、总结 一、引言 网页数据宛如一座蕴藏着无限价值的宝库,无论是企业洞察市场动态、制定…...
【后端】【python】利用反射器----动态设置装饰器
📘 Python 装饰器进阶指南 一、装饰器本质 ✅ 本质概念 Python 装饰器的本质是 函数嵌套 返回函数,它是对已有函数的增强,不修改原函数代码,使用语法糖 decorator 实现包裹效果。 def my_decorator(func):def wrapper(*args, …...
Oracle 中的 NOAUDIT CREATE SESSION 命令详解
Oracle 中的 NOAUDIT CREATE SESSION 命令详解 NOAUDIT CREATE SESSION 是 Oracle 数据库中用于取消对用户登录会话审计的命令,它与 AUDIT CREATE SESSION 命令相对应。 一、基本语法 NOAUDIT CREATE SESSION [BY user1 [, user2]... | BY [SESSION | ACCESS]] …...
《Chronos: Learning the Language of Time Series》
全文摘要 本文提出了Chronos,一个简单而有效的预训练概率时间序列模型框架。Chronos通过缩放和量化将时间序列值标记化为固定词汇,并利用现有的基于变换器的语言模型架构进行训练。我们在多个公开数据集和合成数据集上预训练了Chronos模型,并…...
git UserInterfaceState.xcuserstate 文件频繁更新
1> 退出 Xcdoe,打开终端(Terminal),进入到你的项目目录下。 2> 在终端键入 git rm --cached <YourProjectName>.xcodeproj/project.xcworkspace/xcuserdata/<YourUsername>.xcuserdatad/UserInterfaceState.x…...
Day92 | 灵神 | 二叉树 路径总和
Day92 | 灵神 | 二叉树 路径总和 112.路径总和 112. 路径总和 - 力扣(LeetCode) 思路: 1.递归函数意义 如果在根节点为t的树中可以找到长度为target的路径就返回true,找不到就返回false 2.参数和返回值 bool tra(TreeNode …...
数据集 handpose_x_plus 3D RGB 三维手势 - 打篮球 场景 play basketball
数据集 handpose 相关项目地址:https://github.com/XIAN-HHappy/handpose_x_plus 样例数据下载地址:数据集handpose-x-plus3DRGB三维手势-打篮球场景playbasketball资源-CSDN文库...
GitLab本地安装指南
当前GitLab的最新版是v17.10,安装地址:https://about.gitlab.com/install/。当然国内也可以安装极狐GitLab版本,极狐GitLab 是 GitLab 中国发行版(JH)。极狐GitLab支持龙蜥,欧拉等国内的操作系统平台。安装…...
云数据库:核心分类、技术优势与创新、应用场景、挑战应对和前沿趋势
李升伟 整理 云数据库技术是云计算与数据库技术融合的产物,它通过云服务模式提供数据库功能,彻底改变了数据的存储、管理和访问方式。以下从核心概念、技术优势、应用场景及挑战等方面展开分析: 一、云数据库的核心分类 按部署模式 托管数…...
算力狂飙时代:解码2024年上海及周边区域IDC市场的三重构局
2025年3月,科智咨询《2024-2025年上海及周边地区IDC市场研究报告》正式发布。报告对上海及周边地区IDC市场发展情况进行全面分析与深入解读。 在长三角地区数字经济蓬勃发展的背景下,上海及周边区域的数据中心产业正迎来深刻转型。随着上海市政府陆续出台…...
循环首差链码的通俗解释
循环首差链码的通俗解释 1. 链码是什么? 链码是一种用数字序列描述图像中物体轮廓的方法。例如,在 4-链码 中: 0 表示向右移动;1 表示向上移动;2 表示向左移动;3 表示向下移动。 假设有一段轮廓的链码为…...
✅ MySQL 事务 MVCC ROLLBACK
🧠 一、MVCC 与可重复读(REPEATABLE READ) 项目内容MVCC 概念多版本并发控制,事务中读到的是开启事务时的数据快照实现机制依赖 Read View trx_id Undo Log 实现版本判断快照读普通 SELECT,使用 MVCC,不…...
信息系统项目管理工程师备考计算类真题讲解四
一、三点估算(PERT) PERT(Program Evaluation and Review Technique):计划评估技术,又称三点估算技术。PERT估算是一种项目管理中用于估算项目工期或成本的方法,以下是其详细介绍: …...
winfrom 查询某字符串 找到它在 richTextbox 的位置 定位 并高亮 并且滚动定位到所查询的字符串所在的行
如图: 代码: //查找关键字private void buttonSearch_Click(object sender, EventArgs e){string searchText textBoxSearch.Text;if (!string.IsNullOrEmpty(searchText)){TextBoxFinds(txtJSON, searchText);TextBoxFinds(txtSQL, searchText);}}//查…...
数据结构学习笔记 :线性表的链式存储详解
目录 单链表 1.1 无头单链表 1.2 有头单链表单向循环链表双链表 3.1 双链表 3.2 双向循环链表总结与对比 一、单链表 1. 无头单链表(Headless Singly Linked List) 定义:链表无头结点,直接由头指针指向第一个数据节点。 特点&…...
MyBatis-Plus 详解:快速上手到深入理解
一、前言 🌟 🧩 MyBatis & MyBatis-Plus 是啥关系? MyBatis 是一个优秀的 ORM 框架(Object Relational Mapping,面向对象关系映射),它让我们可以通过编写 SQL 来操作数据库,同…...
【软件工程大系】净室软件工程
净室软件工程(Cleanroom Software Engineering)是一种以缺陷预防(正确性验证)为核心的软件开发方法,旨在通过严格的工程规范和数学验证,在开发过程中避免缺陷的产生,而非依赖后期的测试和调试。…...
[区块链lab2] 构建具备加密功能的Web服务端
实验目标: 掌握区块链中密码技术的工作原理。在基于Flask框架的服务端中实现哈希算法的加密功能。 实验内容: 构建Flash Web服务器,实现哈希算法、非对称加密算法的加密功能。 实验步骤: 哈希算法的应用:创建hash…...
2025年- H10-Lc117-560.和为K的子数组(子串)--java版
1.题目描述 2.思路 例子1: 3.代码实现 class Solution {public int subarraySum(int[] nums, int k) {// List<Integer> listnew ArrayList<>();// int cnt0;// for(int i0;i<nums.length;i)// {// for(int ji1;j<nums.length;j)// {// …...
肾脏系统触发 “元数据泄漏警报“(蛋白尿+)
肾脏系统触发 "元数据泄漏警报"(蛋白尿) 核心故障模块:肾小球滤过屏障(GlomerularFilter v2.5.0) 漏洞类型:孔径屏障漏洞 电荷屏障校验失败 → 元数据(蛋白质)越界泄漏 …...
摄像头的工作原理与应用摄像头的工作原理与应用
一、摄像头的工作原理与应用 基本概念 V4L2的全称是Video For Linux Two,其实指的是V4L的升级版,是linux系统关于视频设备的内核驱动,同时V4L2也包含Linux系统下关于视频以及音频采集的接口,只需要配合对应的视频采集设备就可以…...
一个由通义千问以及FFmpeg的AVFrame、buffer引起的bug:前面几帧影响后面帧数据
目录 1 问题描述 2 我最开始的代码----错误代码 3 正确的代码 4 为什么前面帧的结果会叠加到了后面帧上----因为ffmpeg新一帧只更新上一帧变化的部分 5 以后不要用通义千问写代码 1 问题描述 某个项目中,需要做人脸马赛克,然后这个是君正的某款芯片…...
MyBatis-动态SQL
MyBatis Plus 作为 MyBatis 的增强工具,简化了 CRUD 操作,但在复杂查询时,仍需使用 MyBatis 的动态 SQL 功能。以下是一些常用的动态标签、用法示例及注意事项: 常用动态标签及用法示例 <if> 标签 用途:条件判…...
顺序表(Arraylist)和链表(Linkedlist)
List List是一个接口,继承自Collection。 从数据结构角度来说,List就是一个线性表,即用n个相同类型的有限序列,可以在此序列中可以进行增删改查操作。 List是接口不能直接实例化,Linkedlist和Arraylist都实现了List…...
【python】django sqlite版本过低怎么办
方法一:下载最新版本 复制上面的内容的链接 在服务器上进行操作 wget https://sqlite.org/2025/sqlite-autoconf-3490100.tar.gz tar -zxvf sqlite-autoconf-3490100.tar.gz cd sqlite-autoconf-3490100 ./configure --prefix/usr/local make && make in…...
解决Dify使用Docker Compose部署中无法通过OpenAI插件等国外大模型厂商的插件访问其API的问题
解决Dify使用Docker Compose部署中无法通过OpenAI插件等国外大模型厂商的插件访问其API的问题 问题描述 在使用Docker Compose部署Dify时,发现无法通过OpenAI等国外大模型厂商的插件访问其API。这主要是因为Docker容器内的网络环境与宿主机不同,导致无…...
【ROS】代价地图
【ROS】代价地图 前言代价地图(Costmap)概述代价地图的参数costmap_common_params.yaml 参数说明costmap_common_params.yaml 示例说明global_costmap.yaml 参数说明global_costmap.yaml 示例说明local_costmap.yaml 参数说明local_costmap.yaml 示例说明…...
Deno 统一 Node 和 npm,既是 JS 运行时,又是包管理器
Deno 是一个现代的、一体化的、零配置的 JavaScript 运行时、工具链,专为 JavaScript 和 TypeScript 开发设计。目前已有数十万开发者在使用 Deno,其代码仓库是 GitHub 上 star 数第二高的 Rust 项目。 Stars 数102620Forks 数5553 主要特点 内置安全性…...
把城市变成智能生命体,智慧城市的神奇进化
智能交通系统的建立与优化 智能交通系统(ITS)是智慧城市建设的核心部分之一,旨在提升交通管理效率和安全性。该系统利用传感器网络、GPS定位技术以及实时数据分析来监控和管理城市中的所有交通流动。例如,通过部署于道路两侧或交…...
青少年编程与数学 02-016 Python数据结构与算法 23课题、分布式算法
青少年编程与数学 02-016 Python数据结构与算法 23课题、分布式算法 课题摘要:一、一致性算法Paxos 算法 二、领导者选举算法Bully 算法 三、分布式锁算法基于 ZooKeeper 的分布式锁 四、分布式事务处理算法两阶段提交(2PC) 五、负载均衡算法最少连接法 …...
Windows10系统RabbitMQ无法访问Web端界面
项目场景: 提示:这里简述项目相关背景: 项目场景: 在一个基于 .NET 的分布式项目中,团队使用 RabbitMQ 作为消息队列中间件,负责模块间的异步通信。开发环境为 Windows 10 系统,开发人员按照官…...
人工智能之数学基础:特征值分解与奇异值分解的区别分析
本文重点 矩阵分解是线性代数的核心工具,广泛应用于数据分析、信号处理、机器学习等领域。特征值分解与奇异值分解在数学定义、适用范围、几何意义、计算方法、应用场景及稳定性方面存在显著差异。EVD 适用于方阵,强调矩阵的固有属性;SVD 适用于任意矩阵,揭示矩阵的内在结…...
UDP概念特点+编程流程
UDP概念编程流程 目录 一、UDP基本概念 1.1 概念 1.2 特点 1.2.1 无连接性: 1.2.2 不可靠性 1.2.3 面向报文 二、UDP编程流程 2.1 客户端 cli.c 2.2 服务端ser.c 一、UDP基本概念 1.1 概念 UDP 即用户数据报协议(User Datagram Protocol &…...
Go语言实现OAuth 2.0认证服务器
文章目录 1. 项目概述1.1 OAuth2 流程 2. OAuth 2.0 Storage接口解析2.1 基础方法2.2 客户端管理相关方法2.3 授权码相关方法2.4 访问令牌相关方法2.5 刷新令牌相关方法 2.6 方法调用时序2.7 关键注意点3. MySQL存储实现原理3.1 数据库设计3.2 核心实现 4. OAuth 2.0授权码流程…...
【版本控制】idea中使用git
大家好,我是jstart千语。接下来继续对git的内容进行讲解。也是在开发中最常使用,最重要的部分,在idea中操作git。目录在右侧哦。 如果需要git命令的详解: 【版本控制】git命令使用大全-CSDN博客 一、配置git 要先关闭项目…...
永磁同步电机控制中,滑模观测器是基于反电动势观测转子速度和角度的?扩展卡尔曼滤波观测器是基于什么观测的?扩展卡尔曼滤波观测器也是基于反电动势吗?
滑模观测器在PMSM中的应用: 滑模观测器是一种非线性观测器,利用切换函数设计,使得状态估计误差迅速趋近于零,实现快速响应和对外部干扰的鲁棒性。 在永磁同步电机(PMSM)无传感器控制中,滑模观测…...
十倍开发效率 - IDEA 插件之RestfulBox - API
提高效率不是为了完成更多的任务,而是有充足的时间摸鱼。 快速体验 RestfulBox - API 是 IDEA 的插件,适合本地测试接口,完全不需要对项目进行任何以来。 接口管理:支持接口扫描、浏览、搜索、跳转、导入和导出。支持接口请求&a…...
HTML、CSS 和 JavaScript 常见用法及使用规范
一、HTML 深度剖析 1. 文档类型声明 HTML 文档开头的 <!DOCTYPE html> 声明告知浏览器当前文档使用的是 HTML5 标准。它是文档的重要元信息,能确保浏览器以标准模式渲染页面,避免怪异模式下的兼容性问题。 2. 元数据标签 <meta> 标签&am…...
人工智能概念股投资:10大潜力标的深度研究
人工智能概念股投资:10大潜力标的深度研究 一、人工智能概念股投资的基本概念 人工智能(Artificial Intelligence,AI)是指利用计算机程序模拟人类智能的一种技术,通过对数据的分析和学习,实现类似人类思维和…...
centos部署的openstack发布windows虚拟机
CentOS上部署的OpenStack可以发布Windows虚拟机。在CentOS上部署OpenStack后,可以通过OpenStack平台创建和管理Windows虚拟机。以下是具体的步骤和注意事项: 安装和配置OpenStack: 首先,确保系统满足OpenStack的最低硬件…...
Fortran 中使用 C_LOC 和 C_F_POINTER 结合的方法来实现不同类型指针指向同一块内存区域
在 Fortran 中,可以使用 C_LOC 和 C_F_POINTER 结合的方法来实现不同类型指针指向同一块内存区域。以下是具体方法和示例: 关键步骤: 获取内存地址:用 C_LOC 获取原始数组的 C 地址。类型转换:用 C_F_POINTER 将地址转…...
两个 STM32G0 I2C 通信异常的案例分析
1. 案例一问题描述 客户反馈其产品在使用 STM32G0C1NEY6TR 和一个充电管理 IC 通信时,速率为100KHz 时通信正常,但工作在 400KHz 时,有时会产生 I2C 错误。 把 I2C GPIO 配置为推挽输出后产生错误的概率会下降。 2. 案例一问题确认 针对客…...