EventBus 开源库学习(三)

源码细节阅读

上一节根据EventBus的使用流程把实现源码大体梳理了一遍,因为精力有限,所以看源码都是根据实现过程把基本流程看下,中间实现细节先忽略,否则越看越深不容易把握大体思路,这节把一些细节的部分再看看。

注解函数查找源码逻辑

#EventBus   
 public void register(Object subscriber) {
        Class<?> subscriberClass = subscriber.getClass();
        List<SubscriberMethod> subscriberMethods = subscriberMethodFinder.findSubscriberMethods(subscriberClass);
        synchronized (this) {
            for (SubscriberMethod subscriberMethod : subscriberMethods) {
                subscribe(subscriber, subscriberMethod);
            }
        }
    }

在进行注册的时候,我们使用subscriberMethodFinder对象的findSubscriberMethods方法来查找到所有该类中以@Subscribe注解的函数(以下简称为注解函数)。让我们继续看下查找部分的逻辑。

    List<SubscriberMethod> findSubscriberMethods(Class<?> subscriberClass) {
        List<SubscriberMethod> subscriberMethods = METHOD_CACHE.get(subscriberClass);
        if (subscriberMethods != null) {
            return subscriberMethods;
        }

        if (ignoreGeneratedIndex) {
            subscriberMethods = findUsingReflection(subscriberClass);
        } else {
            subscriberMethods = findUsingInfo(subscriberClass);
        }
        if (subscriberMethods.isEmpty()) {
            throw new EventBusException("Subscriber " + subscriberClass
                    + " and its super classes have no public methods with the @Subscribe annotation");
        } else {
            METHOD_CACHE.put(subscriberClass, subscriberMethods);
            return subscriberMethods;
        }
    }

METHOD_CACHE是一个ConcurrentHashMap类型的常量,用来保存subscriberClass和对应SubscriberMethod的集合,以提高注册效率,防止重复查找。

如果METHOD_CACHE缓存中不存在,判断变量ignoreGeneratedIndex的值,该值是在EventBusBuilder中初始化的,表示是否忽略注解生成器,默认是false。然后会进入到findUsingInfo函数中进行查找(下面再看),最后将找到的subscriberMethods添加到METHOD_CACHE中。

    private List<SubscriberMethod> findUsingInfo(Class<?> subscriberClass) {
        FindState findState = prepareFindState();
        findState.initForSubscriber(subscriberClass);
        ````
        return getMethodsAndRelease(findState);
    }

先看findUsingInfo前两行,出现一个FindState类,它是SubscriberMethodFinder的内部类,用来辅助查找订阅事件的方法。

    private FindState prepareFindState() {
        synchronized (FIND_STATE_POOL) {
            for (int i = 0; i < POOL_SIZE; i++) {
                FindState state = FIND_STATE_POOL[i];
                if (state != null) {
                    FIND_STATE_POOL[i] = null;
                    return state;
                }
            }
        }
        return new FindState();
    }


    static class FindState {
        ```
        void initForSubscriber(Class<?> subscriberClass) {
            this.subscriberClass = clazz = subscriberClass;
            skipSuperClasses = false;
            subscriberInfo = null;
        }
     }

prepareFindState()方法主要是返回一个FindState对象,先从FIND_STATE_POOL这个缓存池中获取一个现成的,如果没有就新建一个对象。获取对象后调用初始化方法,将subscriberClass赋值给clazz变量,subscriberInfo对象赋值为null

    private List<SubscriberMethod> findUsingInfo(Class<?> subscriberClass) {
        FindState findState = prepareFindState();
        findState.initForSubscriber(subscriberClass);
        while (findState.clazz != null) {
            findState.subscriberInfo = getSubscriberInfo(findState);
            if (findState.subscriberInfo != null) {
                ```
            } else {
                findUsingReflectionInSingleClass(findState);
            }
            findState.moveToSuperclass();
        }
        return getMethodsAndRelease(findState);
    }

再回到findUsingInfo,因为findState.clazz刚赋值过,所以不为空。再来看下getSubscriberInfo方法。

    private SubscriberInfo getSubscriberInfo(FindState findState) {
        if (findState.subscriberInfo != null && findState.subscriberInfo.getSuperSubscriberInfo() != null) {
            SubscriberInfo superclassInfo = findState.subscriberInfo.getSuperSubscriberInfo();
            if (findState.clazz == superclassInfo.getSubscriberClass()) {
                return superclassInfo;
            }
        }
        if (subscriberInfoIndexes != null) {
            for (SubscriberInfoIndex index : subscriberInfoIndexes) {
                SubscriberInfo info = index.getSubscriberInfo(findState.clazz);
                if (info != null) {
                    return info;
                }
            }
        }
        return null;
    }

前面执行initForSubscriber的时候,subscriberInfo赋值为null,因此第一个if不成立。第二个判断是对象subscriberInfoIndexes,该变量也是在EventBusBuilder中初始化的,默认是null,因此该方法返回值null

    private List<SubscriberMethod> findUsingInfo(Class<?> subscriberClass) {
        FindState findState = prepareFindState();
        findState.initForSubscriber(subscriberClass);
        while (findState.clazz != null) {
            findState.subscriberInfo = getSubscriberInfo(findState);
            if (findState.subscriberInfo != null) {
                ```
            } else {
                findUsingReflectionInSingleClass(findState);
            }
            findState.moveToSuperclass();
        }
        return getMethodsAndRelease(findState);
    }

再回到findUsingInfo,由于getSubscriberInfo返回为null,逻辑会走到findUsingReflectionInSingleClass方法中,使用反射来获取所有方法(下面再看)。

获取当前类所有的注解方法后,findState.moveToSuperclass()表示修改findState.clazzsubscriberClass的父类Class,继续遍历父类中的注解方法。

private static final int MODIFIERS_IGNORE = Modifier.ABSTRACT | Modifier.STATIC | BRIDGE | SYNTHETIC;

private void findUsingReflectionInSingleClass(FindState findState) {
        Method[] methods;
        try {
            // This is faster than getMethods, especially when subscribers are fat classes like Activities
            methods = findState.clazz.getDeclaredMethods();
        } catch (Throwable th) {
            // Workaround for java.lang.NoClassDefFoundError, see https://github.com/greenrobot/EventBus/issues/149
            ```
        }
        for (Method method : methods) {
            int modifiers = method.getModifiers();
            if ((modifiers & Modifier.PUBLIC) != 0 && (modifiers & MODIFIERS_IGNORE) == 0) {
                Class<?>[] parameterTypes = method.getParameterTypes();
                if (parameterTypes.length == 1) {
                    Subscribe subscribeAnnotation = method.getAnnotation(Subscribe.class);
                    if (subscribeAnnotation != null) {
                        Class<?> eventType = parameterTypes[0];
                        if (findState.checkAdd(method, eventType)) {
                            ThreadMode threadMode = subscribeAnnotation.threadMode();
                            findState.subscriberMethods.add(new SubscriberMethod(method, eventType, threadMode,
                                    subscribeAnnotation.priority(), subscribeAnnotation.sticky()));
                        }
                    }
                } else if (strictMethodVerification && method.isAnnotationPresent(Subscribe.class)) {
                    String methodName = method.getDeclaringClass().getName() + "." + method.getName();
                    throw new EventBusException("@Subscribe method " + methodName +
                            "must have exactly 1 parameter but has " + parameterTypes.length);
                }
            } else if (strictMethodVerification && method.isAnnotationPresent(Subscribe.class)) {
                String methodName = method.getDeclaringClass().getName() + "." + method.getName();
                throw new EventBusException(methodName +
                        " is a illegal @Subscribe method: must be public, non-static, and non-abstract");
            }
        }
    }

上面代码流程如下:

  • 通过方法getDeclaredMethods()获取该类中所有声明的方法列表;
  • 遍历方法列表,获取方法的修饰符,如果修饰符是public并且不是abstractstatic等进入下一步,MODIFIERS_IGNORE定义如第一行代码;
  • 获取方法的参数列表,如果参数的个数是1,并且使用了Subscribe注解,获取唯一的入参进行下一步;
  • 然后checkAdd()方法用来判断FindState的anyMethodByEventType map是否已经添加过以当前eventTypekey的键值对,没添加过则返回true(就是看看当前类中有没有多个事件相同的注解函数);
  • 通过注解对象subscribeAnnotation获取threadMode,然后新建SubscriberMethod添加到findState.subscriberMethods集合中。

分析完findUsingReflectionInSingleClass这个方法后,我们回到findUsingInfo

    private List<SubscriberMethod> findUsingInfo(Class<?> subscriberClass) {
        FindState findState = prepareFindState();
        findState.initForSubscriber(subscriberClass);
        while (findState.clazz != null) {
            findState.subscriberInfo = getSubscriberInfo(findState);
            if (findState.subscriberInfo != null) {
                ```
            } else {
                findUsingReflectionInSingleClass(findState);
            }
            findState.moveToSuperclass();
        }
        return getMethodsAndRelease(findState);
    }

遍历当前类及其父类,通过findUsingReflectionInSingleClass方法找到所有注解方法后,通过getMethodsAndRelease返回所有相关方法。

    private List<SubscriberMethod> getMethodsAndRelease(FindState findState) {
        List<SubscriberMethod> subscriberMethods = new ArrayList<>(findState.subscriberMethods);
        findState.recycle();
        synchronized (FIND_STATE_POOL) {
            for (int i = 0; i < POOL_SIZE; i++) {
                if (FIND_STATE_POOL[i] == null) {
                    FIND_STATE_POOL[i] = findState;
                    break;
                }
            }
        }
        return subscriberMethods;
    }

通过上面的解析知道,所有的SubscriberMethod都添加到findState.subscriberMethods集合中,这个方法就是把findState.subscriberMethods集合中的内容复制出来,然后释放findState中的资源,并把findState放到FIND_STATE_POOL缓存中,POOL_SIZE常量是4,这样下次查找的时候可以重复利用无需每次都新建对象。

以上为注解方法查找的过程。在上面分析的过程中出现了两个变量subscriberInfoIndexesignoreGeneratedIndex看样子都是关于索引的,分析的时候都是使用的EventBusBuilder中的默认值。看下subscriberInfoIndexes赋值的地方,看注释意思是:将EventBus注解处理器生成索引添加添加进来。所以注解处理器需要再了解下。

    /** Adds an index generated by EventBus' annotation preprocessor. */
    public EventBusBuilder addIndex(SubscriberInfoIndex index) {
        if (subscriberInfoIndexes == null) {
            subscriberInfoIndexes = new ArrayList<>();
        }
        subscriberInfoIndexes.add(index);
        return this;
    }

Subscriber Index注解处理器

通过上面分析可知,EventBus注册事件时,主要是在项目运行时通过反射来查找注解方法信息,如果项目中有大量的注解方法,必然会对项目运行时的性能产生影响。

除了在项目运行时通过反射查找注解方法信息,EventBus 还提供了在项目编译时通过注解处理器查找注解方法信息的方式,生成一个辅助的索引类来保存这些信息,这个索引类就是Subscriber Index。

1、Subscriber Index使用方式
首先要在 appbuild.gradle中加入如下配置:

android {
    defaultConfig {
        javaCompileOptions {
            annotationProcessorOptions {
                // 指定辅助索引类的名称和包名,注意,这个类时自动生成的,不需要我们写
                arguments = [ eventBusIndex : 'com.jane.demo.MyEventBusIndex' ]
            }
        }
    }
}
dependencies {
    compile 'org.greenrobot:eventbus:3.3.1'
    // 引入注解处理器
    annotationProcessor 'org.greenrobot:eventbus-annotation-processor:3.3.1'
}

在项目的Application中添加如下配置,以生成一个默认的 EventBus 单例:

EventBus.builder().addIndex(new MyEventBusIndex()).installDefaultEventBus();

此时重新编译下项目,会生成一个MyEventBusIndex类(不同的gradle版本生成的位置可能不一样)。
MyEventBusIndex.png

生成的MyEventBusIndex代码如下:

public class MyEventBusIndex implements SubscriberInfoIndex {
    private static final Map<Class<?>, SubscriberInfo> SUBSCRIBER_INDEX;

    static {
        SUBSCRIBER_INDEX = new HashMap<Class<?>, SubscriberInfo>();

        putIndex(new SimpleSubscriberInfo(EventBusService.class, true, new SubscriberMethodInfo[] {
            new SubscriberMethodInfo("onMsgEventReceived", String.class),
            new SubscriberMethodInfo("onMsgEventReceived", MsgEvent.class),
            new SubscriberMethodInfo("onMsgEventReceived", org.greenrobot.eventbus.NoSubscriberEvent.class),
        }));

        putIndex(new SimpleSubscriberInfo(EventBusActivity.class, true, new SubscriberMethodInfo[] {
             new SubscriberMethodInfo("onMsgEventReceived", Event.class),
        }));
    }

    private static void putIndex(SubscriberInfo info) {
        SUBSCRIBER_INDEX.put(info.getSubscriberClass(), info);
    }

    @Override
    public SubscriberInfo getSubscriberInfo(Class<?> subscriberClass) {
        SubscriberInfo info = SUBSCRIBER_INDEX.get(subscriberClass);
        if (info != null) {
            return info;
        } else {
            return null;
        }
    }
}

代码中SUBSCRIBER_INDEX是一个HashMap,保存了当前注册类的Class类型和其中注解方法的信息。

2、Subscriber Index源码

EventBus.builder().addIndex(new MyEventBusIndex()).installDefaultEventBus();

使用的时候,获取EventBusBuilder对象,然后调用addIndex方法,把生成的MyEventBusIndex加入进去。

    /** Adds an index generated by EventBus' annotation preprocessor. */
    public EventBusBuilder addIndex(SubscriberInfoIndex index) {
        if (subscriberInfoIndexes == null) {
            subscriberInfoIndexes = new ArrayList<>();
        }
        subscriberInfoIndexes.add(index);
        return this;
    }

上面的方法是不是比较眼熟,就是上面代码分析的时候出现的变量subscriberInfoIndexes赋值函数。这样,运行的时候就把之前编译好的索引类的实例保存在subscriberInfoIndexes集合中。然后调用installDefaultEventBus方法创建EventBus实例。

    /**
     * Installs the default EventBus returned by {@link EventBus#getDefault()} using this builders' values. Must be
     * done only once before the first usage of the default EventBus.
     *
     * @throws EventBusException if there's already a default EventBus instance in place
     */
    public EventBus installDefaultEventBus() {
        synchronized (EventBus.class) {
            if (EventBus.defaultInstance != null) {
                throw new EventBusException("Default instance already exists." +
                        " It may be only set once before it's used the first time to ensure consistent behavior.");
            }
            EventBus.defaultInstance = build();
            return EventBus.defaultInstance;
        }
    }

    /** Builds an EventBus based on the current configuration. */
    public EventBus build() {
        return new EventBus(this);
    }

上面通过build方法,以当前EventBusBuilder对象作为参数生成EventBus单例对象,并赋值给defaultInstance,这样就把subscriberInfoIndexes信息传递给了EventBus。以后调用EventBus getDefault()返回的就是这里生成的单例对象,里面包含了subscriberInfoIndexes信息。

而且在Application中生成了 EventBus 的默认单例,这样就保证了在项目其它地方执行EventBus.getDefault()就能得到就是上面包含subscriberInfoIndexes信息的单例。

在上面分析查找注解函数的时候,分析过一个函数:

    private List<SubscriberMethod> findUsingInfo(Class<?> subscriberClass) {
        FindState findState = prepareFindState();
        findState.initForSubscriber(subscriberClass);
        while (findState.clazz != null) {
            findState.subscriberInfo = getSubscriberInfo(findState);
            if (findState.subscriberInfo != null) {
                ····
            } else {
                findUsingReflectionInSingleClass(findState);
            }
            findState.moveToSuperclass();
        }
        return getMethodsAndRelease(findState);
    }

如果没有使用注解处理器的话getSubscriberInfo这个方法返回值是null,因此会走到findUsingReflectionInSingleClass,通过反射区获取注解方法。现在使用使用注解处理器的话在重新看下这个函数。

    private SubscriberInfo getSubscriberInfo(FindState findState) {
        if (findState.subscriberInfo != null && findState.subscriberInfo.getSuperSubscriberInfo() != null) {
            SubscriberInfo superclassInfo = findState.subscriberInfo.getSuperSubscriberInfo();
            if (findState.clazz == superclassInfo.getSubscriberClass()) {
                return superclassInfo;
            }
        }
        //不为空
        if (subscriberInfoIndexes != null) {
            for (SubscriberInfoIndex index : subscriberInfoIndexes) {
                SubscriberInfo info = index.getSubscriberInfo(findState.clazz);
                if (info != null) {
                    return info;
                }
            }
        }
        return null;
    }

    @Override
    public SubscriberInfo getSubscriberInfo(Class<?> subscriberClass) {
        SubscriberInfo info = SUBSCRIBER_INDEX.get(subscriberClass);
        if (info != null) {
            return info;
        } else {
            return null;
        }
    }

如果使用了注解处理器,因为subscriberInfoIndexes这个集合不为空,遍历获取当前类对应的注解方法列表, index.getSubscriberInfo这个方法实际调用的就是系统生成的MyEventBusIndex中的getSubscriberInfo方法,这样就获取到了之前编译生成的注解方法。

    private List<SubscriberMethod> findUsingInfo(Class<?> subscriberClass) {
        FindState findState = prepareFindState();
        findState.initForSubscriber(subscriberClass);
        while (findState.clazz != null) {
            findState.subscriberInfo = getSubscriberInfo(findState);
            if (findState.subscriberInfo != null) {
                SubscriberMethod[] array = findState.subscriberInfo.getSubscriberMethods();
                for (SubscriberMethod subscriberMethod : array) {
                    if (findState.checkAdd(subscriberMethod.method, subscriberMethod.eventType)) {
                        findState.subscriberMethods.add(subscriberMethod);
                    }
                }
            } else {
                findUsingReflectionInSingleClass(findState);
            }
            findState.moveToSuperclass();
        }
        return getMethodsAndRelease(findState);
    }

通过getSubscriberInfo返回当前类所有注解方法后,遍历所有订阅了事件的方法然后加入到subscriberMethods列表中,其它的和之前的注册流程一样。

参考文章:
EventBus 原理解析

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

STM32的电动自行车信息采集上报系统(学习)

摘要 针对电动自行车实时监管不便的问题&#xff0c;设计了一种基于STM32的电动自行车信息采集系统&#xff0c;通过获取电池、位置和行驶状态信息并上报到服务器中&#xff0c;实现实时监管。 通过多路串口请求电池、行驶状态和位置信息&#xff0c;以并发方式进行数据接收、…

机器学习概述及其主要算法

目录 1、什么是机器学习 2、数据集 2.1、结构 3、算法分类 4、算法简介 4.1、K-近邻算法 4.2、贝叶斯分类 4.3、决策树和随机森林 4.4、逻辑回归 4.5、神经网络 4.6、线性回归 4.7、岭回归 4.8、K-means 5、机器学习开发流程 6、学习框架 1、什么是机器学习 机器…

Linux命令200例:用Look一个进行文本搜索工具

&#x1f3c6;作者简介&#xff0c;黑夜开发者&#xff0c;全栈领域新星创作者✌&#xff0c;阿里云社区专家博主&#xff0c;2023年6月csdn上海赛道top4。 &#x1f3c6;数年电商行业从业经验&#xff0c;历任核心研发工程师&#xff0c;项目技术负责人。 &#x1f3c6;本文已…

Web压测工具http_load原理分析

01、前言 http_load是一款测试web服务器性能的开源工具&#xff0c;从下面的网址可以下载到最新版本的http_load&#xff1a; http://www.acme.com/software/http_load/ 这个软件一直在保持着更新&#xff08;不像webbench&#xff0c;已经是十年的老古董了。 webbench的源…

Pandas

系列文章目录 第一章 python数据挖掘基础环境安装和使用 第二章 Matplotlib 第三章 Numpy 文章目录 系列文章目录一、介绍1.1 为什么用Pandas&#xff1f;1.2 核心数据结构1.3 DataFrame1.3.1 结构1.3.2 常用属性1.3.3 常用方法1.3.4 DataFrame索引的设置修改行列索引值重设索…

小白电脑装机(自用)

几个月前买了配件想自己装电脑&#xff0c;结果最后无法成功点亮&#xff0c;出现的问题是主板上的DebugLED黄灯常亮&#xff0c;即DRAM灯亮。对于微星主板的Debug灯&#xff0c;其含义这篇博文中有说明。 根据另一篇博文&#xff0c;有两种可能。 我这边曾将内存条和主板一块…

gin框架学习

文章目录 配置go环境实现一个简单的web响应服务验证功能gin增加页面以及传递数据 配置go环境 去go官网下载对应的版本 go下载地址 tar -C /usr/local -xzf go1.4.linux-amd64.tar.gz 我们可以编辑 ~/.bash_profile 或者 /etc/profile&#xff0c;并将以下命令添加该文件的末…

【新版系统架构补充】-传输介质、子网划分

传输介质 双绞线&#xff1a;无屏蔽双绞线UTP和屏蔽双绞线STP&#xff0c;传输距离在100m内 网线安装标准&#xff1a; 光纤&#xff1a;由纤芯和包层组成&#xff0c;分多模光纤MMF、单模光纤SMF 无线信道&#xff1a;分为无线电波和红外光波 通信方式和交换方式 单工…

使用隧道HTTP时如何解决网站验证码的问题?

使用代理时&#xff0c;有时候会遇到网站验证码的问题。验证码是为了防止机器人访问或恶意行为而设置的一种验证机制。当使用代理时&#xff0c;由于请求的源IP地址被更改&#xff0c;可能会触发网站的验证码机制。以下是解决网站验证码问题的几种方法&#xff1a; 1. 使用高匿…

Golang 函数参数的传递方式 值传递,引用传递

基本介绍 我们在讲解函数注意事项和使用细节时&#xff0c;已经讲过值类型和引用类型了&#xff0c;这里我们再系统总结一下&#xff0c;因为这是重难点&#xff0c;值类型参数默认就是值传递&#xff0c;而引用类型参数默认就是引用传递。 两种传递方式&#xff08;函数默认都…

前端主题切换方案——CSS变量

前言 主题切换是前端开发中老生常谈的问题&#xff0c;本文将介绍主流的前端主题切换实现方案——CSS变量 CSS变量 简介 编写CSS样式时&#xff0c;为了避免代码冗余&#xff0c;降低维护成本&#xff0c;一些CSS预编译工具&#xff08;Sass/Less/Stylus&#xff09;等都支…

安防监控国标GB28181平台EasyGBS视频快照无法显示是什么原因?如何解决?

安防视频监控国标视频云服务EasyGBS支持设备/平台通过国标GB28181协议注册接入&#xff0c;并能实现视频的实时监控直播、录像、检索与回看、语音对讲、云存储、告警、平台级联等功能。平台部署简单、可拓展性强&#xff0c;支持将接入的视频流进行全终端、全平台分发&#xff…

nginx的优化和防盗链 重要!!!

实验一、隐藏版本号 要把nginx的版本号隐藏起来&#xff0c;防止恶意攻击 方法一&#xff1a;修改配置文件 在http模块中加入一个命令 server_token off&#xff1b; 过程&#xff1a; 备份&#xff0c;改配置文件一定要备份 修改配置文件 在http模块中添加 server_tokens …

嵌入式开发学习(STC51-6-独立按键)

内容 通过开发板上的独立按键K1控制D1指示灯亮灭 按键简介 按键是一种电子开关&#xff0c;使用时轻轻按开关按钮就可使开关接通&#xff0c;当松开手时&#xff0c;开关断开&#xff1b; 通常的按键所用开关为机械弹性开关&#xff0c;当机械触点断开、闭合时&#xff0c;…

非阻塞IO

非阻塞IO fcntl 一个文件描述符, 默认都是阻塞IO。fcntl可以将某个文件描述符设置为非阻塞IO&#xff0c;先看一下文档介绍。 传入的cmd的值不同&#xff0c;后面追加的参数也不相同。 fcntl函数有5种功能: 复制一个现有的描述符&#xff08;cmd F_DUPFD&#xff09;。获得…

Unity面板究极优化

首先对于大项目来说UI首选一定的UGUI&#xff0c;目前没有啥可选的余地。多一点都是对性能的负担&#xff0c;UGUI底层基于多线程技术&#xff0c;可以有效分担压力&#xff0c;对于一些不是那么重的面板几乎无感。 无论其他面板只是在此基础上修改的&#xff0c;但每多一层&am…

图解架构 | SaaS、PaaS、IaaS/aPaaS平台是什么?aPaaS与PaaS有什么区别?

参考 图解架构 | SaaS、PaaS、IaaS:https://www.51cto.com/article/717315.html aPaaS平台是什么&#xff1f;aPaaS与PaaS有什么区别&#xff1f;&#xff1a;https://developer.aliyun.com/article/718714 aPaaS和PaaS的区别是什么&#xff1f; aPaaS和PaaS都可以完成软件的…

Redis 双写一致性实践及案例

面试问题&#xff1a; 你只要用缓存&#xff0c;就可能会涉及到redis缓存与数据库双存储双写&#xff0c;你只要是双写&#xff0c;就一定会有数据一致性的问题&#xff0c;那么你如何解决一致性问题&#xff1f;双写一致性&#xff0c;你先动缓存redis还是数据库mysql哪一个&…

Linux从安装到实战 常用命令 Bash常用功能 用户和组管理

1.0初识Linux 1.1虚拟机介绍 1.2VMware Workstation虚拟化软件 下载CentOS; 1.3远程链接Linux系统 &FinalShell 链接finalshell半天没连接进去 他说ip adress 看IP地址是在虚拟机上 win11主机是 终端输入&#xff1a; ifconfig VMware虚拟机的设置 & ssh连接_snge…

Word中如何断开表格中线段

Word中如何断开表格中线段_word表格断线怎么弄_仰望星空_LiDAR的博客-CSDN博客有时候为了美观&#xff0c;需要实现如下的效果&#xff0c;即第2条线段被断开成3段步骤如下&#xff1a;选中需要断开的格网&#xff0c;如下&#xff0c;再选择段落、针对下框标即可。_word表格断…