开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

架构师如何讲解Redis限流——滑动窗口限流

发表于 2021-10-06

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

本文已参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金。

1、需求

限定用户的某个行为在指定时间T内,只允许发生N次。假设T为1秒钟,N为1000次。

2、常见的错误设计

程序员设计了一个在每分钟内只允许访问1000次的限流方案,如下图01:00s-02:00s之间只允许访问1000次,这种设计最大的问题在于,请求可能在01:59s-02:00s之间被请求1000次,02:00s-02:01s之间被请求了1000次,这种情况下01:59s-02:01s间隔0.02s之间被请求2000次,很显然这种设计是错误的。 错误限流.png

3、滑动窗口算法

3.1 解决方案

指定时间T内,只允许发生N次。我们可以将这个指定时间T,看成一个滑动时间窗口(定宽)。我们采用Redis的zset基本数据类型的score来圈出这个滑动时间窗口。在实际操作zset的过程中,我们只需要保留在这个滑动时间窗口以内的数据,其他的数据不处理即可。

  • 每个用户的行为采用一个zset存储,score为毫秒时间戳,value也使用毫秒时间戳(比UUID更加节省内存)
  • 只保留滑动窗口时间内的行为记录,如果zset为空,则移除zset,不再占用内存(节省内存)

zset滑动窗口限流.png ​

3.2 pipeline代码实现

代码的实现的逻辑是统计滑动窗口内zset中的行为数量,并且与阈值maxCount直接进行比较就可以判断当前行为是否被允许。这里涉及多个redis操作,因此使用pipeline可以大大提升效率

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
typescript复制代码package com.lizba.redis.limit;

import redis.clients.jedis.Jedis;
import redis.clients.jedis.Pipeline;
import redis.clients.jedis.Response;

/**
 * <p>
 *     通过zset实现滑动窗口算法限流
 * </p>
 *
 * @Author: Liziba
 * @Date: 2021/9/6 18:11
 */
public class SimpleSlidingWindowByZSet {

    private Jedis jedis;

    public SimpleSlidingWindowByZSet(Jedis jedis) {
        this.jedis = jedis;
    }

    /**
     * 判断行为是否被允许
     *
     * @param userId        用户id
     * @param actionKey     行为key
     * @param period        限流周期
     * @param maxCount      最大请求次数(滑动窗口大小)
     * @return
     */
    public boolean isActionAllowed(String userId, String actionKey, int period, int maxCount) {
        String key = this.key(userId, actionKey);
        long ts = System.currentTimeMillis();
        Pipeline pipe = jedis.pipelined();
        pipe.multi();
        pipe.zadd(key, ts, String.valueOf(ts));
        // 移除滑动窗口之外的数据
        pipe.zremrangeByScore(key, 0, ts - (period * 1000));
        Response<Long> count = pipe.zcard(key);
        // 设置行为的过期时间,如果数据为冷数据,zset将会删除以此节省内存空间
        pipe.expire(key, period);
        pipe.exec();
        pipe.close();
        return count.get() <= maxCount;
    }


    /**
     * 限流key
     *
     * @param userId
     * @param actionKey
     * @return
     */
    public String key(String userId, String actionKey) {
        return String.format("limit:%s:%s", userId, actionKey);
    }

}

测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
java复制代码package com.lizba.redis.limit;

import redis.clients.jedis.Jedis;

/**
 *
 * @Author: Liziba
 * @Date: 2021/9/6 20:10
 */
public class TestSimpleSlidingWindowByZSet {

    public static void main(String[] args) {
        Jedis jedis = new Jedis("192.168.211.108", 6379);
        SimpleSlidingWindowByZSet slidingWindow = new SimpleSlidingWindowByZSet(jedis);
        for (int i = 1; i <= 15; i++) {
            boolean actionAllowed = slidingWindow.isActionAllowed("liziba", "view", 60, 5);

            System.out.println("第" + i +"次操作" + (actionAllowed ? "成功" : "失败"));
        }

        jedis.close();
    }

}

测试效果: 从测试输出的数据可以看出,起到了限流的效果,从第11次以后的请求操作都是失败的,但是这个和我们允许的5次误差还是比较大的。这个问题的原因是我们测试System.currentTimeMillis()的毫秒可能相同,而且此时value也是System.currentTimeMillis()也相同,会导致zset中元素覆盖! image.png 修改代码测试: 在循环中睡眠1毫秒即可,测试结果符合预期!

1
bash复制代码 TimeUnit.MILLISECONDS.sleep(1);

image.png

3.3 lua代码实现

我们在项目中使用原子性的lua脚步来实现限流的使用会更多,因此这里也提供一个基于操作zset的lua版本

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
73
74
typescript复制代码package com.lizba.redis.limit;

import com.google.common.collect.ImmutableList;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Pipeline;
import redis.clients.jedis.Response;

/**
 * <p>
 *     通过zset实现滑动窗口算法限流
 * </p>
 *
 * @Author: Liziba
 * @Date: 2021/9/6 18:11
 */
public class SimpleSlidingWindowByZSet {

    private Jedis jedis;

    public SimpleSlidingWindowByZSet(Jedis jedis) {
        this.jedis = jedis;
    }

    /**
     * lua脚本限流
     *
     * @param userId
     * @param actionKey
     * @param period
     * @param maxCount
     * @return
     */
    public boolean isActionAllowedByLua(String userId, String actionKey, int period, int maxCount) {
        String luaScript = this.buildLuaScript();

        String key = key(userId, actionKey);
        long ts = System.currentTimeMillis();
        System.out.println(ts);
        ImmutableList<String> keys = ImmutableList.of(key);
        ImmutableList<String> args = ImmutableList.of(String.valueOf(ts),String.valueOf((ts - period * 1000)), String.valueOf(period));
        Number count = (Number) jedis.eval(luaScript, keys, args);

        return count != null && count.intValue() <= maxCount;
    }


    /**
     * 限流key
     *
     * @param userId
     * @param actionKey
     * @return
     */
    private String key(String userId, String actionKey) {
        return String.format("limit:%s:%s", userId, actionKey);
    }


    /**
     * 针对某个key使用lua脚本限流
     *
     * @return
     */
    private String buildLuaScript() {
        return "redis.call('ZADD', KEYS[1], tonumber(ARGV[1]), ARGV[1])" +
                "\nlocal c" +
                "\nc = redis.call('ZCARD', KEYS[1])" +
                "\nredis.call('ZREMRANGEBYSCORE', KEYS[1], 0, tonumber(ARGV[2]))" +
                "\nredis.call('EXPIRE', KEYS[1], tonumber(ARGV[3]))" +
                "\nreturn c;";

    }

}

测试代码不变,大家可以自行测试,记得还是要考虑我们测试的时候System.currentTimeMillis()相等的问题,不信你输出System.currentTimeMillis()就知道了!多思考问题,技术其实都在心里!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Rust从0️⃣入门(3) 基础语法

发表于 2021-10-06
  • 小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

基础语法

继续trait

  • 我们也可以利用trait给其他的类型添加成员方法
    • 如下代码,i32根本不是我们写的
    • 但是我们可以给i32增加方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
rust复制代码
trait Double {
fn double(&self) -> Self;
}



impl Double for i32 {
fn double(&self) -> i32 {
*self * 2
}
}


fn main() {
let x : i32 = 10.double();
//输出 double类型的值为 20
println!("double类型的值为 {}",x)
}

通用函数调用语法

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
rust复制代码
trait Cook {
fn start(&self);
}


trait Wash {
fn start(&self);
}

struct Chef;

impl Cook for Chef {
fn start(&self) {
println!("开始烹饪!!");
}
}

impl Wash for Chef {
fn start(&self) {
println!("开始刷碗!!");
}
}



fn main() {
let me = Chef;
//输出
// 开始烹饪!!
// 开始刷碗!!
<Cook>::start(&me);
<Chef as Wash>::start(&me);
}
  • 以上代码之所以在main中,具体写到两个trait去调用
    • 是因为如果只写一句me.start()会产生二义性
    • 所以在调用的时候一定要注意,要对应好是哪个trait

泛型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
rust复制代码
fn test<T : Debug>(x : T) {
println!("this is {:?}.",x);
}


fn main() {
/*
this is "Test".
this is 77.
this is true.
this is ['n', 'a', 's', 's'].
*/
test("Test");
test(77i32);
test(true);
test(['n','a','s','s']);
}
  • 以上代码,其中{:?}为格式化控制符
  • 数组*
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
rust复制代码
fn main() {

let a: [i32; 3] = [1,2,3];
let b: [i32; 10] = [0; 10];

//1 2 3
for i in a {
print!(" {}",i);
}
// 0 0 0 0 0 0 0 0 0 0
for i in b {
print!(" {}",i);
}
}
  • 以上代码:要注意数组长度、数组类型
    • 数组b是赋值了十个0
    数组切片
  • 我们可以将数组切片看做专门指向数组的指针
    • 可以理解成数组的一个视图
    • 例子中的数组是[T; n]类型的
    • 指针类型是&[T; n],通过内部编译将数组类型转换为切片类型&[T]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
rust复制代码fn main() {

fn mut_arr(a : &mut [i32]) {
a[2] = 5;
}

println!("size of &[i32; 3] : {:?}", std::mem::size_of::<&[i32; 3]>());
println!("size of &[i32] : {:?}", std::mem::size_of::<&[i32]>());

let mut v : [i32; 3] = [1,2,3];
{
let s : &mut [i32; 3] = &mut v;
mut_arr(s);
}
// 输出为:
// size of &[i32; 3] : 8
// size of &[i32] : 16
// [1, 2, 5]
println!("{:?}",v);
}
  • 上图代码其中变量v是[i32; 3]类型
    • 变量s是&mut [i32; 3]类型,这是占用的空间大小与指针都是相同的
    • 当自动转为&mut [i32; 3]类型时,传入函数mut_arr
    • 根据输出可以看出,占用的空间大小等于两个指针的空间大小
    • 在函数的内部,修改了外部数组的v的值

胖指针与DST

  • 因为数组切片不只是包含指向数组的指针,切片本身还包含带长度的信息,所以叫胖指针
    • 胖纸真对应的是动态大小类型可简称缩写DST
    • 比如例子中用到的是不定长数组类型是[T],对应的胖指针类型是&[T]
    • 由于无法判断类型占用的空间的大小
    • 所以不能在栈上声明一个不定长大小数组的变量实例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
rust复制代码fn example_slice(arr: &[i32]) {
unsafe {
let (v1, v2) : (usize,usize) = std::mem::transmute(arr);

println!("Value1 is {:x}",v1);
println!("Value2 is {:x}",v2);
}
}

fn main() {

let arr : [i32; 5] = [1,2,3,4,5];
let addr : &[i32; 5] = &arr;
println!("Address of arr is: {:p}",addr);
// 输出为:
// Address of arr is: 0x7ffee759f424
// Value1 is 7ffee759f424
// Value2 is 5
example_slice(addr as &[i32]);
}
  • 在上面代码中,arr的长度是5
    • 其中addr是指向arr的一个指针
    • 使用函数将我们的数组直接转为了一个长度和一个指向源数组的地址

Range

  • Range代表一个区间
    • 使用..代表左开右闭的区间
    • 如1..7代表1到7
1
2
3
4
5
6
7
8
9
rust复制代码fn main() {

let a = 1..7;

// 1 2 3 4 5 6
for i in a {
print!("{:?}\t",i);
}
}

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

盘点 Spring Conditional

发表于 2021-10-05

首先分享之前的所有文章 , 欢迎点赞收藏转发三连下次一定 >>>> 😜😜😜

文章合集 : 🎁 juejin.cn/post/694164…

Github : 👉 github.com/black-ant

CASE 备份 : 👉 gitee.com/antblack/ca…

一 . 前言

这一篇来看一下Conditional的使用和原理 , 先来看一下整体的体系结构

System-SpringBootCondition.png

二 . 使用

  • @ConditionalOnBean:当容器里有指定 Bean 的条件下。
  • @ConditionalOnMissingBean:当容器里没有指定 Bean 的情况下。
  • @ConditionalOnSingleCandidate:当指定 Bean 在容器中只有一个,或者虽然有多个但是指定首选 Bean
  • @ConditionalOnClass:当类路径下有指定类的条件下。
  • @ConditionalOnMissingClass:当类路径下没有指定类的条件下。
  • @ConditionalOnProperty:指定的属性是否有指定的值
  • @ConditionalOnResource:类路径是否有指定的值
  • @ConditionalOnExpression:基于 SpEL 表达式作为判断条件。
  • @ConditionalOnJava:基于 Java 版本作为判断条件
  • @ConditionalOnJndi:在 JNDI 存在的条件下差在指定的位置
  • @ConditionalOnNotWebApplication:当前项目不是 Web 项目的条件下
  • @ConditionalOnWebApplication:当前项目是 Web项 目的条件下

2.1 基础使用

2.2 自定义 Conditional

1
2
3
4
5
6
7
8
9
10
11
java复制代码public class DefaultConditional implements Condition {

private Logger logger = LoggerFactory.getLogger(this.getClass());

@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
logger.info("------> 进行 Conditional 判断 <-------");

return false;
}
}

2.3 基础使用

1
2
3
4
5
6
java复制代码@Bean
@ConditionalOnBean(TestService.class)
public ConfigBean getConfigBean() {
logger.info("------> 开始加载 ConditionalOnBean <-------");
return new ConfigBean();
}

三 . 自定义原理分析

3.1 Conditional 入口

对于 Configuration.@Bean 的创建方式 , Conditinal 的起点是 refush# invokeBeanFactoryPostProcessors , 在其中会调用 ConfigurationClassPostProcessor 进行处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码private void loadBeanDefinitionsForBeanMethod(BeanMethod beanMethod) {

// Step 1 : 获取当前 Configuration 中 @Bean 的元数据信息
ConfigurationClass configClass = beanMethod.getConfigurationClass();
MethodMetadata metadata = beanMethod.getMetadata();
String methodName = metadata.getMethodName();

// 判断是否应该跳过当前 Bean
if (this.conditionEvaluator.shouldSkip(metadata, ConfigurationPhase.REGISTER_BEAN)) {
configClass.skippedBeanMethods.add(methodName);
return;
}
if (configClass.skippedBeanMethods.contains(methodName)) {
return;
}
}

3.2 Conditional 判断的流程

ConditionEvaluator 是核心处理类 , 最终都会调用 shouldSkip 判断是否跳过

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
java复制代码// C- ConditionEvaluator
public boolean shouldSkip(@Nullable AnnotatedTypeMetadata metadata, @Nullable ConfigurationPhase phase) {

// Step 1 : 如果当前 Bean 不包含 @Conditional , 则直接返回
if (metadata == null || !metadata.isAnnotated(Conditional.class.getName())) {
return false;
}

// Step 2 : 有2种 ConfigurationPhase 的类型 , 表示2种配置的阶段
// PARSE_CONFIGURATION :Condition应该在解析@Configuration类时进行计算 , 如果此时条件不匹配,则不会添加@Configuration类
// REGISTER_BEAN : 条件不会阻止@Configuration类被添加 , 在评估条件时,所有@Configuration类都将被解析
if (phase == null) {
if (metadata instanceof AnnotationMetadata &&
ConfigurationClassUtils.isConfigurationCandidate((AnnotationMetadata) metadata)) {
return shouldSkip(metadata, ConfigurationPhase.PARSE_CONFIGURATION);
}
return shouldSkip(metadata, ConfigurationPhase.REGISTER_BEAN);
}

// Step 3 : 获取所有的 Condition 对象
List<Condition> conditions = new ArrayList<>();
for (String[] conditionClasses : getConditionClasses(metadata)) {
for (String conditionClass : conditionClasses) {
Condition condition = getCondition(conditionClass, this.context.getClassLoader());
conditions.add(condition);
}
}

// Step 4 : 排序
AnnotationAwareOrderComparator.sort(conditions);

for (Condition condition : conditions) {
ConfigurationPhase requiredPhase = null;
if (condition instanceof ConfigurationCondition) {
requiredPhase = ((ConfigurationCondition) condition).getConfigurationPhase();
}
// Step 5 : 最终的 Condition 匹配过程
if ((requiredPhase == null || requiredPhase == phase) && !condition.matches(this.context, metadata)) {
return true;
}
}

return false;
}

直到这里就开始匹配到对应的方法

四 .常规加载方式

解析

已知的Conditional 是基于 SpringBootCondition 实现的 , 其具体抽象类为 FilteringSpringBootCondition , 看一下主要的继承关系

System_FilteringSpringBootCondition.png

去除不需要配置的类

第一步是快速去除不需要的类 , 主要流程如下 :

  • 起点 : AbstractApplicationContext # refresh # invokeBeanFactoryPostProcessors
  • 处理 : ConfigurationClassPostProcessor # postProcessBeanDefinitionRegistry 处理主要逻辑
  • 拦截 : ConfigurationClassFilter # filter
  • 匹配 : FilteringSpringBootCondition # match

注意 , 这里是进行 match 匹配 ,目的是获取基于正在导入的Configuration类的AnnotationMetadata的AutoConfigurationEntry

4.1 Filter 拦截

Filter 拦截是在 ConfigurationClassFilter ,其中会对所有的 Conditional 进行拦截处理

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
java复制代码private static class ConfigurationClassFilter {

// 自动配置的元数据
private final AutoConfigurationMetadata autoConfigurationMetadata;

List<String> filter(List<String> configurations) {
long startTime = System.nanoTime();

// 此处为所有的 Confiturations 类
String[] candidates = StringUtils.toStringArray(configurations);
boolean skipped = false;

// 此处包含 OnBeanCondition , OnClassCondition ,OnWebApplicationCondition 三种
for (AutoConfigurationImportFilter filter : this.filters) {
// 获取是否 存在 match 匹配
boolean[] match = filter.match(candidates, this.autoConfigurationMetadata);
for (int i = 0; i < match.length; i++) {
if (!match[i]) {
candidates[i] = null;
skipped = true;
}
}
}
if (!skipped) {
return configurations;
}

// 如果不能跳过 , 记录当前 Confiturations 类
List<String> result = new ArrayList<>(candidates.length);
for (String candidate : candidates) {
if (candidate != null) {
result.add(candidate);
}
}
return result;
}

}

4.2 FilteringSpringBootCondition 中 match 匹配

此处是重写了其父类的 match 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码public boolean[] match(String[] autoConfigurationClasses, AutoConfigurationMetadata autoConfigurationMetadata) {
// Step 1 : 准备 Report 对象 , 用于记录
ConditionEvaluationReport report = ConditionEvaluationReport.find(this.beanFactory);
// Step 2 : 获取对应的所有的 Condition 方法
ConditionOutcome[] outcomes = getOutcomes(autoConfigurationClasses, autoConfigurationMetadata);

boolean[] match = new boolean[outcomes.length];
for (int i = 0; i < outcomes.length; i++) {

// 对match中的数组进行赋值,当outcomes对应下标的ConditionOutcome匹配时为true.其他情况,返回false
match[i] = (outcomes[i] == null || outcomes[i].isMatch());
if (!match[i] && outcomes[i] != null) {
// 记录日志
logOutcome(autoConfigurationClasses[i], outcomes[i]);
if (report != null) {
// 像 ConditionEvaluationReport # SortedMap 存放评估条件
report.recordConditionEvaluation(autoConfigurationClasses[i], this, outcomes[i]);
}
}
}
return match;
}

ConditionEvaluationReport 的作用

该对象用来记录自动化配置过程中条件匹配的详细信息及日志信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码public final class ConditionEvaluationReport {

//bean名称
private static final String BEAN_NAME = "autoConfigurationReport";

//创建一个父的条件匹配对象
private static final AncestorsMatchedCondition ANCESTOR_CONDITION = new AncestorsMatchedCondition();

//存放类名或方法名(key),条件评估输出对象(value)
private final SortedMap<String, ConditionAndOutcomes> outcomes = new TreeMap<>();

//是否是原始条件匹配对象
private boolean addedAncestorOutcomes;
//父的条件评估报告对象
private ConditionEvaluationReport parent;

//记录已经从条件评估中排除的类名称
private final List<String> exclusions = new ArrayList<>();
//记录作为条件评估的候选类名称
private final Set<String> unconditionalClasses = new HashSet<>();

}

4.3 getOutcomes 获取

此处以 OnBean 为例 , 此处存在一定的关联关系 :

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
java复制代码protected final ConditionOutcome[] getOutcomes(String[] autoConfigurationClasses,
AutoConfigurationMetadata autoConfigurationMetadata) {

// Step 1 : 初始化一个和处理类等容量的数组
ConditionOutcome[] outcomes = new ConditionOutcome[autoConfigurationClasses.length];

// Step 2 : 遍历所有的 autoConfigurationClasses
for (int i = 0; i < outcomes.length; i++) {
String autoConfigurationClass = autoConfigurationClasses[i];

if (autoConfigurationClass != null) {

Set<String> onBeanTypes = autoConfigurationMetadata.getSet(autoConfigurationClass, "ConditionalOnBean");
// 判断是否存在 ConditionalOnBean 标注的方法
outcomes[i] = getOutcome(onBeanTypes, ConditionalOnBean.class);

// 判断是否需要输出 ConditionOutcome
if (outcomes[i] == null) {
Set<String> onSingleCandidateTypes = autoConfigurationMetadata.getSet(autoConfigurationClass,
"ConditionalOnSingleCandidate");
// 此处是返回是否要处理
outcomes[i] = getOutcome(onSingleCandidateTypes, ConditionalOnSingleCandidate.class);
}
}
}
return outcomes;
}

4.4 如何判断是否符合评估条件

注意 , 这里的 matches 和 FilteringSpringBootCondition 不是一个

  • FilteringSpringBootCondition # match : 基于 AutoConfigurationImportFilter , 对给定的自动配置类候选应用筛选器
  • SpringBootCondition # matches : 需要返回最终的判断结果

调用流程

  • 在 loadBeanDefinitionsForBeanMethod 等类似流程种调用 shouldSkip , 从而跳转到该逻辑
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
java复制代码@Override
public final boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {

// 获取注解对应的方法名或者类名
String classOrMethodName = getClassOrMethodName(metadata);
try {

// 获取对应的条件匹配类 , 此处会判断 metadata instanceof , 有限判断是否为 ClassMetadata
ConditionOutcome outcome = getMatchOutcome(context, metadata);

// 很简单的打印日志 , Trace 级别
logOutcome(classOrMethodName, outcome);

// 记录结果 , 通过 ConditionEvaluationReport 和 recordEvaluation 方法实现
recordEvaluation(context, classOrMethodName, outcome);

// 返回是否成功匹配
return outcome.isMatch();
}
catch (NoClassDefFoundError ex) {
throw new IllegalStateException(......);
}
catch (RuntimeException ex) {
throw new IllegalStateException("Error processing condition on " + getName(metadata), ex);
}
}

这里的核心就是调用 getMatchOutcome 判断是否符合或者不符合要求 , getMatchOutcome 需要子类重写

五 . getMatchOutcome 详情

5.1 OnClass

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
java复制代码public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
// Step 1 : 获取当前容器 ClassLoader
ClassLoader classLoader = context.getClassLoader();
ConditionMessage matchMessage = ConditionMessage.empty();
// Step 2 : 判断是否有 ConditionalOnClass 约束
List<String> onClasses = getCandidates(metadata, ConditionalOnClass.class);
if (onClasses != null) {
// Step 2-1 : filter 过滤 , 判断是否缺失类
List<String> missing = filter(onClasses, ClassNameFilter.MISSING, classLoader);
if (!missing.isEmpty()) {
return ConditionOutcome.noMatch(ConditionMessage.forCondition(ConditionalOnClass.class)
.didNotFind("required class", "required classes").items(Style.QUOTE, missing));
}
// Step 2-2 : 构建 matchMessage
matchMessage = matchMessage.andCondition(ConditionalOnClass.class)
.found("required class", "required classes")
.items(Style.QUOTE, filter(onClasses, ClassNameFilter.PRESENT, classLoader));
}

// Step 3 : 同理 , 判断是否需要 MissClasses
List<String> onMissingClasses = getCandidates(metadata, ConditionalOnMissingClass.class);
if (onMissingClasses != null) {
// .... 与 ConditionalOnClass 基本类似 ,此处省略
}
return ConditionOutcome.match(matchMessage);
}

补充 : ConditionMessage 的作用

5.2 OnBean

与 OnBean 类似 , 这里就展示一种

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
java复制代码public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
ConditionMessage matchMessage = ConditionMessage.empty();


MergedAnnotations annotations = metadata.getAnnotations();

if (annotations.isPresent(ConditionalOnBean.class)) {
// Step 1 :获取 ConditionalOnBean 注解信息
Spec<ConditionalOnBean> spec = new Spec<>(context, metadata, annotations, ConditionalOnBean.class);
// Step 2 : 返回匹配结果
// 其内部通过 getNamesOfBeansIgnoredByType , getBeanNamesForType 等方式判断类是否存在
MatchResult matchResult = getMatchingBeans(context, spec);
if (!matchResult.isAllMatched()) {
String reason = createOnBeanNoMatchReason(matchResult);
return ConditionOutcome.noMatch(spec.message().because(reason));
}
matchMessage = spec.message(matchMessage).found("bean", "beans").items(Style.QUOTE,
matchResult.getNamesOfAllMatches());
}
if (metadata.isAnnotated(ConditionalOnSingleCandidate.class.getName())) {
//.....
}
if (metadata.isAnnotated(ConditionalOnMissingBean.class.getName())) {
//.....
}
return ConditionOutcome.match(matchMessage);
}

总结

Conditional 本身并不难 , 这一篇主要是为了完善图谱以及后续的 starter 启动流程方案 做准备.

整个流程中有几个环节理解就行了 :

  • Spring 中的 Conditional 都会继承 SpringBootCondition , 会实现其 getOutcomes 方法
  • getOutcomes 是用于快速去掉无需加载的 Configuration , getMatchOutcome 是为了验证匹配关系
  • 通常都会通过 ConditionEvaluator 的 shouldSkip 判断是否需要跳过@Bean 流程

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

ArrayList的扩容机制

发表于 2021-10-05

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

在Java中,ArrayList是一个使用非常频繁的集合类型,它的底层是Object数组,所以它拥有数组所拥有的特性,比如支持随机访问,所以查询效率高,但插入数据需要移动元素,所以效率低。

先来看看若是调用ArrayList的无参构造方法,会发生什么?

1
2
3
4
5
6
7
java复制代码transient Object[] elementData;

private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

在构造方法中,它将DEFAULTCAPACITY_EMPTY_ELEMENTDATA赋值给elementData,这个DEFAULTCAPACITY_EMPTY_ELEMENTDATA是一个空的Object数组,而elementData就是ArrayList实际存储数据的容器。

由此可知,ArrayList在调用无参构造方法时创建的是一个长度为0的空数组,当调用add()方法添加元素时,ArrayList才会触发扩容机制:

1
2
3
4
5
java复制代码public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}

add()方法的第一行即是执行扩容流程:

1
2
3
java复制代码private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

该方法又会先计算扩容容量:

1
2
3
4
5
6
java复制代码private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}

初始elementData就是一个空数组,条件成立,它会从DEFAULT_CAPACITY和minCapacity中选择一个最大值返回,其中DEFAULT_CAPACITY表示默认的初始容量,它的值为10:

1
java复制代码private static final int DEFAULT_CAPACITY = 10;

而minCapacity是add()方法传递过来的,值为size + 1:

1
java复制代码ensureCapacityInternal(size + 1);  // Increments modCount!!

所以calculateCapacity()方法将返回10,之后调用ensureExplicitCapacity()方法:

1
2
3
4
5
6
7
java复制代码private void ensureExplicitCapacity(int minCapacity) {
modCount++;

// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}

首先让modCount++,这是用来记录数组被修改次数的变量,我们先不管它,此时minCapacity的值为10,elementData.length的值为0,条件成立,执行grow()方法:

1
2
3
4
5
6
7
8
9
10
11
java复制代码private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}

注意这一行代码:

1
java复制代码int newCapacity = oldCapacity + (oldCapacity >> 1);

先将旧容量右移1位,再加上旧容量就得到了新容量,正数右移1位相当于除以2,在该基础上加旧容量,则等价于新容量 = 旧容量 * 1.5,所以才有ArrayList每次扩容为旧容量的1.5倍的说法,最后调用Arrays.copyOf()方法进行拷贝,并将elementData指向新数组,而旧数组因为没有引用指向它,很快就会被垃圾收集器回收掉。

当第二次调用add()方法时,程序依然要走到扩容方法:

1
2
3
4
5
6
java复制代码private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}

但此时的elementData已经不是空数组了,所以直接返回当前size + 1,即:2,接着调用:

1
2
3
4
5
6
7
java复制代码private void ensureExplicitCapacity(int minCapacity) {
modCount++;

// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}

因为此时minCapacity小于数组长度,所以if判断不会成立,也就不会发生扩容。

当添加第11个元素时,ArrayList应该会触发第二次扩容,来看源代码:

1
2
3
4
5
6
java复制代码private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}

minCapacity的值为11,紧接着调用它:

1
2
3
4
5
6
7
java复制代码private void ensureExplicitCapacity(int minCapacity) {
modCount++;

// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}

此时minCapacity的值大于elementData的长度,条件成立,触发扩容机制:

1
2
3
4
5
6
7
8
9
10
11
java复制代码private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}

将原容量10右移一位得到5,再加上原容量10得到15,所以新数组的容量为15,最后对数组进行拷贝扩容就完成了。

当ArrayList进行第三次扩容后容量会是多少呢?我们知道,新容量一定是旧容量的1.5倍,而15 * 1.5 = 22.5,那么新容量到底是22还是23呢?所以,如果你只知道新容量是旧容量的1.5倍,这个问题你就无法知道。事实上,ArrayList底层是通过移位操作计算得到的新容量。所以新容量应该等于15 >> 1 + 15 = 22,由此可得,ArrayList经过第三次扩容后容量为22。

然而在addAll()方法中,扩容机制会有一定的变化,比如:

1
2
3
4
java复制代码public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.addAll(Arrays.asList(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10));
}

执行完addAll()方法后,ArrayList的容量应该是多少呢?是15吗?来看看源代码:

1
2
3
4
5
6
7
8
java复制代码public boolean addAll(Collection<? extends E> c) {
Object[] a = c.toArray();
int numNew = a.length;
ensureCapacityInternal(size + numNew); // Increments modCount
System.arraycopy(a, 0, elementData, size, numNew);
size += numNew;
return numNew != 0;
}

它将调用ensureCapacityInternal()方法进行扩容,并传入 size + numNew = 11:

1
2
3
4
5
6
java复制代码private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}

因为初始elementData是一个空数组,符合条件,所以它将返回DEFAULT_CAPACITY和minCapacity中较大的那个,结果是minCapacity较大,所以返回11,这就导致addAll()方法执行结果后ArrayList的容量为11。

addAll()方法总是选择扩容一次后的容量与旧容量加上添加的元素个数的容量中取一个最大值作为新的容量,比如:当前ArrayList中有10个元素,而addAll()方法需要添加6个元素,当ArrayList触发扩容后的新容量应该为15,而旧容量加上需要添加的元素容量为16,从中取一个较大值为16,所以新容量应该为16。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

🌊面试官要我说AQS应用场景,我只好给他讲了CountDow

发表于 2021-10-05

小知识,大挑战!本文正在参与“ 程序员必备小知识 ”创作活动

本文同时参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金

Code皮皮虾 一个沙雕而又有趣的憨憨少年,和大多数小伙伴们一样喜欢听歌、游戏,当然除此之外还有写作的兴趣,emm…,日子还很长,让我们一起加油努力叭🌈


😁前言

博主 常年游荡于牛客面经区,总结了字节、阿里、百度、腾讯、美团等等大厂的高频考题,之后会逐步分享给大家,期待各位的关注、点赞!

在这里插入图片描述


✨CountdownLatch源码讲解

话不多说,开撸,哦不,开讲!!!


CountDownLatch主要有两个方法:==①:await(),②:countDown();==

那我就不先买个关子了,我直接说:
调用await()方法的线程会被阻塞,直到计数器 减到 0 的时候,才能继续往下执行;
countDown():将计数器减一,
相信小伙伴们看完了我的源码讲解之后就会明白了。

但是我们先从CountDownLatch构造函数看起


CountDownLatch构造函数

1
java复制代码CountDownLatch countDownLatch = new CountDownLatch(2);

这里可以看到CountDownLatch 的构造函数其实是new的一个Sync,且将我们传入的int类型值也作为了Sync的参数

1
2
3
4
java复制代码public CountDownLatch(int count) {
if (count < 0) throw new IllegalArgumentException("count < 0");
this.sync = new Sync(count);
}

我们再点进来看,可见Sync继承了 AbstractQueuedSynchronizer 即AQS

而Sync的有参构造调用了setState(count);方法

在这里插入图片描述

再看看setState方法,==其实说白了就是将我们CountDownLatch传入的int值作为了AQS的同步状态。==

在这里插入图片描述

到这再强烈建议大家先入手AQS,入手了之后再看这并发包下的工具类真的不要太简单!!!

AQS快速上手


countDown()图示、源码讲解

再来看看 countDown 方法

1
2
3
java复制代码public void countDown() {
sync.releaseShared(1);
}

这里调用了sync的releaseShared方法,传入了arg = 1

而我们可以看到releaseShared涉及到了两个方法 tryReleaseShared(1)和doReleaseShared()

1
2
3
4
5
6
7
java复制代码public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}

先看tryReleaseShared(1)

在这里插入图片描述

这源码相信给位小伙伴一看都能懂把,那我就简要总结下!

  • 调用getState()方法获取同步状态,如果同步状态为0,则返回false,结合下面的代码看,意味着不能在减同步状态了
  • 如果不为0,也可以说是大于0,那么用一个int变量记录将同步状态减一后的值
  • 最后同步CAS设置同步状态为减一后的值,如果设置失败就自旋重试,如果成功就看减一后的值是不是0
1
2
3
4
5
6
7
8
9
10
11
java复制代码protected boolean tryReleaseShared(int releases) {
// Decrement count; signal when transition to zero
for (;;) {
int c = getState();
if (c == 0)
return false;
int nextc = c-1;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}

结合之前源码看,如果减一后的同步状态为0,那么就会调用 doReleaseShared()方法

在这里插入图片描述

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
java复制代码private void doReleaseShared() {
for (;;) {
//获取头结点
Node h = head;
if (h != null && h != tail) {
//获取head结点的等待状态
int ws = h.waitStatus;
//如果head结点的状态为SIGNAL, 表明后面有人在排队
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
//直到CAS成功
continue;
//再去唤醒后继结点;在独占锁的时候有说明,这里就不多说了;
unparkSuccessor(h);
}
//如果head结点的状态为0, 表明此时后面没人在排队, 就只是将head状态修改为PROPAGATE
else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
//直到CAS成功
continue;
}
if (h == head)
break;
}
}

可能小伙伴们看了源码也没太懂,那我小小总结一下

  • 当同步状态为0的时候才会去调用,doReleaseShared()方法
    • 如果同步状态为0,说明锁没有线程占用
  • 那么就涉及到doReleaseShared()方法,去看该线程后面有没有线程排队
    • 如果有线程排队,那么upark将其唤醒,并且有没有节点都要更新当前节点的状态

await()图示、源码讲解

await()方法调用的 sync的acquireSharedInterruptibly(1)方法

1
2
3
java复制代码public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}

点进来看

第一个if没啥好说的,如果线程被打断了就要抛异常

主要是第二个if,涉及到tryAcquireShared(1),doAcquireSharedInterruptibly(1)方法

1
2
3
4
5
6
7
java复制代码public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}

tryAcquireShared(1)跟传入的参数无关,就是看当前的同步状态是否为0,如果为0返回1,不为0返回-1

==那么有小伙伴问了,为什么要这么返回呢???==

不要着急,我们接着看第二个方法

1
2
3
java复制代码protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}

doAcquireSharedInterruptibly

别的都可以不看,关键就在于这个for(;;)循环
我们可以看到,如果想要退出for循环,必须满足 ==(p == head) && (r >= 0)==,也就是==当前节点等于头节点,且同步状态为0==

tryAcquireShared我们上面介绍过

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
java复制代码private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}

ok,相信能看到这里的小伙伴可能差那么一点就能明白,那我再给各位小伙伴总结一下。
==创作不易,希望小伙伴们可以一键三连多多支持!!!== 😁


🔥总结

就像我开头说的,
调用await()方法的线程会被阻塞,直到计数器 减到 0 的时候,才能继续往下执行;
countDown():将计数器减一。

结合上面的源码讲解,==先说await()==

  • 如果计数器不为0,那么tryAcquireShared返回的就一定为1,那么 r >= 0就不会满足,那么就无法退出,会一直进行for循环即起到阻塞作用

再说countDown()

  • 每调用一次countDown()方法就会去利用CAS将计数器减一
  • 当同步状态为0的时候才会去调用,doReleaseShared()方法
+ **如果同步状态为0,说明锁没有线程占用**
  • 那么就涉及到doReleaseShared()方法,去看该线程后面有没有线程排队
+ **如果有线程排队,那么upark将其唤醒,并且有没有节点都要更新当前节点的状态**

💖最后

我是 Code皮皮虾,一个热爱分享知识的 皮皮虾爱好者,未来的日子里会不断更新出对大家有益的博文,期待大家的关注!!!

创作不易,如果这篇博文对各位有帮助,希望各位小伙伴可以==一键三连哦!==,感谢支持,我们下次再见~


一键三连.png

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

md5加密科普,关于平时数据库密码的保存

发表于 2021-10-05

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

本文已参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。

今天来讲一下加密,讲的是md5的相关知识科普。

MD5算法的原理可简要的叙述为:MD5码以512位分组来处理输入的信息,且每一分组又被划分为16个32位子分组,经过了一系列的处理后,算法的输出由四个32位分组组成,将这四个32位分组级联后将生成一个128位散列值。

MD5主要特点:
不可逆,相同数据的MD5值肯定一样,不同数据的MD5值不一样

一个MD5理论上的确是可能对应无数多个原文的,因为MD5是有限多个的而原文可以是无数多个。比如主流使用的MD5将任意长度的“字节串映射为一个128bit的大整数。也就是一共有2^128种可能,大概是3.4 * 10^38,这个数字是有限多个的,而但是世界上可以被用来加密的原文则会有无数的可能性

由于md5的是不可逆的,所以并没有解密一说法。那么md5广泛应用在什么地方呢?

image.png

用于密码管理

当我们需要保存某些密码信息以用于身份确认时,如果直接将密码信息以明码方式保存在数据库中,不使用任何保密措施,系统管理员就很容易能得到原来的密码信息,这些信息一旦泄露, 密码也很容易被破译。为了增加安全性,有必要对数据库中需要保密的信息进行加密,这样,即使有人得到了整个数据库,如果没有解密算法,也不能得到原来的密码信息。MD5算法可以很好地解决这个问题。

当我们登录时候,需要对比客户输入的密码md5值和数据库的是否相同就行,当md5值相同,那就是登录成功,否则就是登录失败。所以一般情况下,我们上一些网站或者软件上的密码,无论是公司什么级别的人,都是取不到的。在我们看来,MD5值就是一串没有规律的乱码而已。所以一般我们丢了密码,那就只能重置密码,我们会将保存的密码再一次用md5加密,覆盖原来已经保存的,而保存在数据库里面的就成了另外一个md5值。

上面总结的是后端服务器常用到的MD5加密,给大家科普一下数据库是里面是怎么保存的。而实际应用中有很多直接可以使用的md5模块,我们直接使用就行了。

1
2
3
4
5
6
ini复制代码import hashlib
m = hashlib.md5()
m.update(b'123')
m.hexdigest()

#加密后结果 202cb962ac59075b964b07152d234b70

这是用python做的一个简单案例。

欢迎和我讨论有关程序的问题,也可以答疑。关注公众号:诗一样的代码,交一个朋友。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

架构师如何讲解Redis限流——令牌桶限流

发表于 2021-10-05

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

本文已参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金。

1、简介

令牌桶算法比较简单,它就好比摇号买房,拿到号的人才有资格买,没拿到号的就只能等下次了(还好小编不需摇号,因为买不起!)。

在实际的开发中,系统会维护一个容器用于存放令牌(token),并且系统以一个固定速率往容器中添加令牌(token),这个速率通常更加系统的处理能力来权衡。当客户端的请求打过来时,需要从令牌桶中获取到令牌(token)之后,这个请求才会被处理,否则直接拒绝服务。

令牌桶限流的关键在于发放令牌的速率和令牌桶的容量。\

令牌桶限流.png

实现令牌桶限流的方式有很多种,本文讲述的是基于Redis的Redis-Cell限流模块,这是Redis提供的适用于分布式系统、高效、准确的限流方式,使用十分广泛,而且非常简单!

​

2、Redis-Cell的安装

Redis默认是没有集成Redis-Cell这个限流模块的,就好比Redis使用布隆过滤器一样,我们也需要对该模块进行安装与集成。

​

2.1 GitHub源码&安装包

Redis-Cell的GitHub地址:

github.com/brandur/red…

Redis-Cell基于Rust语言开发,如果不想花费精力去搞Rust环境,那么可以直接下载与你的操作系统对应的安装包(这个很关键,我就安装了挺多次的,如果安装的问题比较多的话,也建议降低一个release版本!)\

image.png

image.png

下载对应的安装包:

github.com/brandur/red…

image.png

如果不清楚自己的服务器(Linux)版本的,可以事先查看后再下载安装包:

1
2
3
4
bash复制代码# Linux 查看当前操作系统的内核信息
uname -a
# 查看当前操作系统系统的版本信息
cat /proc/version

image.png

2.2 安装&异常处理

  • 在Redis的安装目录的同级目录下,新建文件夹Redis-Cell,将压缩包上传后解压
1
sql复制代码tar -zxvf redis-cell-v0.2.5-powerpc64-unknown-linux-gnu.tar.gz
  • 解压后出现如下文件,复制libredis_cell.so文件的路径(pwd查看当前路径

image.png

  • 修改Redis配置文件,redis.conf,添加完成后记得保存后再退出

image.png

  • 重启Redis,如果启动正常,进入redis客户端,通过module list查看挂载的模块是否有Redis-Cell

image.png

  • 测试指令,出现如下情况说明集成Redis-Cell成功

image.png

  • 如果重启Redis后,客户端无法连接成功,说明Redis启动失败,这个时候我们需要查看Redis的启动日志,如果已经配置日志文件的可以直接查看日志定位问题,如果还未配置日志文件的需要先配置日志文件,redis.conf添加日志文件路径地址,再次重启,查看日志文件输出的错误日志

image.png

  • 错误可能千奇百怪,问题不大搞技术就不要心急,一个个解决,我这里记录下我最后遇到的问题, /lib64/libc.so.6: version `GLIBC_2.18‘ not found
1
2
lua复制代码43767:M 08 Sep 2021 21:39:39.643 # Module /usr/local/soft/Redis-Cell-0.3.0/libredis_cell.so failed to load: /lib64/libc.so.6: version `GLIBC_2.18' not found (required by /usr/local/soft/Redis-Cell-0.3.0/libredis_cell.so)
43767:M 08 Sep 2021 21:39:39.643 # Can't load module from /usr/local/soft/Redis-Cell-0.3.0/libredis_cell.so: server aborting
  • 缺失GLIBC_2.18,那就安装它(最后两个编译的过程时间比较长,耐心等待几分钟)
1
2
3
4
5
6
7
8
9
bash复制代码yum install gcc
wget http://ftp.gnu.org/gnu/glibc/glibc-2.18.tar.gz
tar zxf glibc-2.18.tar.gz 
cd glibc-2.18/
mkdir build
cd build/
../configure --prefix=/usr
make -j4
make install
  • 安装完成后,重启Redis,测试是否安装成功,循环上面的过程,通过日志分析错误即可

3、CL.THROTTLE指令

指令CL.THROTTLE参数含义

1
2
3
4
5
6
scss复制代码CL.THROTTLE liziba  10  5 60 1
               ▲     ▲  ▲  ▲ ▲
               |     |  |  | └───── apply 1 token (default if omitted) (本次申请一个token)
               |     |  └──┴─────── 5 tokens / 60 seconds  (60秒添加5个token到令牌桶中)
               |     └───────────── 10 max_burst    (最大的突发请求,不是令牌桶的最大容量)
               └─────────────────── key "liziba" (限流key)

输出参数值含义

1
2
3
4
5
6
bash复制代码127.0.0.1:6379> cl.throttle liziba 10 5 60 1
1) (integer) 0                    # 当前请求是否被允许,0表示允许,1表示不允许
2) (integer) 11                    # 令牌桶的最大容量,令牌桶中令牌数的最大值
3) (integer) 10                  # 令牌桶中当前的令牌数
4) (integer) -1                    # 如果被拒绝,需要多长时间后在重试,如果当前被允许则为-1
5) (integer) 12                    # 多长时间后令牌桶中的令牌会满

这里唯一有歧义的可能是max_burst,这个并不是令牌桶的最大容量,从作者的README.md中的解释也可以看出来

The total limit of the key (max_burst + 1). This is equivalent to the common X-RateLimit-Limit HTTP header.

image.png

4、Java调用Redis-Cell模块实现限流

4.1 导入依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
xml复制代码<dependency>
  <groupId>io.lettuce</groupId>
  <artifactId>lettuce-core</artifactId>
  <version>5.3.4.RELEASE</version>
  <!--排除 netty 包冲突-->
  <exclusions>
    <exclusion>
      <groupId>io.netty</groupId>
      <artifactId>netty-buffer</artifactId>
    </exclusion>
    <exclusion>
      <groupId>io.netty</groupId>
      <artifactId>netty-common</artifactId>
    </exclusion>
    <exclusion>
      <groupId>io.netty</groupId>
      <artifactId>netty-codec</artifactId>
    </exclusion>
    <exclusion>
      <groupId>io.netty</groupId>
      <artifactId>netty-transport</artifactId>
    </exclusion>
  </exclusions>
</dependency>

4.2 实现代码

Redis命令接口定义:

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
java复制代码package com.lizba.redis.limit.tokenbucket;

import io.lettuce.core.dynamic.Commands;
import io.lettuce.core.dynamic.annotation.Command;

import java.util.List;

/**
 * <p>
 *      Redis命令接口定义
 * </p>
 *
 * @Author: Liziba
 * @Date: 2021/9/8 23:50
 */
public interface IRedisCommand extends Commands {


    /**
     * 定义限流方法
     *
     * @param key           限流key
     * @param maxBurst      最大的突发请求,桶容量等于maxBurst + 1
     * @param tokens        tokens 与 seconds 是组合参数,表示seconds秒内添加个tokens
     * @param seconds       tokens 与 seconds 是组合参数,表示seconds秒内添加个tokens
     * @param apply         当前申请的token数
     * @return
     */
    @Command("CL.THROTTLE ?0 ?1 ?2 ?3 ?4")
    List<Object> throttle(String key, long maxBurst, long tokens, long seconds, long apply);

}

Redis-Cell令牌桶限流类定义:

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
java复制代码package com.lizba.redis.limit.tokenbucket;

import io.lettuce.core.RedisClient;
import io.lettuce.core.api.StatefulRedisConnection;
import io.lettuce.core.dynamic.RedisCommandFactory;

import java.util.List;

/**
 * <p>
 *      Redis-Cell令牌桶限流
 * </p>
 *
 * @Author: Liziba
 * @Date: 2021/9/8 23:47
 */
public class TokenBucketRateLimiter {

    private static final String SUCCESS = "0";
    private RedisClient client;
    private StatefulRedisConnection<String, String> connection;
    private IRedisCommand command;

    public TokenBucketRateLimiter(RedisClient client) {
        this.client = client;
        this.connection = client.connect();
        this.command = new RedisCommandFactory(connection).getCommands(IRedisCommand.class);
    }


    /**
     * 请是否被允许
     *
     * @param key
     * @param maxBurst
     * @param tokens
     * @param seconds
     * @param apply
     * @return
     */
    public boolean isActionAllowed(String key, long maxBurst, long tokens, long seconds, long apply) {
        List<Object> result = command.throttle(key, maxBurst, tokens, seconds, apply);
        if (result != null && result.size() > 0) {
            return SUCCESS.equals(result.get(0).toString());
        }
        return false;
    }

}

测试代码:

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
java复制代码package com.lizba.redis.limit.tokenbucket;

import io.lettuce.core.RedisClient;

/**
 * <p>
 *      测试令牌桶限流
 *      测试参数 cl.throttle liziba 10 5 60 1
 * </p>
 *
 * @Author: Liziba
 * @Date: 2021/9/9 0:02
 */
public class TestTokenBucketRateLimiter {

    public static void main(String[] args) {
        RedisClient client = RedisClient.create("redis://192.168.211.108:6379");
        TokenBucketRateLimiter limiter = new TokenBucketRateLimiter(client);
        // cl.throttle liziba 10 5 60 1
        for (int i = 1; i <= 15; i++) {
            boolean success = limiter.isActionAllowed("liziba", 10, 5, 60, 1);
            System.out.println("第" + i + "次请求" + (success ? "成功" : "失败"));
        }

    }

}

测试结果(这里也说明了令牌桶的容量是max_burst + 1):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码第0次请求成功
第1次请求成功
第2次请求成功
第3次请求成功
第4次请求成功
第5次请求成功
第6次请求成功
第7次请求成功
第8次请求成功
第9次请求成功
第10次请求成功
第11次请求成功
第14次请求失败
第15次请求失败
第14次请求失败
第15次请求失败

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

RocketMQ消息"推送"浅析(上)

发表于 2021-10-04
以往的经验告诉我们:推送模式往往需要Broker与Consumer两者建立长链接,Poducer将消息成功发送至Broker后,Broker会源源不断的主动的将消息推送至Consumer。


而Roket MQ的实现另辟蹊径,ta的推送模式本质上是对拉取模式的包装。将手动拉取的动作交给一个专门的线程负责来实现其自动化。


这意味着Rocket MQ不存在真正的推送模式。

从PullMessageService说起

实现消息拉取自动化的关键就是PullMessageService。

通常了解一个类从ta的继承关系入手会获得巨大回报,但是有时候因为继承关系复杂导致我们无从下手,所幸ta的继承体系比较简单这有利于我们尽快摸清脉络。
image.png
从UML图示中我们能够自然的产生以下联想:

  1. 实现Runnable接口,可能run方法就是消息自动拉取的入口
  2. 基于第一点我们推断可能存在某线程默默支持run方法的运行
  3. 基于前两点和命名约定我们有理由相信ServiceThread必然跟线程有联系
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
arduino复制代码public abstract class ServiceThread implements Runnable {

/* 内部持有一个线程 */
private Thread thread;
/* 线程安全的boolean类型 */
private final AtomicBoolean started = new AtomicBoolean(false);

public void start() {
/* started保证了线程只会被启动一次 */
if (!started.compareAndSet(false, true)) {
return;
}
stopped = false;
/* getServiceName()抽象方法交由子类实现,用以获取线程名称 */
this.thread = new Thread(this, getServiceName());
this.thread.setDaemon(isDaemon);
this.thread.start();
}

}
ServiceThread源码完美契合我们之前的猜想,由此我们可以将焦点转移到PullMessageService的run方法中来。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
scala复制代码public class PullMessageService extends ServiceThread {

/* PullRequest队列 */
private final LinkedBlockingQueue<PullRequest> pullRequestQueue
= new LinkedBlockingQueue<>();

@Override
public void run() {
/* 每次拉取消息都要检测自身状态,改变stopped的状态可以停止拉取消息逻辑 */
while (!this.isStopped()) {
/* pullRequestQueue 队列为空会阻塞 */
PullRequest pullRequest = this.pullRequestQueue.take();
/* 进行消息拉取 */
this.pullMessage(pullRequest);
}
}
}
这显然是一个生产者,消费者模型。pullRequestQueue队列中存在PullRequest对象时,run方法会take出对象,执行拉取逻辑,如果生产者队列为空,那么阻塞队列,暂停消费行为。消息拉取的细节直到此刻我们依然不得见,不妨看看pullMessage方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
scala复制代码public class PullMessageService extends ServiceThread {

private final MQClientInstance mQClientFactory;

public PullMessageService(MQClientInstance mQClientFactory) {
this.mQClientFactory = mQClientFactory;
}

private void pullMessage(PullRequest pullRequest) {
MQConsumerInner consumer = this.mQClientFactory
.selectConsumer(pullRequest.getConsumerGroup());

DefaultMQPushConsumerImpl impl =
(DefaultMQPushConsumerImpl) consumer;

impl.pullMessage(pullRequest);
}

}
也就是说PullMessageService产生实例对象的过程中要求必须传入mQClientFactory对象。通过MQClientInstance#selectConsumer()可以查询到当前pullRequest对应的Consumer对象,然后执行强转逻辑,这里能够直接强转是因为只有推送模式会用到PullMessageService对象。而后将拉取消息的逻辑委托给DefaultMQPushConsumerImp#pullMessage()。


这一瞬间涌入的陌生对象是在太多,如果笔者不加以交代,恐怕读者很难接受,简单的分析一下mQClientFactory对象对应的MQClientInstance类。

MQClientInstance

先看看MQClientInstance对象是怎么诞生的,每一个Consumer启动之时都会填充mQClientFactor成员变量,但是MQClientInstanc设计为单例模式,

一个JVM中的所有消费者、生产者持有同一个MQClientInstance,Consumer在最后会将自己注册进mQClientFactory。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码public class DefaultMQPushConsumerImpl implements MQConsumerInner {

private MQClientInstance mQClientFactory;

public synchronized void start() throws MQClientException {
switch (this.serviceState) {
case CREATE_JUST: {
/* 初始化 MQClientInstance */
mQClientFactory = MQClientManager.getInstance()
.getOrCreateMQClientInstance(this.defaultMQPushConsumer, this.rpcHook);
mQClientFactory.registerConsumer(
this.defaultMQPushConsumer.getConsumerGroup(), this
);
/* 虽然每个Consumer都调用此方法,但只启动一次 */
mQClientFactory.start()
}
default:
break;
}
}

}
简而言之就是MQClientInstance实例的成员变量consumerTable维护者所有的消费者信息,key为ConsumerGroup Name,value就是Consumer本身。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码public class MQClientInstance { 

private final ConcurrentMap<String, MQConsumerInner> consumerTable
= new ConcurrentHashMap<>();

public boolean registerConsumer(String group, MQConsumerInner consumer) {
if (null == group || null == consumer) {
return false;
}

MQConsumerInner prev = this.consumerTable
.putIfAbsent(group, consumer);
return true;
}

}
所以上文中可以通过MQClientInstance对象根据ConsumerGroup Name查询到与之对应的消费者对象。Consumer、MQClientInstance二者相互引用,形成循环依赖关系。

image.png

PullMessageService初始化、启动时机

直到此刻我们PullMessageService依然只是一个普通对象,因为没有start()的线程就像没有物质的爱情,只是一盘散沙,不用风吹,就散了。
  • 我们一直都知道PullMessageService可以查询到消费者,但刚刚我们分析Consumer的启动源码时并未提及。
  • 同时如果你总结一下发现其实同一个JVM中的多个消费者貌似使用的也是同一个PullMessageService对象,不然还有什么必要搜索一下呢。
    看看对应关系,感觉PullMessageService应该与MQClientInstance有联系。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码public class MQClientInstance {

private final PullMessageService pullMessageService;

public MQClientInstance(ClientConfig clientConfig,
int instanceIndex, String clientId, RPCHook rpcHook) {
this.rebalanceService = new RebalanceService(this);
}


public void start() throws MQClientException {
synchronized (this) {
switch (this.serviceState) {
case CREATE_JUST:
this.rebalanceService.start();
break;
default:
break;
}
}
}

}
果不其然MQClientInstance对象执行构造方法,会初始化pullMessageService对象,start方法执行中同时调用了pullMessageService.start()将拉取消息的线程启动。

关于生产

上面笔者介绍了谁负责拉取消息,怎么的机制来拉取,拉取线程又是怎么启动的,但是因为PullMessageService内部是一个典型的生产-消费模型,我们仅仅算是介绍了消费过程。
  • 生产队列如何获得消息拉取请求?
  • 拉取请求的对象又是何时诞生?
  • LinkedBlockingQueue是一个无界队列会不会有内存溢出的风险呢?
    这一个个问题萦绕在我心头,挥之不去。
观察到生产队列是一个privat修饰的成员变量,外界是没有权限访问的,那么基本思路就是查阅一下PullMessageService中操作该属性的方法我们应该就能顺藤摸瓜得到答案。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
scala复制代码public class PullMessageService extends ServiceThread {

public void executePullRequestLater(PullRequest pullRequest,
long timeDelay) {
if (!isStopped()) {
this.scheduledExecutorService.schedule(
() -> executePullRequestImmediately(pullRequest),
timeDelay,
TimeUnit.MILLISECONDS
);
}
}

public void executePullRequestImmediately(PullRequest pullRequest) {
this.pullRequestQueue.put(pullRequest);
}

}
调用这两个方法会为队列生产拉取请求的对象,其中一个会立即往队列中填充,另外一个会交给scheduledExecutorService调度,经过timeDelay的短暂延迟后执行put逻辑。


一路跟踪下去发现所有的调用入口都是DefaultMQPushConsumerImpl的executePullRequestLater和executePullRequestImmediately方法,方法十分简单就是通过自身持有的mQClientFactory对象获取到消息拉取对象然后调用该对象的入队方法。
1
2
3
4
5
6
7
8
9
10
java复制代码private void executePullRequestLater(PullRequest pullRequest, 
long timeDelay) {
this.mQClientFactory.getPullMessageService()
.executePullRequestLater(pullRequest, timeDelay);
}

public void executePullRequestImmediately(PullRequest pullRequest) {
this.mQClientFactory.getPullMessageService()
.executePullRequestImmediately(pullRequest);
}
调用关系如下:

image.png
image.png
可以看到几乎所有的调用都存在于pullMessage方法,而在此方法中有没有看到PullRequest构造实例的过程,排除过后就仅仅只剩下负载均衡实现类RebalancePushImpl。

一路追踪发现原来是在RebalancePushImpl父类RebalanceImpl中构造了一个拉取请求对象。


具体原理就是RebalanceImpl负责ConsumeQueue的负载均衡,故而RebalanceImpl总是第一时间知道每个Consumer分配到到了哪些队列,因此ta就可能根据自己分配得到的队列构造PullRequest对象。此部分的逻辑在updateProcessQueueTableInRebalance中有具体体现。
1
2
3
4
5
6
java复制代码public void dispatchPullRequest(List<PullRequest> pullRequestList) {
for (PullRequest pullRequest : pullRequestList) {
this.defaultMQPushConsumerImpl
.executePullRequestImmediately(pullRequest);
}
}
**构造完毕的PullRequest对象通过dispatchPullRequest进行入队,这里其实就是一切消息拉取的源头。**


介绍到此处,拉取机制最重要的核心原理、重要机制算是都讲解完毕了。接下来我们就看看,消息拉取动作触发,具体处理委托给DefaultMQPushConsumerImpl#pullMessage()之后还有哪些实现细节。我准备从三个对象作为切入点。

三个重要对象的说明

我们先来看看与本文关系最密切的拉取请求对象的构成
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
arduino复制代码public class PullRequest {

/* 消费组 */
private String consumerGroup;

/* 待拉取消费队列 */
private MessageQueue messageQueue;

/* 消息处理队列,从Broker拉取到的消息会存放至此队列,而后提交到消费者消费线程池 */
private ProcessQueue processQueue;

/* 下一次拉取的启始偏移量 */
private long nextOffset;

}
属性构成比较简单,主要是为了引申出另外两个关键对象MessageQueue、ProcessQueue。
1
2
3
4
5
6
7
8
java复制代码public class MessageQueue implements Comparable<MessageQueue>, 
Serializable {

private String topic;
private String brokerName;
private int queueId;

}
1
2
3
4
5
6
7
8
9
10
java复制代码public class ProcessQueue {

/* 消息临时存储容器 TreeMap<消息在ConsumerQueue中的偏移量,消息实体> */
private final TreeMap<Long, MessageExt> msgTreeMap = new TreeMap<>();
/* ProcessQueue中消息数量 */
private final AtomicLong msgCount = new AtomicLong();
/* ProcessQueue消息占用内存总和 */
private final AtomicLong msgSize = new AtomicLong();

}

⚠️:ProcessQueue仅仅摘录了本文相关的属性,实际上ta比这个远远复杂的多。

为了方便说明上述两个对象的意义与用途,笔者准备了一幅示意图,相信通过图示能够比较容易说明。

image.png
MessageQueue功能比较单一,ta就像一个身份证,我通过该对象就可以确认唯一一个队列,如下MessageQueue对象就指定的是图中的queue0队列。

1
2
3
4
5
json复制代码{
"topic": "topic",
"brokerName": "broker0",
"queueId": 0
}
ProcessQueue官方定义为:Queue consumption snapshot,即queue在Client端的一个快照。其实我觉得官方定义的并不太准确,在我看来ta是queue的一个片段,ta保存着queue中的部分消息,而且还保管其他消费相关的信息比如是否能触发Client的消费流控,是否被丢弃等等关键信息。

DefaultMQPushConsumerImpl#pullMessage

结合前文我们已经知道拉取消息的请求对象是负载均衡的相关实现中诞生的,也是由ta将各个队列对应的拉取请求入队,而后被take()阻塞的线程被唤醒,执行pullMessage的操作,Consumer的pullMessage方法中又会分情况将各个PullRequest重新放回队列,如此循环往复,不停拉取。


由于PullRequest与分配到的queue是严格一一对应,所以根本不用关心过多产生这个对象,即使是无界队列也完全没问题。


pullMessage方法实在太长,摘录一下关键代码进行分析:
  1. check状态,根据情况决定是舍弃还是延迟该请求
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
java复制代码public void pullMessage(PullRequest pullRequest) {
/* 通过 pullRequest 获取到 processQueue */
ProcessQueue processQueue = pullRequest.getProcessQueue();
/**
* 由于Consumer节点,或者Broker Queue数量变换,导致负载均衡结果变动
* 有可能之前分布在此 Consumer 节点上的 Queue 会被分配到别处,
* 此时该 Queue 对应的快照 processQueue,本节点无权消费,置于被丢弃状态
*/
if (processQueue.isDropped()) {
/*
* 这里是直接丢弃pullRequest
* 除非下一次负载均衡将该队列又分配回来,否则请求再也没有循环流转的机会
*/
return;
}

/* 检查当前Consumer运行状态是否ServiceState.RUNNING,不是则直接抛出异常 */
try {
this.makeSureStateOK();
} catch(MQClientException e) {
/* 放弃本次,默认延迟3s再次拉取,pullRequest又入队了 */
executePullRequestLater(pullRequest, pullTimeDelayMillsWhenException);
return;
}

/* 若当前消费者被挂起,则将拉取任务延迟1s放至 PullMessageService 的拉取队列中,结束本次拉取 */
if (this.isPause()) {
/* 同样pullRequest又入队了 */
executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_SUSPEND);
return;
}
}
  1. 判断是否需要触发流量控制,触发后请求对象默认延迟50ms后再次入队
  2. 众多条件包装成请求对象RemotingCommand
  3. 执行RPC拉取消息
  4. 执行回调,修改下一次拉取消息起始偏移量,根据状态决定是立即还是延迟将PullRequest对象入队等待执行下一次的拉取
    总结一下:
    image.png

如果本文对您有用,冒昧求一个赞👍

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

关于常见的组织树结构实现方法

发表于 2021-10-04

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

0 前言

因关于组织树在项目中常见,且功能基本一致,故整理一下之前遇到的相关实现,持续更新相关实现啊.

1 组织树形结构 (递归实现)

1 部门组织类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码@Data
public class Org {

@ApiModelProperty(value = "部门ID")
private String orgId;

@ApiModelProperty(value = "部门名称")
private String orgName;

@ApiModelProperty(value = "父部门ID")
private String parentOrgId;

// ... 还有其他字段如 org_path org_level
}

2 组织树形类

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
java复制代码@Data
public class OrgTreeNode {
private String id;
private String label;
private String parentId;
private Boolean disabled;
private String isLeaf;
private String isHidden;
private String orgPath;
private String ownerUserIds;
private String ownerUserNames;
private int sortNo;
private Object data;

private List<OrgTreeNode> children;

public void addChildren(OrgTreeNode orgTree) {
if (children == null) {
children = new ArrayList<>();
}
children.add(orgTree);
}

public void addChildrens(List<OrgTreeNode> orgTreeList) {
if (children == null) {
children = new ArrayList<>();
}
children.addAll(orgTreeList);
}

}

3 树形工具类

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
java复制代码@Slf4j
public class OrgTreeMenuUtil {

/*
* 排序,根据order排序
*/
public Comparator<OrgTreeNode> order() {
return (o1, o2) -> {
if (o1.getSortNo() != o2.getSortNo()) {
return o1.getSortNo() - o2.getSortNo();
}
return 0;
};
}

public List<OrgTreeNode> findTree(List<OrgTreeNode> allMenu, String rootNode) {
List<OrgTreeNode> rootMenu = new ArrayList<>();
try {
// 根节点
for (OrgTreeNode nav : allMenu) {
// rootNode为根节点
if (rootNode.equals(nav.getParentId())) {
rootMenu.add(nav);
}
}
// 根据Menu类的order排序
rootMenu.sort(order());
// 为根菜单设置子菜单,getClild是递归调用的
for (OrgTreeNode nav : rootMenu) {
// 获取根节点下的所有子节点 使用getChild方法
List<OrgTreeNode> childList = getChild(nav.getId(), allMenu);
// 给根节点设置子节点
nav.setChildren(childList);
}
// 输出构建好的菜单数据
return rootMenu;
} catch (Exception e) {
log.error(e.getMessage());
return Collections.emptyList();
}
}

/**
* 获取子节点
* @param id 父节点id
* @param allMenu 所有菜单列表
* @return 每个根节点下,所有子菜单列表
*/
public List<OrgTreeNode> getChild(String id,List<OrgTreeNode> allMenu){
// 子菜单
List<OrgTreeNode> childList = new ArrayList<OrgTreeNode>();
for (OrgTreeNode nav : allMenu) {
// 遍历所有节点,将所有菜单的父id与传过来的根节点的id比较
// 相等说明:为该根节点的子节点。
if (id.equals(nav.getParentId())){
childList.add(nav);
}
}
// 递归
for (OrgTreeNode nav : childList) {
nav.setChildren(getChild(nav.getId(), allMenu));
}
// 排序
childList.sort(order());
// 如果节点下没有子节点,返回一个空List(递归退出)
if (CollectionUtils.isEmpty(childList)){
return new ArrayList<OrgTreeNode>();
}
return childList;
}
}

4 使用说明

首先,将所有部门信息查询出来,转换为OrgTreeNode对象, 再调用OrgTreeMenuUtil工具类的findTree方法,并传入OrgTreeNode对象和组织根节点. 工具类会使用递归,将组织结构封装.

另关于org_path和org_level的编辑时,对于子部门的修改问题, 对于org_path修改:(==分隔符建议使用,尽量少用/==, 如: 阿里巴巴,采购部,手机业务部)

1 先使用原路径+上一级路径, 去进行模糊查询,把所有子部门查询出来.

2 使用字符串的切割,将路径前一半去掉,拼上最新的前缀.

3 更新所有子部门的org_path字段.

对于org_level修改:(如: 阿里巴巴,采购部,手机业务部 就是第三等级部门)

1 本字段依赖于org_path字段,看org_path字段中的分隔符能拆分为几部分,即为那个等级.

2 组织树形结构 (非递归实现)

1 部门组织类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码@Data
public class Org {

@ApiModelProperty(value = "部门ID")
private String orgId;

@ApiModelProperty(value = "部门名称")
private String orgName;

@ApiModelProperty(value = "父部门ID")
private String orgParentId;

@ApiModelProperty(value = "子部门集合")
private List<Org> children;

// ... 还有其他字段如 org_path org_level
}

2 树形工具类

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
java复制代码@Slf4j
public class OrgTreeMenuUtil {

/**
* 生成组织树
*
* @param list
* @param root 根节点id
* @return
*/
public List<Org> getTree(List<Org> list, String root) {
List<Org> orgList = new ArrayList<>();
Map<String, Org> parentObjs = new HashMap<>();
// 找出所有的一级菜单
for (Org org : list) {
if (org.getOrgParentId().equals(root)) {
orgList.add(org);
}
// 记录所有菜单的id及对应的实体
parentObjs.put(org.getOrgId(), org);
}

// 把每个组织加到父组织的子集中
for (Org org : list) {
// 如果是一级菜单,不需要找它的父级
if (org.getOrgParentId().equals(root)) {
continue;
}
// 每个组织找到自己的直接父级,并加入到父级的子集中
Org parentObj = parentObjs.get(org.getOrgParentId());

// 如果还没有子组织集合,新建一个集合并添加子组织
if (Objects.isNull(parentObj.getChildren())) {
parentObj.setChildren(new ArrayList<>());
}
parentObj.getChildren().add(org);
}
return orgList;

}

}

用空间换时间的做法,不用循环递归去封装,直接通过两次遍历,完成组织树的封装.

3 部门和人员的树形结构

1 组织用户对象

1
2
3
4
5
6
7
8
9
java复制代码@Data
public class OrgDropdownListRes {
private String id;
private String pId;
private String isLeaf;
private String value;
private String title;
private List<Person> persons;
}

2 用户对象

1
2
3
4
5
6
7
8
java复制代码@Data
public class Person {
private String id;
private String username;
private String password;
private String orgId;
private Integer gender;
}

3 部门集合与用户集合合并代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码	    private List<OrgDropdownListRes> convertOrg(List<Org> orgList, List<Person> personList) {
List<OrgDropdownListRes> resList = Lists.newArrayList();
orgList.forEach(o -> {
OrgDropdownListRes res = new OrgDropdownListRes();
res.setId(o.getOrgId());
res.setPId(o.getParentOrgId());
res.setValue(o.getOrgId());
res.setTitle(o.getOrgName());
res.setIsLeaf(SysConstants.IS_LEAF_1.equals(o.getIsLeaf()));
if (!CollectionUtils.isEmpty(personList)) {
Map<String, List<Person>> stringListMap = personList.stream().collect(Collectors.groupingBy(Person::getOrgId));
res.setPersons(stringListMap.getOrDefault(o.getOrgId(), Collections.emptyList()));
}
resList.add(res);
});
return resList;
}

查询所有部门 和 所有人员(根据部门id 和 企业id),通过Stream流将人员集合变成map结构,遍历部门,set用户数据.

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

原来可以用Python代码画出这么漂亮有仙境感的樱花树 樱花

发表于 2021-10-04

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

樱花树

在这里插入图片描述
在这里插入图片描述
了解命令说明,有利于了解源码是怎么运行的

screensize(width, height, bg=color) 设置画幕大小及颜色
setup(p1, p2) 设置画幕大小,当 p1、p2 为小数表示屏幕占比;当 p1、p2 为整数表示像素
tracer(speed) 设置绘制速度,speed越大表示绘制速度越快
penup() 起笔(可以想象成画画的时候沾墨之后提笔)
forward() 向前移动
backward() 向后移动
left(degree) 逆时针旋转 degree 度
right(degree) 顺时针旋转 degree 度
pendown() 落笔
pencolor(color) 笔墨颜色为 color
circle(r) 画一个半径为 r 的圆

源码分享

可以直接复制下面的源码去运行,不会出错。

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
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
python复制代码import turtle

from random import random
from random import randint


def draw_petal(turtle_obj, flower):
# 绘制掉落的花瓣
for i in range(int(flower)):
# 有正有负就可以让画笔往二个方向走
x = flower - 4 * flower * random()

# 花瓣整体宽度(-10, 10)
y = 10 - 20 * random()

# 提笔,向前y,左转90,走x,落笔
turtle_obj.penup()
turtle_obj.forward(y)
turtle_obj.left(90)
turtle_obj.forward(x)
turtle_obj.pendown()

# 珊瑚色
turtle_obj.pencolor("lightcoral")
# 画圆
turtle_obj.circle(1)

# 回到起点
# 提笔,后退x,右转90,后退y,落笔
turtle_obj.penup()
turtle_obj.backward(x)
turtle_obj.right(90)
turtle_obj.backward(y)
turtle_obj.pendown()


# 画树枝部分
def draw_tree(turtle_obj, branch, tree_color):
# 设置一个最小分支长度
min_branch = 4

if branch > min_branch:
if branch < 8:
# 以0.5的概率,向左、右分支
if randint(0, 1) == 0:
# 左为白色
turtle_obj.pencolor("snow")
else:
# 右为珊瑚色
turtle_obj.pencolor("lightcoral")
# 枝干
turtle_obj.pensize(branch / 2)
elif 8 <= branch <= 16:
# 以0.33的概率,分为左、中、右分支
if randint(0, 2) == 0:
# 左为白色
turtle_obj.pencolor("snow")
else:
# 中、右为珊瑚色
turtle_obj.pencolor("lightcoral")
# 树枝
turtle_obj.pensize(branch / 4)
else:
# 褐色
turtle_obj.pencolor(tree_color)
# 细枝
turtle_obj.pensize(branch / 10)

# 最开始的树干长度
turtle_obj.forward(branch)

# 随机度数因子
a = 1.5 * random()
# 顺时针旋转随机角度(0~30度)
turtle_obj.right(20 * a)

# 随机长度因子
b = 1.5 * random()
# 往右画,直到画不动为止
draw_tree(turtle_obj, branch - 10 * b, tree_color)

# 左转随机角度
turtle_obj.left(40 * a)
# 往左画,直到画不动位置
draw_tree(turtle_obj, branch - 10 * b, tree_color)

# 右转一定角度
turtle_obj.right(20 * a)
# 提笔
turtle_obj.penup()

# 递归结束回到起点
turtle_obj.backward(branch)
turtle_obj.pendown()


def get_screen(width, height, color, speed):
# 创建画幕
screen_obj = turtle.Screen()
# 画布大小:(width, height),颜色:color
screen_obj.screensize(width, height, bg=color)
screen_obj.setup(1.0, 1.0)
# speed倍加速
screen_obj.tracer(speed)

return screen_obj

# 绘制多棵树
def trees(tree_num):
# 颜色
color = ['brown', 'tan', 'black']

for j in range(tree_num):
# 树干颜色
tree_color = color[randint(0, len(color) - 1)]

# 画笔大小
pensize = randint(2, 5)
# 前进像素
forward = ((-1) ** pensize) * pensize * randint(20, 50)
# 后退像素
if pensize <= 3:
backward = ((-1) ** pensize) * (5 - pensize) * randint(10, 15)
else:
backward = pensize * randint(45, 50)

# 创建画笔
turtle_obj = turtle.Turtle()
# 画笔粗细
turtle_obj.pensize(pensize)
# 提笔,向前forward,左转90,backward,落笔
turtle_obj.penup()
turtle_obj.forward(forward)
turtle_obj.left(90)
turtle_obj.backward(backward)
turtle_obj.pendown()
# 画笔颜色:褐色
turtle_obj.pencolor(tree_color)

# 枝干粗细
branch = pensize * 15
# 落花数
flowers = branch
# 第j棵树
draw_tree(turtle_obj, branch, tree_color)
# 花瓣
draw_petal(turtle_obj, flowers)


if __name__ == '__main__':

# 创建画幕
my_screen_width = 800
my_screen_height = 600
my_screen_color = 'wheat'
my_screen_speed = 5
my_screen_obj = get_screen(my_screen_width, my_screen_height,
my_screen_color, my_screen_speed)

# 樱花树
# 棵数
my_tree_num = 5
trees(my_tree_num)

# 点击关闭画布
my_screen_obj.exitonclick()

更多有趣源码分享,可以关注我。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…508509510…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%