【源码解析】流控框架Sentinel源码深度解析

前言

前面写了一篇Sentinel的源码解析,主要侧重点在于Sentinel流程的运转原理。流控框架Sentinel源码解析,侧重点在整个流程。该篇文章将对里面的细节做深入剖析。

统计数据

StatisticSlot用来统计节点访问次数

@SpiOrder(-7000)
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.fireEntry(context, resourceWrapper, node, count, prioritized, args);// Request passed, add thread count and pass count.node.increaseThreadNum();node.addPassRequest(count);//...} catch (Throwable e) {// Unexpected internal error, set error to current entry.context.getCurEntry().setError(e);throw e;}}

StatisticNode#addPassRequest

    private transient volatile Metric rollingCounterInSecond = new ArrayMetric(SampleCountProperty.SAMPLE_COUNT,IntervalProperty.INTERVAL);@Overridepublic void addPassRequest(int count) {rollingCounterInSecond.addPass(count);rollingCounterInMinute.addPass(count);}

ArrayMetric#addPassOccupiableBucketLeapArray获取当前窗口,窗口中的请求数据增长。

     public ArrayMetric(int sampleCount, int intervalInMs) {this.data = new OccupiableBucketLeapArray(sampleCount, intervalInMs);}@Overridepublic void addPass(int count) {WindowWrap<MetricBucket> wrap = data.currentWindow();wrap.value().addPass(count);}

MetricBucket#addPass,对应事件请求数增加。

    public void addPass(int n) {add(MetricEvent.PASS, n);}public MetricBucket add(MetricEvent event, long n) {counters[event.ordinal()].add(n);return this;}

LeapArraysampleCount定义了窗口数,数组大小;intervalInMs定义了统计的时间间隔,windowLengthInMs定义了每一个窗口的时间间隔。

    public LeapArray(int sampleCount, int intervalInMs) {AssertUtil.isTrue(sampleCount > 0, "bucket count is invalid: " + sampleCount);AssertUtil.isTrue(intervalInMs > 0, "total time interval of the sliding window should be positive");AssertUtil.isTrue(intervalInMs % sampleCount == 0, "time span needs to be evenly divided");this.windowLengthInMs = intervalInMs / sampleCount;this.intervalInMs = intervalInMs;this.sampleCount = sampleCount;this.array = new AtomicReferenceArray<>(sampleCount);}

LeapArray#currentWindow(),计算索引值和对应窗口的开始时间。如果可以根据索引值获取不到窗口,新建窗口;如果获取到了窗口,判断是否已经过期,过期就更新开始时间,请求数重置;如果获取到了窗口,且判断未过期,返回。

    public WindowWrap<T> currentWindow() {return currentWindow(TimeUtil.currentTimeMillis());}public WindowWrap<T> currentWindow(long timeMillis) {if (timeMillis < 0) {return null;}int idx = calculateTimeIdx(timeMillis);// Calculate current bucket start time.long windowStart = calculateWindowStart(timeMillis);while (true) {WindowWrap<T> old = array.get(idx);if (old == null) {WindowWrap<T> window = new WindowWrap<T>(windowLengthInMs, windowStart, newEmptyBucket(timeMillis));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()) {return old;} else if (windowStart > old.windowStart()) {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));}}}private int calculateTimeIdx(/*@Valid*/ long timeMillis) {long timeId = timeMillis / windowLengthInMs;// Calculate current index so we can map the timestamp to the leap array.return (int)(timeId % array.length());}protected long calculateWindowStart(/*@Valid*/ long timeMillis) {return timeMillis - timeMillis % windowLengthInMs;}

StatisticNode#passQps,计算通过的QPS,通过数量除以时间间隔。

    @Overridepublic double passQps() {return rollingCounterInSecond.pass() / rollingCounterInSecond.getWindowIntervalInSec();}

ArrayMetric#pass,过滤掉失效的窗口

    @Overridepublic long pass() {data.currentWindow();long pass = 0;List<MetricBucket> list = data.values();for (MetricBucket window : list) {pass += window.pass();}return pass;}

LeapArray#values(),获取所有生效的窗口数据,求和。

    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);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;}

流控

FlowSlot用于流控管理

@SpiOrder(-2000)
public class FlowSlot extends AbstractLinkedProcessorSlot<DefaultNode> {private final FlowRuleChecker checker;public FlowSlot() {this(new FlowRuleChecker());}@Overridepublic void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count,boolean prioritized, Object... args) throws Throwable {checkFlow(resourceWrapper, context, node, count, prioritized);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);}private final Function<String, Collection<FlowRule>> ruleProvider = new Function<String, Collection<FlowRule>>() {@Overridepublic Collection<FlowRule> apply(String resource) {// Flow rule map should not be null.Map<String, List<FlowRule>> flowRules = FlowRuleManager.getFlowRuleMap();return flowRules.get(resource);}};
}

FlowRuleChecker#checkFlow,获取到的FlowRule不为空,挨个判断。获取FlowRuleTrafficShapingController,进行初始化。

public class FlowRuleChecker {public void checkFlow(Function<String, Collection<FlowRule>> ruleProvider, ResourceWrapper resource,Context context, DefaultNode node, int count, boolean prioritized) throws BlockException {if (ruleProvider == null || resource == null) {return;}Collection<FlowRule> rules = ruleProvider.apply(resource.getName());if (rules != null) {for (FlowRule rule : rules) {if (!canPassCheck(rule, context, node, count, prioritized)) {throw new FlowException(rule.getLimitApp(), rule);}}}}public boolean canPassCheck(/*@NonNull*/ FlowRule rule, Context context, DefaultNode node, int acquireCount,boolean prioritized) {String limitApp = rule.getLimitApp();if (limitApp == null) {return true;}if (rule.isClusterMode()) {return passClusterCheck(rule, context, node, acquireCount, prioritized);}return passLocalCheck(rule, context, node, acquireCount, prioritized);}private static boolean passLocalCheck(FlowRule rule, Context context, DefaultNode node, int acquireCount,boolean prioritized) {Node selectedNode = selectNodeByRequesterAndStrategy(rule, context, node);if (selectedNode == null) {return true;}return rule.getRater().canPass(selectedNode, acquireCount, prioritized);}
}

FlowRuleUtil#generateRater,根据rule生成对应的TrafficShapingController

    private static TrafficShapingController generateRater(/*@Valid*/ FlowRule rule) {if (rule.getGrade() == RuleConstant.FLOW_GRADE_QPS) {switch (rule.getControlBehavior()) {case RuleConstant.CONTROL_BEHAVIOR_WARM_UP:return new WarmUpController(rule.getCount(), rule.getWarmUpPeriodSec(),ColdFactorProperty.coldFactor);case RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER:return new RateLimiterController(rule.getMaxQueueingTimeMs(), rule.getCount());case RuleConstant.CONTROL_BEHAVIOR_WARM_UP_RATE_LIMITER:return new WarmUpRateLimiterController(rule.getCount(), rule.getWarmUpPeriodSec(),rule.getMaxQueueingTimeMs(), ColdFactorProperty.coldFactor);case RuleConstant.CONTROL_BEHAVIOR_DEFAULT:default:// Default mode or unknown mode: default traffic shaping controller (fast-reject).}}return new DefaultController(rule.getCount(), rule.getGrade());}

DefaultController#canPass(Node, int),当前数量加请求数量大于规则配置的数量,返回false。

    @Overridepublic boolean canPass(Node node, int acquireCount) {return canPass(node, acquireCount, false);}@Overridepublic boolean canPass(Node node, int acquireCount, boolean prioritized) {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;}private int avgUsedTokens(Node node) {if (node == null) {return DEFAULT_AVG_USED_TOKENS;}return grade == RuleConstant.FLOW_GRADE_THREAD ? node.curThreadNum() : (int)(node.passQps());}

针对于突发大流量情况下可能存在把系统压垮而设计的限流。通过预热,让流量缓慢增加。设想一下,在一段时间内运输中,系统长期处于低水位的情况下,当流量突然增加时,会直接把系统拉升到高水位,并有可能瞬间把系统压垮。但通过冷启动,可以让流量缓慢增加,在一定时间内逐渐增加到阈值上限,给冷系统一个预热的时间,避免冷系统被压垮。

WarmUpController,当超过warningToken,进行预热;不在预热阶段,直接判断passQps + acquireCount <= count。令牌桶算法,每通过一个请求,就会从令牌桶中取走一个令牌。当令牌桶中的令牌达到最大值的时候,意味着系统目前处于最冷阶段,因为桶里的令牌始终处于一个非常饱和的状态。(有点绕,需要多看几遍)

    public WarmUpController(double count, int warmUpPeriodInSec, int coldFactor) {construct(count, warmUpPeriodInSec, coldFactor);}public WarmUpController(double count, int warmUpPeriodInSec) {construct(count, warmUpPeriodInSec, 3);}private void construct(double count, int warmUpPeriodInSec, int coldFactor) {if (coldFactor <= 1) {throw new IllegalArgumentException("Cold factor should be larger than 1");}this.count = count;this.coldFactor = coldFactor;// thresholdPermits = 0.5 * warmupPeriod / stableInterval.// warningToken = 100;warningToken = (int)(warmUpPeriodInSec * count) / (coldFactor - 1);// / maxPermits = thresholdPermits + 2 * warmupPeriod /// (stableInterval + coldInterval)// maxToken = 200maxToken = warningToken + (int)(2 * warmUpPeriodInSec * count / (1.0 + coldFactor));// slope// slope = (coldIntervalMicros - stableIntervalMicros) / (maxPermits// - thresholdPermits);slope = (coldFactor - 1.0) / count / (maxToken - warningToken);}@Overridepublic boolean canPass(Node node, int acquireCount) {return canPass(node, acquireCount, false);}@Overridepublic boolean canPass(Node node, int acquireCount, boolean prioritized) {long passQps = (long) node.passQps();long previousQps = (long) node.previousPassQps();syncToken(previousQps);// 开始计算它的斜率// 如果进入了警戒线,开始调整他的qpslong restToken = storedTokens.get();if (restToken >= warningToken) {long aboveToken = restToken - warningToken;// 消耗的速度要比warning快,但是要比慢// current interval = restToken*slope+1/countdouble warningQps = Math.nextUp(1.0 / (aboveToken * slope + 1.0 / count));if (passQps + acquireCount <= warningQps) {return true;}} else {if (passQps + acquireCount <= count) {return true;}}return false;}protected void syncToken(long passQps) {long currentTime = TimeUtil.currentTimeMillis();currentTime = currentTime - currentTime % 1000;long oldLastFillTime = lastFilledTime.get();if (currentTime <= oldLastFillTime) {return;}long oldValue = storedTokens.get();long newValue = coolDownTokens(currentTime, passQps);if (storedTokens.compareAndSet(oldValue, newValue)) {long currentValue = storedTokens.addAndGet(0 - passQps);if (currentValue < 0) {storedTokens.set(0L);}lastFilledTime.set(currentTime);}}private long coolDownTokens(long currentTime, long passQps) {long oldValue = storedTokens.get();long newValue = oldValue;// 添加令牌的判断前提条件:// 当令牌的消耗程度远远低于警戒线的时候if (oldValue < warningToken) {newValue = (long)(oldValue + (currentTime - lastFilledTime.get()) * count / 1000);} else if (oldValue > warningToken) {if (passQps < (int)count / coldFactor) {newValue = (long)(oldValue + (currentTime - lastFilledTime.get()) * count / 1000);}}return Math.min(newValue, maxToken);}

RateLimiterController,记录两个请求之间允许通过的最小时间。获取最近一次请求的时间,判断当前时间减去最近一次请求的时间是否大于两个请求的最小间隔时间,大于就放行,否则就进入睡眠等待。

public class RateLimiterController implements TrafficShapingController {private final int maxQueueingTimeMs;private final double count;private final AtomicLong latestPassedTime = new AtomicLong(-1);public RateLimiterController(int timeOut, double count) {this.maxQueueingTimeMs = timeOut;this.count = count;}@Overridepublic boolean canPass(Node node, int acquireCount) {return canPass(node, acquireCount, false);}@Overridepublic boolean canPass(Node node, int acquireCount, boolean prioritized) {// Pass when acquire count is less or equal than 0.if (acquireCount <= 0) {return true;}// Reject when count is less or equal than 0.// Otherwise,the costTime will be max of long and waitTime will overflow in some cases.if (count <= 0) {return false;}long currentTime = TimeUtil.currentTimeMillis();// Calculate the interval between every two requests.long costTime = Math.round(1.0 * (acquireCount) / count * 1000);// Expected pass time of this request.long expectedTime = costTime + latestPassedTime.get();if (expectedTime <= currentTime) {// Contention may exist here, but it's okay.latestPassedTime.set(currentTime);return true;} else {// Calculate the time to wait.long waitTime = costTime + latestPassedTime.get() - TimeUtil.currentTimeMillis();if (waitTime > maxQueueingTimeMs) {return false;} else {long oldTime = latestPassedTime.addAndGet(costTime);try {waitTime = oldTime - TimeUtil.currentTimeMillis();if (waitTime > maxQueueingTimeMs) {latestPassedTime.addAndGet(-costTime);return false;}// in race condition waitTime may <= 0if (waitTime > 0) {Thread.sleep(waitTime);}return true;} catch (InterruptedException e) {}}}return false;}}

降级

熔断状态有以下三种:

状态说明
OPEN熔断开启状态,拒绝所有请求
HALF_OPEN半开状态,如果接下来的一个请求顺利通过则表示结束熔断,否则继续熔断
CLOSE熔断关闭状态,请求顺利通过

DegradeSlot获取CircuitBreaker集合,挨个执行CircuitBreaker#tryPass,放行通过后会执行CircuitBreaker#onRequestComplete

@SpiOrder(-1000)
public class DegradeSlot extends AbstractLinkedProcessorSlot<DefaultNode> {@Overridepublic void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count,boolean prioritized, Object... args) throws Throwable {performChecking(context, resourceWrapper);fireEntry(context, resourceWrapper, node, count, prioritized, args);}void performChecking(Context context, ResourceWrapper r) throws BlockException {List<CircuitBreaker> circuitBreakers = DegradeRuleManager.getCircuitBreakers(r.getName());if (circuitBreakers == null || circuitBreakers.isEmpty()) {return;}for (CircuitBreaker cb : circuitBreakers) {if (!cb.tryPass(context)) {throw new DegradeException(cb.getRule().getLimitApp(), cb.getRule());}}}@Overridepublic void exit(Context context, ResourceWrapper r, int count, Object... args) {Entry curEntry = context.getCurEntry();if (curEntry.getBlockError() != null) {fireExit(context, r, count, args);return;}List<CircuitBreaker> circuitBreakers = DegradeRuleManager.getCircuitBreakers(r.getName());if (circuitBreakers == null || circuitBreakers.isEmpty()) {fireExit(context, r, count, args);return;}if (curEntry.getBlockError() == null) {// passed requestfor (CircuitBreaker circuitBreaker : circuitBreakers) {circuitBreaker.onRequestComplete(context);}}fireExit(context, r, count, args);}
}

DegradeRuleManager#newCircuitBreakerFrom,根据DegradeRule生成对应的CircuitBreaker

    private static CircuitBreaker newCircuitBreakerFrom(/*@Valid*/ DegradeRule rule) {switch (rule.getGrade()) {case RuleConstant.DEGRADE_GRADE_RT:return new ResponseTimeCircuitBreaker(rule);case RuleConstant.DEGRADE_GRADE_EXCEPTION_RATIO:case RuleConstant.DEGRADE_GRADE_EXCEPTION_COUNT:return new ExceptionCircuitBreaker(rule);default:return null;}}

AbstractCircuitBreaker#tryPass,如果当前状态是关闭,放行;如果是半开,拒绝请求;如果是开启状态,过了重试时间就修改为半开状态。

    @Overridepublic boolean tryPass(Context context) {// Template implementation.if (currentState.get() == State.CLOSED) {return true;}if (currentState.get() == State.OPEN) {// For half-open state we allow a request for probing.return retryTimeoutArrived() && fromOpenToHalfOpen(context);}return false;}protected boolean retryTimeoutArrived() {return TimeUtil.currentTimeMillis() >= nextRetryTimestamp;}protected void updateNextRetryTimestamp() {this.nextRetryTimestamp = TimeUtil.currentTimeMillis() + recoveryTimeoutMs;}protected boolean fromCloseToOpen(double snapshotValue) {State prev = State.CLOSED;if (currentState.compareAndSet(prev, State.OPEN)) {updateNextRetryTimestamp();notifyObservers(prev, State.OPEN, snapshotValue);return true;}return false;}protected boolean fromOpenToHalfOpen(Context context) {if (currentState.compareAndSet(State.OPEN, State.HALF_OPEN)) {notifyObservers(State.OPEN, State.HALF_OPEN, null);Entry entry = context.getCurEntry();entry.whenTerminate(new BiConsumer<Context, Entry>() {@Overridepublic void accept(Context context, Entry entry) {// Note: This works as a temporary workaround for https://github.com/alibaba/Sentinel/issues/1638// Without the hook, the circuit breaker won't recover from half-open state in some circumstances// when the request is actually blocked by upcoming rules (not only degrade rules).if (entry.getBlockError() != null) {// Fallback to OPEN due to detecting request is blockedcurrentState.compareAndSet(State.HALF_OPEN, State.OPEN);notifyObservers(State.HALF_OPEN, State.OPEN, 1.0d);}}});return true;}return false;}

ResponseTimeCircuitBreaker#onRequestComplete,当前状态是开启,不做处理;当前状态是半开,判断是否是慢请求,如果是慢请求,转到开启状态;如果不是慢请求,转到关闭状态。如果当前状态是关闭,计算比例,如果超过最大的慢请求比例,转到开启状态。

    @Overridepublic void onRequestComplete(Context context) {SlowRequestCounter counter = slidingCounter.currentWindow().value();Entry entry = context.getCurEntry();if (entry == null) {return;}long completeTime = entry.getCompleteTimestamp();if (completeTime <= 0) {completeTime = TimeUtil.currentTimeMillis();}long rt = completeTime - entry.getCreateTimestamp();if (rt > maxAllowedRt) {counter.slowCount.add(1);}counter.totalCount.add(1);handleStateChangeWhenThresholdExceeded(rt);}private void handleStateChangeWhenThresholdExceeded(long rt) {if (currentState.get() == State.OPEN) {return;}if (currentState.get() == State.HALF_OPEN) {// In detecting request// TODO: improve logic for half-open recoveryif (rt > maxAllowedRt) {fromHalfOpenToOpen(1.0d);} else {fromHalfOpenToClose();}return;}List<SlowRequestCounter> counters = slidingCounter.values();long slowCount = 0;long totalCount = 0;for (SlowRequestCounter counter : counters) {slowCount += counter.slowCount.sum();totalCount += counter.totalCount.sum();}if (totalCount < minRequestAmount) {return;}double currentRatio = slowCount * 1.0d / totalCount;if (currentRatio > maxSlowRequestRatio) {transformToOpen(currentRatio);}}

在这里插入图片描述

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

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

相关文章

chatgpt赋能python:Python中的矩阵转置操作

Python中的矩阵转置操作 在Python中&#xff0c;矩阵的转置是十分常见且重要的操作。有许多的情况下&#xff0c;我们需要对一个矩阵进行转置操作。本文将介绍Python语言中的矩阵转置操作以及如何在Python中实现这个操作。 什么是矩阵转置&#xff1f; 矩阵是一种常用的数学…

ChatGPT + Midjourney + 闲鱼,能赚钱吗?

最近天天在朋友群内看到朋友接单&#xff08;帮人调试代码&#xff09;&#xff0c;轻轻松松半小时就赚200-300&#xff0c;今天晚上实在忍不住&#xff0c;产生一个想法&#xff1a;把闲鱼搞起来&#xff0c;怎么做&#xff1f; 手把手教你&#xff1a; 1、怎么在 ChatGPT 招收…

直播带货源码的核心功能以及对直播源码的选择技巧

&#xff08;一&#xff09;直播带货源码的核心功能 如今的风口是啥?很显然&#xff0c;就是直播行业。直播带货系统的风潮最近突然就刮了起来&#xff0c;不但有现场感&#xff0c;还能通过弹幕互动&#xff0c;让你不知不觉就冲动消费了。 本文就来简单分析下&#xff0c;…

你肯定不知道Java 对象不再使用时,为什么要赋值为 null ?

先替老大插播一条招聘&#xff1a;有没有在看机会或者准备看机会的同学&#xff0c;淘系闲鱼技术部急需 p6-p7 的前后端&#xff0c;flutter Dart Faas一体化的践行者。base 杭州&#xff0c;面试通过可年后入职&#xff0c;感兴趣的私聊我微信&#xff1a;xttblog 前言 许多J…

基于微信小程序的二手闲置跳蚤市场交易平台 uni-app

随着我国经济迅速发展,人们对手机的需求越来越大,各种手机软件也都在被广泛应用,但是对于手机进行数据信息管理,对于手机的各种软件也是备受用户的喜爱,跳蚤市场小程序被用户普遍使用,为方便用户能够可以随时进行跳蚤市场小程序的数据信息管理,特开发了基于跳蚤市场小程序的管理…

零成本、零流量,我是如何空手反套白狼?

我大学的时候&#xff0c;在网上看到一篇文章&#xff0c;好像叫「钱不亦痴」啥的&#xff0c;里面讲了很多创业思维和方法&#xff0c;我室友用这个方法&#xff0c;短短两年就买了房&#xff0c;我现在也在学习里面的知识&#xff0c;真的是颠覆我的认知&#xff0c;想看的朋…

旧手机这桩生意 爱回收、转转和闲鱼谁做的更好?

iPhone 7正式发布之后&#xff0c;佳佳&#xff08;化名&#xff09;在爱回收上估价的一部iPhone 6 Plus&#xff08;64G&#xff09;整整下降300元变成了3450元。 据《中国企业家》获得的数据&#xff0c;9月18日当天&#xff0c;2.5万个回收订单涌向爱回收&#xff0c;在三天…

运营新人也可以做的副业平台丨闲鱼运营(下)

文章上半部分给大家深度分享了关于闲鱼的赚钱方法和店铺权重建设该如何提升&#xff0c;如果没有看上半部分内容&#xff0c;直接翻阅主页就可以查阅到&#xff0c;接下来我们分享关于闲鱼运营的下半部分&#xff08;店铺运营&#xff09; 闲鱼何运营总共分为三个部分 第一个部…

闲鱼鱼塘引流什么意思?大家明白其中的技巧吗?

闲鱼鱼塘引流什么意思&#xff1f;大家明白其中的技巧吗&#xff1f; 闲鱼鱼塘一直受很多朋友的关注&#xff0c;主要是因为通过鱼塘&#xff0c;很多朋友可以看到附近二手产品&#xff0c;进而购买到自己心仪的&#xff0c;那怎么通过闲鱼引流呢&#xff0c;今天我给大家总结…

淘宝客如何通过闲鱼引流?如何抓住用户眼球实现精准引流?

淘宝客如何通过闲鱼引流&#xff1f;如何抓住用户眼球实现精准引流&#xff1f; 大家一直都知道淘宝有一个二手交易平台叫闲鱼&#xff0c;很多的朋友会在上面花很少的钱淘到很划算的物品。而淘宝客对于淘宝内部流量一直都很关注&#xff0c;他们会用各种方式为店铺做推广&…

阿里技术分享:闲鱼IM基于Flutter的移动端跨端改造实践

本文由阿里闲鱼技术团队祈晴分享&#xff0c;本次有修订和改动&#xff0c;感谢作者的技术分享。 1、内容概述 本文总结了阿里闲鱼技术团队使用Flutter在对闲鱼IM进行移动端跨端改造过程中的技术实践等&#xff0c;文中对比了传统Native与现在大热的Flutter跨端方案在一些主要…

闲鱼如何高效承接并处理用户纠纷

背景 闲鱼是一个基于C2C场景的闲置交易平台&#xff0c;每个用户既是买家也是卖家&#xff0c;在自由享受交易乐趣的同时也容易带来一些问题&#xff0c;如发一些侵权违规商品而不自知&#xff0c;发一些带情绪化言语对他人照成了伤害等,因此这也带来了一个核心问题&#xff1…

拆解闲鱼无货源盈利模式,需要注意的细节太多?

一.为什么要做闲鱼项目&#xff1f; &#xff08;更多精彩干货请关注共众号&#xff1a;萤火宠&#xff09; 闲鱼目前是阿里旗下的、全国最大的二手交易平台。用户以80后、90后为主&#xff0c;女性多于男性&#xff0c;学生、宝妈及上班族居多&#xff0c;不仅用户数量庞大&…

chatgpt赋能python:Python中的英文单词

Python中的英文单词 Python是一种流行的编程语言&#xff0c;它具有人类易读性、功能强大、支持多种编程范例等特点。Python中包含着大量的英文单词&#xff0c;这些单词在Python编程中极为重要&#xff0c;因为它们直接影响代码的可读性和理解难度。本文将介绍一些最常用的Py…

如何在PPT中自动同时播放两个视频

嵌入视频 视频工具中选择自动播放 动画中第二个视频选择和上一动画同时

如何在多个视频画面的任意位置上添加上同一张图片

现在大家都会做视频&#xff0c;在视频画面上添加一张图片&#xff0c;会让作品一半是视频一半是图片&#xff0c;那这种效果的视频要如何快速的制作呢&#xff1f;下面就随小编一起用视频剪辑高手来操作试试。 准备几个相同格式的视频及尺寸相应的图片保存在同一文件夹中 在电…

视频合并技巧,如何将多个视频合并在一起

视频合并的方法有很多&#xff0c;可以直接使用剪辑工具合并&#xff0c;也可以借助一些剪辑工具。这里将视频合并的方法分享出来&#xff0c;有需要的可以参考试试哦。 推荐使用这个【媒体梦工厂】在浏览器中搜索即可下载安装到电脑上&#xff0c;每一次使用先注册再登录。 …

如何在每段视频的同一个画面中添加镜像播放效果

刷视频时&#xff0c;会看到视频中有这样的效果&#xff0c;视频画面以中间为轴线左右对称的效果&#xff0c;画面中间会被一条竖线分成左右或者上下对称且完全一样的画面&#xff0c;也叫镜像效果&#xff0c;那么该如何实现呢&#xff1f;下面一起来试试。 需要哪些工具&…

剪辑技巧,将视频放在另一个视频画面上同时播放

有时候&#xff0c;我们想要将视频放在同一个画面中&#xff0c;而且能同时播放的那种&#xff0c;不知道怎么操作&#xff0c;那么其实想要实现也不是很难&#xff0c;下面随小编一起来试试这个新的技巧。 需要哪些工具&#xff1f; 一台电脑 视频素材 怎么快速剪辑&#xff…

剪辑小技巧,把两个不同视频合成在同一屏幕播放

我们偶尔会看到一些视频&#xff0c;一个屏幕里同时播放着两个不同的视频&#xff0c;好奇这是如何做到的&#xff1f;于是就上网搜索到媒体梦工厂&#xff0c;发现画中画功能可以合成&#xff0c;下面来分享一下给大家。 准备工具&#xff1a; 小编是到电脑软件站中&#xff0…