2021-09-03

学nm个锤子 2021-03-07 21:59:49 106 已收藏 1
分类专栏: cloud 文章标签: java 分布式 网络 中间件
版权

cloud
专栏收录该内容
7 篇文章0 订阅
订阅专栏
一,限流的基本认识
限流虽然能够保护系统不被压垮,但是对于被限流的用户,就会很不开心。所以限流其实是一种有损的解决方案。但是相比于全部不可用,有损服务是最好的一种解决办法。

限流的作用
限流的设计还能防止恶意请求流量、恶意攻击

限流的基本原理是通过对并发访问/请求进行限速或者一个时间窗口内的请求进行限速来保护系统,一旦达到限制速率则可以拒绝服务(定向到错误页或者告知资源没有了)、排队或等待(秒杀、下单)、降级(返回兜底数据或默认数据或默认数据,如商品详情页库存默认有货)

常见的限流有:限制总并发数(如数据库连接池、线程池)、限制瞬时并发数(nginx的limit_conn模块,用来限制瞬时并发连接数)、限制时间窗口内的平均速率(如Guava的RateLimiter、nginx的limit_req模块,限制每秒的平均速率);其他的还有限制远程接口调用速率、限制MQ的消费速率。另外还可以根据网络连接数、网络流量、CPU或内存负载等来限流。

有了限流,就意味着在处理高并发的时候多了一种保护机制,不用担心瞬间流量导致系统挂掉或雪崩,最终做到有损服务而不是不服务;但是限流需要评估好,不能乱用,否则一些正常流量出现一些奇怪的问题而导致用户体验很差造成用户流失。

二,常见的限流算法
计数法
计数器法是限流算法里最简单也是最容易实现的一种算法。比如我们规定,对于A接口来说,我们1分钟的访问次数不能超过100个。那么我们可以这么做:在一开始的时候,我们可以设置一个计数器counter,每当一个请求过来的时候,counter就加1,如果counter的值大于100并且该请求与第一个 请求的间隔时间还在1分钟之内,那么说明请求数过多;如果该请求与第一个请求的间隔时间大于1分钟,且counter的值还在限流范围内,那么就重置 counter。

public class CountDemo {

//current time
public long timeStamp = System.currentTimeMillis();
//init counter
public int reqCount = 0;
//int the timeWindow most req
public final int limit = 100;
//time window
public final long timeWindow = 1000 * 60;

public boolean limit() {
    long now = System.currentTimeMillis();
    if (now < timeStamp + timeWindow) {
        //in time window
        reqCount++;
        //check is arrived most req
        return reqCount <= limit;
    }
    //time out reset
    timeStamp = now;
    reqCount = 1;
    return true;
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
滑动时间窗口
发送和接受方都会维护一个数据帧的序列,这个序列被称作窗口。发送方的窗口大小由接受方确定,目的在于控制发送速度,以免接受方的缓存不够大,而导致溢出,同时控制流量也可以避免网络拥塞。下面图中的4,5,6号数据帧已经被发送出去,但是未收到关联的ACK,7,8,9帧则是等待发送。可以看出发送端的窗口大小为6,这是由接受端告知的。此时如果发送端收到4号ACK,则窗口的左边缘向右收缩,窗口的右边缘则向右扩展,此时窗口就向前“滑动了”,即数据帧10也可以被发送。

滑动窗口演示地址

/**

  • @author yhd

  • @createtime 2021/3/7 19:44

  • @description 滑动时间窗口限流实现

  • 假设某个服务最多只能每秒钟处理100个请求,我们可以设置一个1秒钟的滑动时间窗口,

  • 窗口中有10个格子,每个格子100毫秒,每100毫秒移动一次,每次移动都需要记录当前服务请求的次数
    */
    public class SlidingTimeWindow {
    //服务访问次数,可以放在Redis中,实现分布式系统的访问计数
    Long counter = 0L;
    //使用LinkedList来记录滑动窗口的10个格子。
    List slots = new LinkedList();

    @SneakyThrows
    public static void main(String[] args) {
    SlidingTimeWindow timeWindow = new SlidingTimeWindow();

     new Thread(()->{
         timeWindow.doCheck();
     }).start();
    
     for (;;){
         //判断限流标记
         timeWindow.counter++;
         TimeUnit.SECONDS.sleep(2);
     }
    

    }

    @SneakyThrows
    private void doCheck() {
    for (;😉{
    //TODO 尾插
    slots.addLast(counter);
    if (slots.size()>10)
    slots.removeFirst();
    //TODO 修改限流标记为true
    else
    //TODO 修改限流标记为false
    Thread.sleep(100);
    }
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    漏桶
    漏桶算法思路是,不断的往桶里面注水,无论注水的速度是大还是小,水都是按固定的速率往外漏水;如果桶满了,水会溢出;

桶本身具有一个恒定的速率往下漏水,而上方时快时慢的会有水进入桶内。当桶还未满时,上方的水可以加入。一旦水满,上方的水就无法加入。桶满正是算法中的一个关键的触发条件(即流量异常判断成立的条件)。而此条件下如何处理上方流下来的水,有两种方式

在桶满水之后,常见的两种处理方式为:

1)暂时拦截住上方水的向下流动,等待桶中的一部分水漏走后,再放行上方水。

2)溢出的上方水直接抛弃。

特点

漏水的速率是固定的
即使存在突然注水量变大的情况,漏水的速率也是固定的

public class LeakyBucket {
public long timeStamp = System.currentTimeMillis(); // 当前时间
public long capacity; // 桶的容量
public long rate; // 水漏出的速度(每秒系统能处理的请求数)
public long water; // 当前水量(当前累积请求数)

public boolean limit(){
    long now = System.currentTimeMillis();
    // 先执行漏水,
    water = Math.max(0, water - ((now - timeStamp)/1000) * rate);
    timeStamp = now;
    if ((water + 1) < capacity) {
        // 尝试加水,并且水还未满
        water += 1;
        return true;
    }else {
        //水满,拒绝加水
        return false;
    }
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
令牌桶(能够解决突发流量)
令牌桶算法是网络流量整形(Traffic Shaping)和速率限制(Rate Limiting)中最常使用的一种算法。典型情况下,令牌桶算法用来控制发送到网络上的数据的数目,并允许突发数据的发送。

令牌桶算法:在网络传输数据时,为了防止网络阻塞,需要限制流出网络的流量,使流量以比较均匀的速度向外发送。令牌桶算法就实现了这个功能,可控制发送到网络上数据的数目,并允许突发数据的发送。大小固定的令牌桶可自行以恒定的速率源源不断的产生令牌。如果令牌不被消耗,或者被消耗的速度小于产生的速度,令牌就会不断的增多,直到把通填满。后面在产生的令牌就会从桶中溢出。最后桶中可以保存的最大令牌数永远不会超过桶的大小。这意味着,面对瞬时大流量,该算法可以在短时间内请求拿到大量令牌,而且拿令牌的过程并不是消耗很大的事情。

有以下三种情形可能发生:

数据流的速率 等于 令牌流的速率。这种情况下,每个到来的数据包或者请求都能对应一个令牌,然后无延迟地通过队列;

数据流的速率 小于 令牌流的速率。通过队列的数据包或者请求只消耗了一部分令牌,剩下的令牌会在令牌桶里积累下来,直到桶被装满。剩下的令牌可以在突发请求的时候消耗掉。

数据流的速率 大于 令牌流的速率。这意味着桶里的令牌很快就会被耗尽。导致服务中断一段时间,如果数据包或者请求持续到来,将发生丢包或者拒绝响应。

public class TokenBucket {
public long timeStamp = System.currentTimeMillis(); // 当前时间
public long capacity; // 桶的容量
public long rate; // 令牌放入速度
public long tokens; // 当前令牌数量

public boolean grant() {
    long now = System.currentTimeMillis();
    // 先添加令牌
    tokens = Math.min(capacity, tokens + (now - timeStamp) * rate);
    timeStamp = now;
    if (tokens < 1) {
        // 若不到1个令牌,则拒绝
        return false;
    } else {
        // 还有令牌,领取令牌
        tokens -= 1;
        return true;
    }
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
小结
计数器 VS 滑动窗口
计数器算法是最简单的算法,可以看成是滑动窗口的低精度实现。滑动窗口由于需要存储多份的计数器(每一个格子存一份),所以滑动窗口在实现上需要更多的存储空间。也就是说,如果滑动窗口的精度越高,需要的存储空间就越大。

漏桶算法 VS 令牌桶算法
漏桶算法和令牌桶算法最明显的区别是令牌桶算法允许流量一定程度的突发。因为默认的令牌桶算法,取走token是不需要耗费时间的,也就是说,假设桶内有100个token时,那么可以瞬间允当然我们需要具体情况具体分析,只有最合适的算法,没有最优的算法。

三,如何实现分布式限流
假如有这样一个场景,现在把provider部署了10个集群,希望调用这个服务的api的总的qps是100,意味着每一台机器的qps是10,理想情况下总的qps就是100.但是实际上由于负载均衡策略的流量分发并不是非常均匀的,就会导致总的qps不足100时,就被限了。在这个场景中,仅仅依靠单机来实现总体流量的控制是有问题的。所以最好是能实现集群限流。

架构图
要想使用集群流控功能,我们需要在应用端配置动态规则源,并通过 Sentinel 控制台实时进行推送。

四,sentinel限流熔断降级源码分析
从@SentinelResource注解作为切入点,看Sentinel源码

1,SentinelResourceProxy.invokeResourceWithSentinel()
@Around(“sentinelResourceAnnotationPointcut()”)
public Object invokeResourceWithSentinel(ProceedingJoinPoint pjp) throws Throwable {
Method originMethod = resolveMethod(pjp);

    SentinelResource annotation = originMethod.getAnnotation(SentinelResource.class);
    if (annotation == null) {
        // Should not go through here.
        throw new IllegalStateException("Wrong state for SentinelResource annotation");
    }
    String resourceName = getResourceName(annotation.value(), originMethod);
    EntryType entryType = annotation.entryType();
    int resourceType = annotation.resourceType();
    Entry entry = null;
    try {
        //切入点
        entry = SphU.entry(resourceName, resourceType, entryType, pjp.getArgs());
        return pjp.proceed();
    } catch (BlockException ex) {
        return handleBlockException(pjp, annotation, ex);
    } catch (Throwable ex) {
        Class<? extends Throwable>[] exceptionsToIgnore = annotation.exceptionsToIgnore();
        // The ignore list will be checked first.
        if (exceptionsToIgnore.length > 0 && exceptionBelongsTo(ex, exceptionsToIgnore)) {
            throw ex;
        }
        if (exceptionBelongsTo(ex, annotation.exceptionsToTrace())) {
            traceException(ex);
            return handleFallback(pjp, annotation, ex);
        }

        // No fallback function can handle the exception, so throw it out.
        throw ex;
    } finally {
        if (entry != null) {
            entry.exit(1, pjp.getArgs());
        }
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
2,entry()
public static Entry entry(String name, int resourceType, EntryType trafficType, Object[] args)
throws BlockException {
//申请一个entry,如果能够申请成功,则说明没有被限流,否则会抛出BlockException,表示已经被限流了
return Env.sph.entryWithType(name, resourceType, trafficType, 1, args);
}
1
2
3
4
5
3,entryWithType()
public Entry entryWithType(String name, int resourceType, EntryType entryType, int count, Object[] args)
throws BlockException {
return entryWithType(name, resourceType, entryType, count, false, args);
}
1
2
3
4
4,entryWithPriority()
public Entry entryWithType(String name, int resourceType, EntryType entryType, int count, boolean prioritized,
Object[] args) throws BlockException {
StringResourceWrapper resource = new StringResourceWrapper(name, entryType, resourceType);
return entryWithPriority(resource, count, prioritized, args);
}
1
2
3
4
5
5,entryWithPriority()
private Entry entryWithPriority(ResourceWrapper resourceWrapper, int count, boolean prioritized, Object… args)
throws BlockException {
Context context = ContextUtil.getContext();
if (context instanceof NullContext) {

    return new CtEntry(resourceWrapper, null, context);
}

if (context == null) {

    context = InternalContextUtil.internalEnter(Constants.CONTEXT_DEFAULT_NAME);
}


if (!Constants.ON) {
    return new CtEntry(resourceWrapper, null, context);
}

//责任链模式-构造slot链
ProcessorSlot<Object> chain = lookProcessChain(resourceWrapper);


if (chain == null) {
    return new CtEntry(resourceWrapper, null, context);
}

Entry e = new CtEntry(resourceWrapper, chain, context);
try {

    /**
     * 从头到尾调用对应的slot链
     * StatisticSlot 用于存储资源的统计信息及调用者信息,例如该资源的RT,QPS,thread count等等
     * 这些信息将用作为多维度限流,降级的依据(fireEntry用于触发下一个规则的调用)
     * FlowSlot
     */
    chain.entry(context, resourceWrapper, null, count, prioritized, args);
} catch (BlockException e1) {
    e.exit(count, args);
    throw e1;
} catch (Throwable e1) {
}
return e;

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
这个 **entry()**有很多的实现类,其实每一个实现类代表一个slot槽位。

1)StatisticSlot.entry()
StatisticSlot 用于存储资源的统计信息及调用者信息,例如该资源的RT,QPS,thread count等等

@Override
public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count,
boolean prioritized, Object… args) throws Throwable {
try {

    //调用下一个slot
    fireEntry(context, resourceWrapper, node, count, prioritized, args);
    //当前运行线程数+1
    node.increaseThreadNum();
    //每秒钟通过的线程数+1
    //每分钟通过的线程数+1
    node.addPassRequest(count);

    if (context.getCurEntry().getOriginNode() != null) {

        context.getCurEntry().getOriginNode().increaseThreadNum();
        context.getCurEntry().getOriginNode().addPassRequest(count);
    }

    if (resourceWrapper.getEntryType() == EntryType.IN) {

        Constants.ENTRY_NODE.increaseThreadNum();
        Constants.ENTRY_NODE.addPassRequest(count);
    }

    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) {
    // Blocked, set block exception to current entry.
    context.getCurEntry().setBlockError(e);

    // 添加每秒钟被规则限流 线程数
    //添加每分钟被规则限流  线程数
    node.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;
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
为什么异常里面的代码也要看,因为先调用的下一个槽位,然后处理自己的业务逻辑,所以可能是其他槽位抛出异常,如果是其他槽位抛出异常,说明可能被其他槽位处理了,限流了,熔断了,降级了。

①fireEntry()
public void fireEntry(Context context, ResourceWrapper resourceWrapper, Object obj, int count, boolean prioritized, Object… args)
throws Throwable {
if (next != null) {
next.transformEntry(context, resourceWrapper, obj, count, prioritized, args);
}
}
1
2
3
4
5
6
②transformEntry()
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);
}
1
2
3
4
5
6
2)FlowSlot.entry()
FlowSlot 流控规则

public 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);
}
1
2
3
4
5
6
7
①checkFlow()
void checkFlow(ResourceWrapper resource, Context context, DefaultNode node, int count, boolean prioritized)
throws BlockException {
checker.checkFlow(ruleProvider, resource, context, node, count, prioritized);
}
1
2
3
4
②checkFlow()
public void checkFlow(Function<String, Collection> ruleProvider, ResourceWrapper resource,
Context context, DefaultNode node, int count, boolean prioritized) throws BlockException {
if (ruleProvider == null || resource == null) {
return;
}
//获取到内存中的所有规则
Collection 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);
}
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
③canPassCheck()
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);

}
1
2
3
4
5
6
7
8
9
10
11
12
13
④passLocalCheck()
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;
}
//不同的实现对应不同的策略 default ->快速失败(滑动时间窗口算法) warmUP(令牌桶算法) rateLimiter 排队等待(漏桶算法)
return rule.getRater().canPass(selectedNode, acquireCount, prioritized);
}
1
2
3
4
5
6
7
8
9
6,具体流程图

录标题)

欢迎使用Markdown编辑器

你好! 这是你第一次使用 Markdown编辑器 所展示的欢迎页。如果你想学习如何使用Markdown编辑器, 可以仔细阅读这篇文章,了解一下Markdown的基本语法知识。

新的改变

我们对Markdown编辑器进行了一些功能拓展与语法支持,除了标准的Markdown编辑器功能,我们增加了如下几点新功能,帮助你用它写博客:

  1. 全新的界面设计 ,将会带来全新的写作体验;
  2. 在创作中心设置你喜爱的代码高亮样式,Markdown 将代码片显示选择的高亮样式 进行展示;
  3. 增加了 图片拖拽 功能,你可以将本地的图片直接拖拽到编辑区域直接展示;
  4. 全新的 KaTeX数学公式 语法;
  5. 增加了支持甘特图的mermaid语法1 功能;
  6. 增加了 多屏幕编辑 Markdown文章功能;
  7. 增加了 焦点写作模式、预览模式、简洁写作模式、左右区域同步滚轮设置 等功能,功能按钮位于编辑区域与预览区域中间;
  8. 增加了 检查列表 功能。

功能快捷键

撤销:Ctrl/Command + Z
重做:Ctrl/Command + Y
加粗:Ctrl/Command + B
斜体:Ctrl/Command + I
标题:Ctrl/Command + Shift + H
无序列表:Ctrl/Command + Shift + U
有序列表:Ctrl/Command + Shift + O
检查列表:Ctrl/Command + Shift + C
插入代码:Ctrl/Command + Shift + K
插入链接:Ctrl/Command + Shift + L
插入图片:Ctrl/Command + Shift + G
查找:Ctrl/Command + F
替换:Ctrl/Command + G

合理的创建标题,有助于目录的生成

直接输入1次#,并按下space后,将生成1级标题。
输入2次#,并按下space后,将生成2级标题。
以此类推,我们支持6级标题。有助于使用TOC语法后生成一个完美的目录。

如何改变文本的样式

强调文本 强调文本

加粗文本 加粗文本

标记文本

删除文本

引用文本

H2O is是液体。

210 运算结果是 1024.

插入链接与图片

链接: link.

图片: Alt

带尺寸的图片: Alt

居中的图片: Alt

居中并且带尺寸的图片: Alt

当然,我们为了让用户更加便捷,我们增加了图片拖拽功能。

如何插入一段漂亮的代码片

博客设置页面,选择一款你喜欢的代码片高亮样式,下面展示同样高亮的 代码片.

// An highlighted block
var foo = 'bar';

生成一个适合你的列表

  • 项目
    • 项目
      • 项目
  1. 项目1
  2. 项目2
  3. 项目3
  • 计划任务
  • 完成任务

创建一个表格

一个简单的表格是这么创建的:

项目Value
电脑$1600
手机$12
导管$1

设定内容居中、居左、居右

使用:---------:居中
使用:----------居左
使用----------:居右

第一列第二列第三列
第一列文本居中第二列文本居右第三列文本居左

SmartyPants

SmartyPants将ASCII标点字符转换为“智能”印刷标点HTML实体。例如:

TYPEASCIIHTML
Single backticks'Isn't this fun?'‘Isn’t this fun?’
Quotes"Isn't this fun?"“Isn’t this fun?”
Dashes-- is en-dash, --- is em-dash– is en-dash, — is em-dash

创建一个自定义列表

Markdown
Text-to- HTML conversion tool
Authors
John
Luke

如何创建一个注脚

一个具有注脚的文本。2

注释也是必不可少的

Markdown将文本转换为 HTML

KaTeX数学公式

您可以使用渲染LaTeX数学表达式 KaTeX:

Gamma公式展示 Γ ( n ) = ( n − 1 ) ! ∀ n ∈ N \Gamma(n) = (n-1)!\quad\forall n\in\mathbb N Γ(n)=(n1)!nN 是通过欧拉积分

Γ ( z ) = ∫ 0 ∞ t z − 1 e − t d t   . \Gamma(z) = \int_0^\infty t^{z-1}e^{-t}dt\,. Γ(z)=0tz1etdt.

你可以找到更多关于的信息 LaTeX 数学表达式here.

新的甘特图功能,丰富你的文章

Mon 06 Mon 13 Mon 20 已完成 进行中 计划一 计划二 现有任务 Adding GANTT diagram functionality to mermaid
  • 关于 甘特图 语法,参考 这儿,

UML 图表

可以使用UML图表进行渲染。 Mermaid. 例如下面产生的一个序列图:

张三 李四 王五 你好!李四, 最近怎么样? 你最近怎么样,王五? 我很好,谢谢! 我很好,谢谢! 李四想了很长时间, 文字太长了 不适合放在一行. 打量着王五... 很好... 王五, 你怎么样? 张三 李四 王五

这将产生一个流程图。:

链接
长方形
圆角长方形
菱形
  • 关于 Mermaid 语法,参考 这儿,

FLowchart流程图

我们依旧会支持flowchart的流程图:

Created with Raphaël 2.3.0 开始 我的操作 确认? 结束 yes no
  • 关于 Flowchart流程图 语法,参考 这儿.

导出与导入

导出

如果你想尝试使用此编辑器, 你可以在此篇文章任意编辑。当你完成了一篇文章的写作, 在上方工具栏找到 文章导出 ,生成一个.md文件或者.html文件进行本地保存。

导入

如果你想加载一篇你写过的.md文件,在上方工具栏可以选择导入功能进行对应扩展名的文件导入,
继续你的创作。


  1. mermaid语法说明 ↩︎

  2. 注脚的解释 ↩︎

使用python中的pymsql完成如下:表结构与数据创建 1. 建立 `users` 表和 `orders` 表。 `users` 表有用户ID、用户名、年龄字段,(id,name,age) `orders` 表有订单ID、订单日期、订单金额,用户id字段。(id,order_date,amount,user_id) 2 两表的id作为主键,`orders` 表用户id为users的外键 3 插入数据 `users` (1, '张三', 18), (2, '李四', 20), (3, '王五', 22), (4, '赵六', 25), (5, '钱七', 28); `orders` (1, '2021-09-01', 500, 1), (2, '2021-09-02', 1000, 2), (3, '2021-09-03', 600, 3), (4, '2021-09-04', 800, 4), (5, '2021-09-05', 1500, 5), (6, '2021-09-06', 1200, 3), (7, '2021-09-07', 2000, 1), (8, '2021-09-08', 300, 2), (9, '2021-09-09', 700, 5), (10, '2021-09-10', 900, 4); 查询语句 1. 查询订单总金额 2. 查询所有用户的平均年龄,并将结果四舍五入保留两位小数。 3. 查询订单总数最多的用户的姓名和订单总数。 4. 查询所有不重复的年龄。 5. 查询订单日期在2021年9月1日至9月4日之间的订单总金额。 6. 查询年龄不大于25岁的用户的订单数量,并按照降序排序。 7. 查询订单总金额排名前3的用户的姓名和订单总金额。 8. 查询订单总金额最大的用户的姓名和订单总金额。 9. 查询订单总金额最小的用户的姓名和订单总金额。 10. 查询所有名字中含有“李”的用户,按照名字升序排序。 11. 查询所有年龄大于20岁的用户,按照年龄降序排序,并只显示前5条记录。 12. 查询每个用户的订单数量和订单总金额,并按照总金额降序排序。
06-03
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值