首页 文章 精选 留言 我的

精选列表

搜索[面试],共4924篇文章
优秀的个人博客,低调大师

精选21道Java后端面试题,看完你也能唬住面试官拿30K

微信公众号:慕容千语的架构笔记。欢迎关注一起进步。 1. 如何用数组实现队列? 用数组实现队列时要注意 溢出 现象,这时我们可以采用循环数组的方式来解决,即将数组收尾相接。使用front指针指向队列首位,tail指针指向队列末位。 2. 内部类访问局部变量的时候,为什么变量必须加上final修饰? 因为生命周期不同。局部变量在方法结束后就会被销毁,但内部类对象并不一定,这样就会导致内部类引用了一个不存在的变量。 所以编译器会在内部类中生成一个局部变量的拷贝,这个拷贝的生命周期和内部类对象相同,就不会出现上述问题。 但这样就导致了其中一个变量被修改,两个变量值可能不同的问题。为了解决这个问题,编译器就要求局部变量需要被final修饰,以保证两个变量值相同。 在JDK8之后,编译器不要求内部类访问的局部变量必须被final修饰,但局部变量值不能被修改(无论是方法中还是内部类中),否则会报编译错误。利用javap查看编译后的字节码可以发现,编译器已经加上了final。 3. long s = 499999999 * 499999999 在上面的代码中,s的值是多少? 根据代码的计算结果,s的值应该是-1371654655,这是由于Java中右侧值的计算默认是int类型。 4. NIO相关,Channels、Buffers、Selectors NIO(Non-blocking IO)为所有的原始类型提供(Buffer)缓存支持,字符集编码解码解决方案。 Channel :一个新的原始I/O 抽象。 支持锁和内存映射文件的文件访问接口。提供多路(non-bloking) 非阻塞式的高伸缩性网络I/O 。 IO NIO 面向流 面向缓冲 阻塞IO 非阻塞IO 无 选择器 流与缓冲 Java NIO和IO之间第一个最大的区别是,IO是面向流的,NIO是面向缓冲区的。 Java IO面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。此外,它不能前后移动流中的数据。如果需要前后移动从流中读取的数据,需要先将它缓存到一个缓冲区。 Java NIO的缓冲导向方法略有不同。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。但是,还需要检查是否该缓冲区中包含所有您需要处理的数据。而且,需确保当更多的数据读入缓冲区时,不要覆盖缓冲区里尚未处理的数据。 阻塞与非阻塞IO Java IO的各种流是阻塞的。这意味着,当一个线程调用read() 或 write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。 Java NIO的非阻塞模式,是线程向某通道发送请求读取数据,仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,当然它不会保持线程阻塞。所以直至数据变的可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此。所以一个单独的线程现在可以管理多个输入和输出通道。 选择器(Selectors) Java NIO 的 选择器允许一个单独的线程来监视多个输入通道,你可以注册多个通道使用一个选择器,然后使用一个单独的线程来“选择”通道:这些通道里已经有可以处理的输入,或者选择已准备写入的通道。这种选择机制,使得一个单独的线程很容易来管理多个通道。 5. 反射的用途 Java反射机制可以让我们在编译期(Compile Time)之外的运行期(Runtime)检查类,接口,变量以及方法的信息。反射还可以让我们在运行期实例化对象,调用方法,通过调用get/set方法获取变量的值。同时我们也可以通过反射来获取泛型信息,以及注解。还有更高级的应用–动态代理和动态类加载(ClassLoader.loadclass())。 下面列举一些比较重要的方法: getFields:获取所有 public 的变量。 getDeclaredFields:获取所有包括 private , protected 权限的变量。 setAccessible:设置为 true 可以跳过Java权限检查,从而访问private权限的变量。 getAnnotations:获取注解,可以用在类和方法上。 获取方法的泛型参数: method = Myclass.class.getMethod("setStringList", List.class); Type[] genericParameterTypes = method.getGenericParameterTypes(); for(Type genericParameterType : genericParameterTypes){ if(genericParameterType instanceof ParameterizedType){ ParameterizedType aType = (ParameterizedType) genericParameterType; Type[] parameterArgTypes = aType.getActualTypeArguments(); for(Type parameterArgType : parameterArgTypes){ Class parameterArgClass = (Class) parameterArgType; System.out.println("parameterArgClass = " + parameterArgClass); } } } 动态代理: //Main.java public static void main(String[] args) { HelloWorld helloWorld=new HelloWorldImpl(); InvocationHandler handler=new HelloWorldHandler(helloWorld); //创建动态代理对象 HelloWorld proxy=(HelloWorld)Proxy.newProxyInstance( helloWorld.getClass().getClassLoader(), helloWorld.getClass().getInterfaces(), handler); proxy.sayHelloWorld(); } //HelloWorldHandler.java public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { Object result = null; //调用之前 doBefore(); //调用原始对象的方法 result=method.invoke(obj, args); //调用之后 doAfter(); return result; } 通过反射获取方法注解的参数: Class aClass = TheClass.class; Annotation[] annotations = aClass.getAnnotations(); for(Annotation annotation : annotations){ if(annotation instanceof MyAnnotation){ MyAnnotation myAnnotation = (MyAnnotation) annotation; System.out.println("name: " + myAnnotation.name()); System.out.println("value: " + myAnnotation.value()); } } 非静态内部类能定义静态方法吗? public class OuterClass{ private static float f = 1.0f; class InnerClass{ public static float func(){return f;} } } 以上代码会出现编译错误,因为只有静态内部类才能定义静态方法。 6. Lock 和 Synchronized 有什么区别? 使用方法的区别 Synchronized:在需要同步的对象中加入此控制,synchronized可以加在方法上,也可以加在特定代码块中,括号中表示需要锁的对象。 Lock:需要显示指定起始位置和终止位置。一般使用ReentrantLock类做为锁,多个线程中必须要使用一个ReentrantLock类做为对象才能保证锁的生效。且在加锁和解锁处需要通过lock()和unlock()显示指出。所以一般会在finally块中写unlock()以防死锁。 性能的区别 synchronized是托管给JVM执行的,而lock是java写的控制锁的代码。在Java1.5中,synchronize是性能低效的。因为这是一个重量级操作,需要调用操作接口,导致有可能加锁消耗的系统时间比加锁以外的操作还多。相比之下使用Java提供的Lock对象,性能更高一些。但是到了Java1.6,发生了变化。synchronize在语义上很清晰,可以进行很多优化,有适应自旋,锁消除,锁粗化,轻量级锁,偏向锁等等。导致在Java1.6上synchronize的性能并不比Lock差。 Synchronized:采用的是CPU悲观锁机制,即线程获得的是独占锁。独占锁意味着 其他线程只能依靠阻塞来等待线程释放锁。而在CPU转换线程阻塞时会引起线程上下文切换,当有很多线程竞争锁的时候,会引起CPU频繁的上下文切换导致效率很低。 Lock:用的是乐观锁方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。乐观锁实现的机制就是CAS操作。我们可以进一步研究ReentrantLock的源代码,会发现其中比较重要的获得锁的一个方法是compareAndSetState。这里其实就是调用的CPU提供的特殊指令。 ReentrantLock:具有更好的可伸缩性:比如时间锁等候、可中断锁等候、无块结构锁、多个条件变量或者锁投票。 7. float 变量如何与 0 比较? folat类型的还有double类型的,这些小数类型在趋近于0的时候直接等于0的可能性很小,一般都是无限趋近于0,因此不能用==来判断。应该用|x-0| //用程序表示就是 fabs(x) < 0.00001f 8. 如何新建非静态内部类? 内部类在声明的时候必须是 Outer.Inner a,就像int a 一样,至于静态内部类和非静态内部类new的时候有点区别: Outer.Inner a = new Outer().new Inner()(非静态,先有Outer对象才能 new 内部类) Outer.Inner a = new Outer.Inner()(静态内部类) 9. Java标识符命名规则 可以包含:字母、数字、$、_(下划线),不可用数字开头,不能是 Java 的关键字和保留字。 11. 你知道哪些JDK中用到的设计模式? 装饰模式:java.io 单例模式:Runtime类 简单工厂模式:Integer.valueOf方法 享元模式:String常量池、Integer.valueOf(int i)、Character.valueOf(char c) 迭代器模式:Iterator 职责链模式:ClassLoader的双亲委派模型 解释器模式:正则表达式java.util.regex.Pattern 12. ConcurrentHashMap如何保证线程安全 JDK 1.7及以前: ConcurrentHashMap允许多个修改操作并发进行,其关键在于使用了锁分离技术。它使用了多个锁来控制对hash表的不同部分进行的修改。ConcurrentHashMap内部使用段(Segment)来表示这些不同的部分,每个段其实就是一个小的hash table,它们有自己的锁。只要多个修改操作发生在不同的段上,它们就可以并发进行。 JDK 1.8: Segment虽保留,但已经简化属性,仅仅是为了兼容旧版本。 插入时使用CAS算法:unsafe.compareAndSwapInt(this, valueOffset, expect, update)。 CAS(Compare And Swap)意思是如果valueOffset位置包含的值与expect值相同,则更新valueOffset位置的值为update,并返回true,否则不更新,返回false。插入时不允许key或value为null 与Java8的HashMap有相通之处,底层依然由“数组”+链表+红黑树; 底层结构存放的是TreeBin对象,而不是TreeNode对象; CAS作为知名无锁算法,那ConcurrentHashMap就没用锁了么?当然不是,当hash值与链表的头结点相同还是会synchronized上锁,锁链表。 13. i++在多线程环境下是否存在问题,怎么解决? 虽然递增操作++i是一种紧凑的语法,使其看上去只是一个操作,但这个操作并非原子的,因而它并不会作为一个不可分割的操作来执行。实际上,它包含了三个独立的操作:读取count的值,将值加1,然后将计算结果写入count。这是一个“读取 - 修改 - 写入”的操作序列,并且其结果状态依赖于之前的状态。所以在多线程环境下存在问题。 要解决自增操作在多线程环境下线程不安全的问题,可以选择使用Java提供的原子类,如AtomicInteger或者使用synchronized同步方法。 14. new与newInstance()的区别 new是一个关键字,它是调用new指令创建一个对象,然后调用构造方法来初始化这个对象,可以使用带参数的构造器 newInstance()是Class的一个方法,在这个过程中,是先取了这个类的不带参数的构造器Constructor,然后调用构造器的newInstance方法来创建对象。 Class.newInstance不能带参数,如果要带参数需要取得对应的构造器,然后调用该构造器的Constructor.newInstance(Object … initargs)方法 15. 你了解哪些JDK1.8的新特性? 接口的默认方法和静态方法,JDK8允许我们给接口添加一个非抽象的方法实现,只需要使用default关键字即可。也可以定义被static修饰的静态方法。 对HashMap进行了改进,当单个桶的元素个数大于6时就会将实现改为红黑树实现,以避免构造重复的hashCode的攻击 多并发进行了优化。如ConcurrentHashMap实现由分段加锁、锁分离改为CAS实现。 JDK8拓宽了注解的应用场景,注解几乎可以使用在任何元素上,并且允许在同一个地方多次使用同一个注解 Lambda表达式 16. 你用过哪些JVM参数? Xms 堆最小值 Xmx 堆最大值 Xmn: 新生代容量 XX:SurvivorRatio 新生代中Eden与Surivor空间比例 Xss 栈容量 XX:PermSize 方法区初始容量 XX:MaxPermSize 方法区最大容量 XX:+PrintGCDetails 收集器日志参数 17. 如何打破 ClassLoader 双亲委托? 重写loadClass()方法。 18. hashCode() && equals() hashcode() 返回该对象的哈希码值,支持该方法是为哈希表提供一些优点,例如,java.util.Hashtable 提供的哈希表。 在 Java 应用程序执行期间,在同一对象上多次调用 hashCode 方法时,必须一致地返回相同的整数,前提是对象上 equals 比较中所用的信息没有被修改(equals默认返回对象地址是否相等)。如果根据 equals(Object)方法,两个对象是相等的,那么在两个对象中的每个对象上调用 hashCode 方法都必须生成相同的整数结果。 以下情况不是必需的:如果根据 equals(java.lang.Object) 方法,两个对象不相等,那么在两个对象中的任一对象上调用 hashCode 方法必定会生成不同的整数结果。但是,程序员应该知道,为不相等的对象生成不同整数结果可以提高哈希表的性能。 实际上,由 Object 类定义的 hashCode 方法确实会针对不同的对象返回不同的整数。(这一般是通过将该对象的内部地址转换成一个整数来实现的,但是 JavaTM 编程语言不需要这种实现技巧I。) hashCode的存在主要是用于查找的快捷性,如 Hashtable,HashMap等,hashCode 是用来在散列存储结构中确定对象的存储地址的; 如果两个对象相同,就是适用于 equals(java.lang.Object) 方法,那么这两个对象的 hashCode 一定要相同; 如果对象的 equals 方法被重写,那么对象的 hashCode 也尽量重写,并且产生 hashCode 使用的对象,一定要和 equals 方法中使用的一致,否则就会违反上面提到的第2点; 两个对象的hashCode相同,并不一定表示两个对象就相同,也就是不一定适用于equals(java.lang.Object) 方法,只能够说明这两个对象在散列存储结构中,如Hashtable,他们“存放在同一个篮子里”。 19. Thread.sleep() & Thread.yield() sleep()和yield()都会释放CPU。 sleep()使当前线程进入停滞状态,所以执行sleep()的线程在指定的时间内肯定不会执行;yield()只是使当前线程重新回到可执行状态,所以执行yield()的线程有可能在进入到可执行状态后马上又被执行。 sleep()可使优先级低的线程得到执行的机会,当然也可以让同优先级和高优先级的线程有执行的机会;yield()只能使同优先级的线程有执行的机会。 20. #{}和${}的区别是什么? {}是预编译处理,${}是字符串替换。 Mybatis在处理#{}时,会将sql中的#{}替换为?号,调用PreparedStatement的set方法来赋值;Mybatis在处理${}时,就是把${}替换成变量的值。使用#{}可以有效的防止SQL注入,提高系统安全性。 21. 通常一个Xml映射文件,都会写一个Dao接口与之对应,请问,这个Dao接口的工作原理是什么?Dao接口里的方法,参数不同时,方法能重载吗? Dao接口,就是人们常说的Mapper接口,接口的全限名,就是映射文件中的namespace的值,接口的方法名,就是映射文件中MappedStatement的id值,接口方法内的参数,就是传递给sql的参数。Mapper接口是没有实现类的,当调用接口方法时,接口全限名+方法名拼接字符串作为key值,可唯一定位一个MappedStatement,举例:com.mybatis3.mappers.StudentDao.findStudentById,可以唯一找到namespace为com.mybatis3.mappers.StudentDao下面id = findStudentById的MappedStatement。在Mybatis中,每一个、、、标签,都会被解析为一个MappedStatement对象。 Dao接口里的方法,是不能重载的,因为是全限名+方法名的保存和寻找策略。 Dao接口的工作原理是JDK动态代理,Mybatis运行时会使用JDK动态代理为Dao接口生成代理proxy对象,代理对象proxy会拦截接口方法,转而执行MappedStatement所代表的sql,然后将sql执行结果返回。 欢迎关注微信公众号:慕容千语的架构笔记 一起学习提升

优秀的个人博客,低调大师

几道高级前端面试题解析

为什么 0.1 + 0.2 != 0.3,请详述理由 因为 JS 采用 IEEE 754 双精度版本(64位),并且只要采用 IEEE 754 的语言都有该问题。 我们都知道计算机表示十进制是采用二进制表示的,所以 0.1 在二进制表示为 // (0011) 表示循环 0.1 = 2^-4 * 1.10011(0011) 复制代码 那么如何得到这个二进制的呢,我们可以来演算下 小数算二进制和整数不同。乘法计算时,只计算小数位,整数位用作每一位的二进制,并且得到的第一位为最高位。所以我们得出 0.1 = 2^-4 * 1.10011(0011),那么 0.2 的演算也基本如上所示,只需要去掉第一步乘法,所以得出 0.2 = 2^-3 * 1.10011(0011)。 回来继续说 IEEE 754 双精度。六十四位中符号位占一位,整数位占十一位,其余五十二位都为小数位。因为 0.1 和 0.2 都是无限循环的二进制了,所以在小数位末尾处需要判断是否进位(就和十进制的四舍五入一样)。 所以 2^-4 * 1.10011...001 进位后就变成了 2^-4 * 1.10011(0011 * 12次)010 。那么把这两个二进制加起来会得出 2^-2 * 1.0011(0011 * 11次)0100 , 这个值算成十进制就是 0.30000000000000004 下面说一下原生解决办法,如下代码所示 parseFloat((0.1 + 0.2).toFixed(10)) 复制代码 10 个 Ajax 同时发起请求,全部返回展示结果,并且至多允许三次失败,说出设计思路 这个问题相信很多人会第一时间想到 Promise.all ,但是这个函数有一个局限在于如果失败一次就返回了,直接这样实现会有点问题,需要变通下。以下是两种实现思路 // 以下是不完整代码,着重于思路 非 Promise 写法 let successCount = 0 let errorCount = 0 let datas = [] ajax(url, (res) => { if (success) { success++ if (success + errorCount === 10) { console.log(datas) } else { datas.push(res.data) } } else { errorCount++ if (errorCount > 3) { // 失败次数大于3次就应该报错了 throw Error('失败三次') } } }) // Promise 写法 let errorCount = 0 let p = new Promise((resolve, reject) => { if (success) { resolve(res.data) } else { errorCount++ if (errorCount > 3) { // 失败次数大于3次就应该报错了 reject(error) } else { resolve(error) } } }) Promise.all([p]).then(v => { console.log(v); }); 复制代码 基于 Localstorage 设计一个 1M 的缓存系统,需要实现缓存淘汰机制 设计思路如下: 存储的每个对象需要添加两个属性:分别是过期时间和存储时间。 利用一个属性保存系统中目前所占空间大小,每次存储都增加该属性。当该属性值大于 1M 时,需要按照时间排序系统中的数据,删除一定量的数据保证能够存储下目前需要存储的数据。 每次取数据时,需要判断该缓存数据是否过期,如果过期就删除。 以下是代码实现,实现了思路,但是可能会存在 Bug,但是这种设计题一般是给出设计思路和部分代码,不会需要写出一个无问题的代码 class Store { constructor() { let store = localStorage.getItem('cache') if (!store) { store = { maxSize: 1024 * 1024, size: 0 } this.store = store } else { this.store = JSON.parse(store) } } set(key, value, expire) { this.store[key] = { date: Date.now(), expire, value } let size = this.sizeOf(JSON.stringify(this.store[key])) if (this.store.maxSize < size + this.store.size) { console.log('超了-----------'); var keys = Object.keys(this.store); // 时间排序 keys = keys.sort((a, b) => { let item1 = this.store[a], item2 = this.store[b]; return item2.date - item1.date; }); while (size + this.store.size > this.store.maxSize) { let index = keys[keys.length - 1] this.store.size -= this.sizeOf(JSON.stringify(this.store[index])) delete this.store[index] } } this.store.size += size localStorage.setItem('cache', JSON.stringify(this.store)) } get(key) { let d = this.store[key] if (!d) { console.log('找不到该属性'); return } if (d.expire > Date.now) { console.log('过期删除'); delete this.store[key] localStorage.setItem('cache', JSON.stringify(this.store)) } else { return d.value } } sizeOf(str, charset) { var total = 0, charCode, i, len; charset = charset ? charset.toLowerCase() : ''; if (charset === 'utf-16' || charset === 'utf16') { for (i = 0, len = str.length; i < len; i++) { charCode = str.charCodeAt(i); if (charCode <= 0xffff) { total += 2; } else { total += 4; } } } else { for (i = 0, len = str.length; i < len; i++) { charCode = str.charCodeAt(i); if (charCode <= 0x007f) { total += 1; } else if (charCode <= 0x07ff) { total += 2; } else if (charCode <= 0xffff) { total += 3; } else { total += 4; } } } return total; } } 复制代码 详细说明 Event loop 众所周知 JS 是门非阻塞单线程语言,因为在最初 JS 就是为了和浏览器交互而诞生的。如果 JS 是门多线程的语言话,我们在多个线程中处理 DOM 就可能会发生问题(一个线程中新加节点,另一个线程中删除节点),当然可以引入读写锁解决这个问题。 JS 在执行的过程中会产生执行环境,这些执行环境会被顺序的加入到执行栈中。如果遇到异步的代码,会被挂起并加入到 Task(有多种 task) 队列中。一旦执行栈为空,Event Loop 就会从 Task 队列中拿出需要执行的代码并放入执行栈中执行,所以本质上来说 JS 中的异步还是同步行为。 console.log('script start'); setTimeout(function() { console.log('setTimeout'); }, 0); console.log('script end'); 复制代码 以上代码虽然 setTimeout 延时为 0,其实还是异步。这是因为 HTML5 标准规定这个函数第二个参数不得小于 4 毫秒,不足会自动增加。所以 setTimeout 还是会在 script end 之后打印。 不同的任务源会被分配到不同的 Task 队列中,任务源可以分为 微任务(microtask) 和 宏任务(macrotask)。在 ES6 规范中,microtask 称为 jobs,macrotask 称为 task。 console.log('script start'); setTimeout(function() { console.log('setTimeout'); }, 0); new Promise((resolve) => { console.log('Promise') resolve() }).then(function() { console.log('promise1'); }).then(function() { console.log('promise2'); }); console.log('script end'); // script start => Promise => script end => promise1 => promise2 => setTimeout 复制代码 以上代码虽然 setTimeout 写在 Promise 之前,但是因为 Promise 属于微任务而 setTimeout 属于宏任务,所以会有以上的打印。 微任务包括 process.nextTick ,promise ,Object.observe ,MutationObserver 宏任务包括 script , setTimeout ,setInterval ,setImmediate ,I/O ,UI rendering 很多人有个误区,认为微任务快于宏任务,其实是错误的。因为宏任务中包括了 script ,浏览器会先执行一个宏任务,接下来有异步代码的话就先执行微任务。 所以正确的一次 Event loop 顺序是这样的 执行同步代码,这属于宏任务 执行栈为空,查询是否有微任务需要执行 执行所有微任务 必要的话渲染 UI 然后开始下一轮 Event loop,执行宏任务中的异步代码 通过上述的 Event loop 顺序可知,如果宏任务中的异步代码有大量的计算并且需要操作 DOM 的话,为了更快的 界面响应,我们可以把操作 DOM 放入微任务中。 Node 中的 Event loop Node 中的 Event loop 和浏览器中的不相同。 Node 的 Event loop 分为6个阶段,它们会按照顺序反复运行 ┌───────────────────────┐ ┌─>│ timers │ │ └──────────┬────────────┘ │ ┌──────────┴────────────┐ │ │ I/O callbacks │ │ └──────────┬────────────┘ │ ┌──────────┴────────────┐ │ │ idle, prepare │ │ └──────────┬────────────┘ ┌───────────────┐ │ ┌──────────┴────────────┐ │ incoming: │ │ │ poll │<──connections─── │ │ └──────────┬────────────┘ │ data, etc. │ │ ┌──────────┴────────────┐ └───────────────┘ │ │ check │ │ └──────────┬────────────┘ │ ┌──────────┴────────────┐ └──┤ close callbacks │ └───────────────────────┘ 复制代码 timer timers 阶段会执行 setTimeout 和 setInterval 一个 timer 指定的时间并不是准确时间,而是在达到这个时间后尽快执行回调,可能会因为系统正在执行别的事务而延迟。 下限的时间有一个范围:[1, 2147483647] ,如果设定的时间不在这个范围,将被设置为1。 I/O I/O 阶段会执行除了 close 事件,定时器和 setImmediate 的回调 idle, prepare idle, prepare 阶段内部实现 poll poll 阶段很重要,这一阶段中,系统会做两件事情 执行到点的定时器 执行 poll 队列中的事件 并且当 poll 中没有定时器的情况下,会发现以下两件事情 如果 poll 队列不为空,会遍历回调队列并同步执行,直到队列为空或者系统限制 如果 poll 队列为空,会有两件事发生 如果有 setImmediate 需要执行,poll 阶段会停止并且进入到 check 阶段执行 setImmediate 如果没有 setImmediate 需要执行,会等待回调被加入到队列中并立即执行回调 如果有别的定时器需要被执行,会回到 timer 阶段执行回调。 check check 阶段执行 setImmediate close callbacks close callbacks 阶段执行 close 事件 并且在 Node 中,有些情况下的定时器执行顺序是随机的 setTimeout(() => { console.log('setTimeout'); }, 0); setImmediate(() => { console.log('setImmediate'); }) // 这里可能会输出 setTimeout,setImmediate // 可能也会相反的输出,这取决于性能 // 因为可能进入 event loop 用了不到 1 毫秒,这时候会执行 setImmediate // 否则会执行 setTimeout 复制代码 当然在这种情况下,执行顺序是相同的 var fs = require('fs') fs.readFile(__filename, () => { setTimeout(() => { console.log('timeout'); }, 0); setImmediate(() => { console.log('immediate'); }); }); // 因为 readFile 的回调在 poll 中执行 // 发现有 setImmediate ,所以会立即跳到 check 阶段执行回调 // 再去 timer 阶段执行 setTimeout // 所以以上输出一定是 setImmediate,setTimeout 复制代码 上面介绍的都是 macrotask 的执行情况,microtask 会在以上每个阶段完成后立即执行。 setTimeout(()=>{ console.log('timer1') Promise.resolve().then(function() { console.log('promise1') }) }, 0) setTimeout(()=>{ console.log('timer2') Promise.resolve().then(function() { console.log('promise2') }) }, 0) // 以上代码在浏览器和 node 中打印情况是不同的 // 浏览器中打印 timer1, promise1, timer2, promise2 // node 中打印 timer1, timer2, promise1, promise2 复制代码 Node 中的 process.nextTick 会先于其他 microtask 执行。 setTimeout(() => { console.log("timer1"); Promise.resolve().then(function() { console.log("promise1"); }); }, 0); process.nextTick(() => { console.log("nextTick"); }); // nextTick, timer1, promise1 1.如果觉得这篇文章还不错,来个分享、点赞吧,让更多的人也看到 如果你觉得这篇文章对你有点用的话,麻烦请给我们的开源项目点点star: http://github.crmeb.net/u/defu 不胜感激 ! 来自 “开源世界 ” ,链接: https://ym.baisou.ltd/post/754.html ,如需转载,请注明出处,否则将追究法律责任。

优秀的个人博客,低调大师

11道浏览器原理面试

浏览器与新技术 这是一篇很长的文章,可以算上一本小书了,有精力的非常建议阅读。 常见的浏览器内核有哪些? 浏览器/RunTime 内核(渲染引擎) JavaScript 引擎 Chrome Blink(28~) Webkit(Chrome 27) V8 FireFox Gecko SpiderMonkey Safari Webkit JavaScriptCore Edge EdgeHTML Chakra(for JavaScript) IE Trident Chakra(for JScript) PhantomJS Webkit JavaScriptCore Node.js - V8 浏览器的主要组成部分是什么? 用户界面- 包括地址栏、前进/后退按钮、书签菜单等。除了浏览器主窗口显示的您请求的页面外,其他显示的各个部分都属于用户界面。 浏览器引擎- 在用户界面和呈现引擎之间传送指令。 呈现引擎- 负责显示请求的内容。如果请求的内容是 HTML,它就负责解析 HTML 和 CSS 内容,并将解析后的内容显示在屏幕上。 网络- 用于网络调用,比如 HTTP 请求。其接口与平台无关,并为所有平台提供底层实现。 用户界面后端- 用于绘制基本的窗口小部件,比如组合框和窗口。其公开了与平台无关的通用接口,而在底层使用操作系统的用户界面方法。 JavaScript 解释器。用于解析和执行 JavaScript 代码。 数据存储。这是持久层。浏览器需要在硬盘上保存各种数据,例如 Cookie。新的 HTML 规范 (HTML5) 定义了“网络数据库”,这是一个完整(但是轻便)的浏览器内数据库。 图:浏览器的主要组件。 值得注意的是,和大多数浏览器不同,Chrome 浏览器的每个标签页都分别对应一个呈现引擎实例。每个标签页都是一个独立的进程。 浏览器是如何渲染UI的? 浏览器获取HTML文件,然后对文件进行解析,形成DOM Tree 与此同时,进行CSS解析,生成Style Rules 接着将DOM Tree与Style Rules合成为 Render Tree 接着进入布局(Layout)阶段,也就是为每个节点分配一个应出现在屏幕上的确切坐标 随后调用GPU进行绘制(Paint),遍历Render Tree的节点,并将元素呈现出来 浏览器如何解析css选择器? 浏览器会『从右往左』解析CSS选择器。 我们知道DOM Tree与Style Rules合成为 Render Tree,实际上是需要将Style Rules附着到DOM Tree上,因此需要根据选择器提供的信息对DOM Tree进行遍历,才能将样式附着到对应的DOM元素上。 以下这段css为例 .mod-nav h3 span {font-size: 16px;} 复制代码 我们对应的DOM Tree 如下 若从左向右的匹配,过程是: 从 .mod-nav 开始,遍历子节点 header 和子节点 div 然后各自向子节点遍历。在右侧 div 的分支中 最后遍历到叶子节点 a ,发现不符合规则,需要回溯到 ul 节点,再遍历下一个 li-a,一颗DOM树的节点动不动上千,这种效率很低。 如果从右至左的匹配: 先找到所有的最右节点 span,对于每一个 span,向上寻找节点 h3 由 h3再向上寻找 class=mod-nav 的节点 最后找到根元素 html 则结束这个分支的遍历。 后者匹配性能更好,是因为从右向左的匹配在第一步就筛选掉了大量的不符合条件的最右节点(叶子节点);而从左向右的匹配规则的性能都浪费在了失败的查找上面。 DOM Tree是如何构建的? 转码: 浏览器将接收到的二进制数据按照指定编码格式转化为HTML字符串 生成Tokens: 之后开始parser,浏览器会将HTML字符串解析成Tokens 构建Nodes: 对Node添加特定的属性,通过指针确定 Node 的父、子、兄弟关系和所属 treeScope 生成DOM Tree: 通过node包含的指针确定的关系构建出DOM Tree 浏览器重绘与重排的区别? 重排: 部分渲染树(或者整个渲染树)需要重新分析并且节点尺寸需要重新计算,表现为重新生成布局,重新排列元素 重绘: 由于节点的几何属性发生改变或者由于样式发生改变,例如改变元素背景色时,屏幕上的部分内容需要更新,表现为某些元素的外观被改变 单单改变元素的外观,肯定不会引起网页重新生成布局,但当浏览器完成重排之后,将会重新绘制受到此次重排影响的部分 重排和重绘代价是高昂的,它们会破坏用户体验,并且让UI展示非常迟缓,而相比之下重排的性能影响更大,在两者无法避免的情况下,一般我们宁可选择代价更小的重绘。 『重绘』不一定会出现『重排』,『重排』必然会出现『重绘』。 如何触发重排和重绘? 任何改变用来构建渲染树的信息都会导致一次重排或重绘: 添加、删除、更新DOM节点 通过display: none隐藏一个DOM节点-触发重排和重绘 通过visibility: hidden隐藏一个DOM节点-只触发重绘,因为没有几何变化 移动或者给页面中的DOM节点添加动画 添加一个样式表,调整样式属性 用户行为,例如调整窗口大小,改变字号,或者滚动。 如何避免重绘或者重排? 集中改变样式 我们往往通过改变class的方式来集中改变样式 // 判断是否是黑色系样式 const theme = isDark ? 'dark' : 'light' // 根据判断来设置不同的class ele.setAttribute('className', theme) 复制代码 使用DocumentFragment 我们可以通过createDocumentFragment创建一个游离于DOM树之外的节点,然后在此节点上批量操作,最后插入DOM树中,因此只触发一次重排 var fragment = document.createDocumentFragment(); for (let i = 0;i<10;i++){ let node = document.createElement("p"); node.innerHTML = i; fragment.appendChild(node); } document.body.appendChild(fragment); 复制代码 提升为合成层 将元素提升为合成层有以下优点: 合成层的位图,会交由 GPU 合成,比 CPU 处理要快 当需要 repaint 时,只需要 repaint 本身,不会影响到其他的层 对于 transform 和 opacity 效果,不会触发 layout 和 paint 提升合成层的最好方式是使用 CSS 的 will-change 属性: #target { will-change: transform; } 复制代码 前端如何实现即时通讯? 短轮询 短轮询的原理很简单,每隔一段时间客户端就发出一个请求,去获取服务器最新的数据,一定程度上模拟实现了即时通讯。 优点:兼容性强,实现非常简单 缺点:延迟性高,非常消耗请求资源,影响性能 comet comet有两种主要实现手段,一种是基于 AJAX 的长轮询(long-polling)方式,另一种是基于 Iframe 及 htmlfile 的流(streaming)方式,通常被叫做长连接。 长轮询优缺点: 优点:兼容性好,资源浪费较小 缺点:服务器hold连接会消耗资源,返回数据顺序无保证,难于管理维护 长连接优缺点: 优点:兼容性好,消息即时到达,不发无用请求 缺点:服务器维护长连接消耗资源 SSE SSE(Server-Sent Event,服务端推送事件)是一种允许服务端向客户端推送新数据的HTML5技术。 优点:基于HTTP而生,因此不需要太多改造就能使用,使用方便,而websocket非常复杂,必须借助成熟的库或框架 缺点:基于文本传输效率没有websocket高,不是严格的双向通信,客户端向服务端发送请求无法复用之前的连接,需要重新发出独立的请求 Websocket Websocket是一个全新的、独立的协议,基于TCP协议,与http协议兼容、却不会融入http协议,仅仅作为html5的一部分,其作用就是在服务器和客户端之间建立实时的双向通信。 优点:真正意义上的实时双向通信,性能好,低延迟 缺点:独立与http的协议,因此需要额外的项目改造,使用复杂度高,必须引入成熟的库,无法兼容低版本浏览器 Web Worker 后面性能优化部分会用到,先做了解 Web Worker 的作用,就是为 JavaScript 创造多线程环境,允许主线程创建 Worker 线程,将一些任务分配给后者运行 Service workers 后面性能优化部分会用到,先做了解 Service workers 本质上充当Web应用程序与浏览器之间的代理服务器,也可以在网络可用时作为浏览器和网络间的代理,创建有效的离线体验。 什么是浏览器同源策略? 同源策略限制了从同一个源加载的文档或脚本如何与来自另一个源的资源进行交互。这是一个用于隔离潜在恶意文件的重要安全机制。 同源是指"协议+域名+端口"三者相同,即便两个不同的域名指向同一个ip地址,也非同源。 下表给出了相对http://store.company.com/dir/page.html同源检测的示例: 浏览器中的大部分内容都是受同源策略限制的,但是以下三个标签可以不受限制: <img src=XXX> <link href=XXX> <script src=XXX> 如何实现跨域? 跨域是个比较古老的命题了,历史上跨域的实现手段有很多,我们现在主要介绍三种比较主流的跨域方案,其余的方案我们就不深入讨论了,因为使用场景很少,也没必要记这么多奇技淫巧。 最经典的跨域方案jsonp jsonp本质上是一个Hack,它利用<script>标签不受同源策略限制的特性进行跨域操作。 jsonp优点: 实现简单 兼容性非常好 jsonp的缺点: 只支持get请求(因为<script>标签只能get) 有安全性问题,容易遭受xss攻击 需要服务端配合jsonp进行一定程度的改造 jsonp的实现: function JSONP({ url, params, callbackKey, callback }) { // 在参数里制定 callback 的名字 params = params || {} params[callbackKey] = 'jsonpCallback' // 预留 callback window.jsonpCallback = callback // 拼接参数字符串 const paramKeys = Object.keys(params) const paramString = paramKeys .map(key => `${key}=${params[key]}`) .join('&') // 插入 DOM 元素 const script = document.createElement('script') script.setAttribute('src', `${url}?${paramString}`) document.body.appendChild(script) } JSONP({ url: 'http://s.weibo.com/ajax/jsonp/suggestion', params: { key: 'test', }, callbackKey: '_cb', callback(result) { console.log(result.data) } }) 复制代码 最流行的跨域方案cors cors是目前主流的跨域解决方案,跨域资源共享(CORS) 是一种机制,它使用额外的 HTTP 头来告诉浏览器 让运行在一个 origin (domain) 上的Web应用被准许访问来自不同源服务器上的指定的资源。当一个资源从与该资源本身所在的服务器不同的域、协议或端口请求一个资源时,资源会发起一个跨域 HTTP 请求。 如果你用express,可以这样在后端设置 //CORS middleware var allowCrossDomain = function(req, res, next) { res.header('Access-Control-Allow-Origin', 'http://example.com'); res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE'); res.header('Access-Control-Allow-Headers', 'Content-Type'); next(); } //... app.configure(function() { app.use(express.bodyParser()); app.use(express.cookieParser()); app.use(express.session({ secret: 'cool beans' })); app.use(express.methodOverride()); app.use(allowCrossDomain); app.use(app.router); app.use(express.static(__dirname + '/public')); }); 复制代码 在生产环境中建议用成熟的开源中间件解决问题。 最方便的跨域方案Nginx nginx是一款极其强大的web服务器,其优点就是轻量级、启动快、高并发。 现在的新项目中nginx几乎是首选,我们用node或者java开发的服务通常都需要经过nginx的反向代理。 反向代理的原理很简单,即所有客户端的请求都必须先经过nginx的处理,nginx作为代理服务器再讲请求转发给node或者java服务,这样就规避了同源策略。 #进程, 可更具cpu数量调整 worker_processes 1; events { #连接数 worker_connections 1024; } http { include mime.types; default_type application/octet-stream; sendfile on; #连接超时时间,服务器会在这个时间过后关闭连接。 keepalive_timeout 10; # gizp压缩 gzip on; # 直接请求nginx也是会报跨域错误的这里设置允许跨域 # 如果代理地址已经允许跨域则不需要这些, 否则报错(虽然这样nginx跨域就没意义了) add_header Access-Control-Allow-Origin *; add_header Access-Control-Allow-Headers X-Requested-With; add_header Access-Control-Allow-Methods GET,POST,OPTIONS; # srever模块配置是http模块中的一个子模块,用来定义一个虚拟访问主机 server { listen 80; server_name localhost; # 根路径指到index.html location / { root html; index index.html index.htm; } # localhost/api 的请求会被转发到192.168.0.103:8080 location /api { rewrite ^/b/(.*)$ /$1 break; # 去除本地接口/api前缀, 否则会出现404 proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_pass http://192.168.0.103:8080; # 转发地址 } # 重定向错误页面到/50x.html error_page 500 502 503 504 /50x.html; location = /50x.html { root html; } } } 复制代码 其它跨域方案 HTML5 XMLHttpRequest 有一个API,postMessage()方法允许来自不同源的脚本采用异步方式进行有限的通信,可以实现跨文本档、多窗口、跨域消息传递。 WebSocket 是一种双向通信协议,在建立连接之后,WebSocket 的 server 与 client 都能主动向对方发送或接收数据,连接建立好了之后 client 与 server 之间的双向通信就与 HTTP 无关了,因此可以跨域。 window.name + iframe:window.name属性值在不同的页面(甚至不同域名)加载后依旧存在,并且可以支持非常长的 name 值,我们可以利用这个特点进行跨域。 location.hash + iframe:a.html欲与c.html跨域相互通信,通过中间页b.html来实现。 三个页面,不同域之间利用iframe的location.hash传值,相同域之间直接js访问来通信。 document.domain + iframe: 该方式只能用于二级域名相同的情况下,比如 a.test.com 和 b.test.com 适用于该方式,我们只需要给页面添加 document.domain ='test.com' 表示二级域名都相同就可以实现跨域,两个页面都通过js强制设置document.domain为基础主域,就实现了同域。 1.如果觉得这篇文章还不错,来个分享、点赞吧,让更多的人也看到 如果你觉得这篇文章对你有点用的话,麻烦请给我们的开源项目点点star: http://github.crmeb.net/u/defu 不胜感激 ! 来自 “开源世界 ” ,链接: https://ym.baisou.ltd/post/753.html ,如需转载,请注明出处,否则将追究法律责任。

优秀的个人博客,低调大师

面试系列六 之 用户行为数据分析

# 关注我的公众号【宝哥大数据】,更多干货等着你 ![在这里插入图片描述](https://img-blog.csdnimg.cn/2021062208300594.png) ## 1.1、数仓分层架构 ![在这里插入图片描述](https://img-blog.csdnimg.cn/2021062105583461.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3d1eGludGRyaA==,size_16,color_FFFFFF,t_70) **分层优点:复杂问题简单化、清晰数据结构(方便管理)、增加数据的复用性、隔离原始数据(解耦)** 层级 | 功能 ---|--- ods | 原始数据层 存放原始数据,保持原貌不做处理 dwd | 明细数据层 对ods层数据清洗(去除空值,脏数据,超过极限范围的数据) dws | 服务数据层 轻度聚合 ads | 应用数据层 具体需求 数仓中各层建的表都是外部表 ## 1.2、埋点行为数据基本格式(基本字段) 公共字段:基本所有安卓手机都包含的字段 业务字段:埋点上报的字段,有具体的业务类型 下面就是一个示例,表示业务字段的上传。 行为数据启动日志/事件日志表关键字段: ```json { "ap":"xxxxx",//项目数据来源 app pc "cm": { //公共字段 "mid": "", // (String) 设备唯一标识 "uid": "", // (String) 用户标识 "vc": "1", // (String) versionCode,程序版本号 "vn": "1.0", // (String) versionName,程序版本名 "l": "zh", // (String) 系统语言 "sr": "", // (String) 渠道号,应用从哪个渠道来的。 "os": "7.1.1", // (String) Android系统版本 "ar": "CN", // (String) 区域 "md": "BBB100-1", // (String) 手机型号 "ba": "blackberry", // (String) 手机品牌 "sv": "V2.2.1", // (String) sdkVersion "g": "", // (String) gmail "hw": "1620x1080", // (String) heightXwidth,屏幕宽高 "t": "1506047606608", // (String) 客户端日志产生时的时间 "nw": "WIFI", // (String) 网络模式 "ln": 0, // (double) lng经度 "la": 0 // (double) lat 纬度 }, "et": [ //事件 { "ett": "1506047605364", //客户端事件产生时间 "en": "display", //事件名称 启动和事件日志是根据事件名称的不同 "kv": { //事件结果,以key-value形式自行定义 "goodsid": "236", "action": "1", "extend1": "1", "place": "2", "category": "75" } } ] } ``` 根据事件标签的不同可以分成不同的日志表 ## 1.3、各个层的表介绍 ### 1.3.1、ods层 1)ods_start_log 启动日志表 - 只有一个字段 line(保存着json),按照日期dt分区,表的格式:lzo 2)ods_event_log 事件日志表(格式同启动日志表) - 只有一个字段 line ,按照日期dt 分区,表的格式:lzo ### 1.3.2、dwd层 1)dwd_start_log 启动表 - 关键字段:mid_id,user_id,dt(分区字段,按照日期分区) (其实这是启动表和事件表的公共字段) - 从ods_start_log中的line用`get_json_object(line,'$.mid') mid_id`的方式获取字段 #### 1.3.2.1、自定义UDF/UDTF(项目中的应用) - 自定义UDF函数(解析公共字段,一进一出) - 自定义UDTF函数(解析具体事件字段,一进多出) - 自定义UDF:继承UDF,重写evaluate方法 - 自定义UDTF:继承自GenericUDTF,重写3个方法:initialize(自定义输出的列名和类型),process(将结果返回forward(result)),close - 为什么要自定义UDF/UDTF,因为自定义函数,可以自己埋点Log打印日志,出错或者数据异常,方便调试。 #### 1.3.2.2、事件日志基础明细表 dwd_base_event_log 事件日志基础明细表 - 1)关键字段: - 公共字段:mid_id,user_id,dt(分区字段)以及event_name、event_json、server_time - 2)从 ods_event_log的line 中用 UDF 获取 公共字段 和 server_time,用UDTF 获取 event_name , event_json。 #### 1.3.2.3、商品点击表 dwd_display_log 商品点击表 - 关键字段:公共字段 + 特有字段 - 从dwd_base_event_log中直接获取公共字段和server_time,从 dwd_base_event_log的 event_json中获取特有字段,`where event_name = "display"` - `get_json_object(event_json,'$.kv.action') action` #### 1.3.2.4、其他的具体事件明细表 类似 表明|表注释 --|-- dwd_newsdetail_log | 商品详情页表 dwd_loading_log |商品列表页表 dwd_ad_log| 广告表 dwd_notification_log |消息通知表 dwd_active_foreground_log |用户前台活跃表 dwd_active_background_log |用户后台活跃表 dwd_comment_log |评论表 dwd_favorites_log| 收藏表 dwd_praise_log |点赞表 dwd_error_log |错误日志表 从一张事件基础明细表dwd_base_event_log一共可以获得11张具体事件明细表 # 二、需求解析 ## 2.1、用户活跃主题 ### 2.1.1、DWS层日活明细表 每日活跃设备分析 ![每日活跃设备分析](https://img-blog.csdnimg.cn/20210621062330155.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3d1eGludGRyaA==,size_16,color_FFFFFF,t_70) ### 2.1.2、DWS层周活明细表 每周活跃设备分析 ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210621062730526.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3d1eGludGRyaA==,size_16,color_FFFFFF,t_70) ### 2.1.3、DWS层月活明细表 每月活跃设备分析 ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210621062811618.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3d1eGludGRyaA==,size_16,color_FFFFFF,t_70) ### 2.1.4、ADS层日周月活跃设备数表 活跃设备分析 ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210621062950502.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3d1eGludGRyaA==,size_16,color_FFFFFF,t_70) ## 2.2、用户新增主题 ### 2.2.1、DWS层日新增明细表 ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210621063135426.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3d1eGludGRyaA==,size_16,color_FFFFFF,t_70) ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210621063334946.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3d1eGludGRyaA==,size_16,color_FFFFFF,t_70) ### 2.2.2、ADS层每日新增设备数表 ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210621063456915.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3d1eGludGRyaA==,size_16,color_FFFFFF,t_70) ## 2.3、用户留存主题 ### 2.3.1、用户留存介绍 ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210621063556386.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3d1eGludGRyaA==,size_16,color_FFFFFF,t_70) ### 2.3.2、用户留存率分析 ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210621063733813.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3d1eGludGRyaA==,size_16,color_FFFFFF,t_70) ### 2.3.3、DWS层日留存明细表 ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210621065046609.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3d1eGludGRyaA==,size_16,color_FFFFFF,t_70) ### 2.3.4、ADS层留存用户数表 ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210621064917726.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3d1eGludGRyaA==,size_16,color_FFFFFF,t_70) ### 2.3.5、ADS层留存用户率表 ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210621064854479.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3d1eGludGRyaA==,size_16,color_FFFFFF,t_70) ## 2.4、沉默用户 ![ 指的是](https://img-blog.csdnimg.cn/20210622054004280.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3d1eGludGRyaA==,size_16,color_FFFFFF,t_70) ## 2.5、本周回流用户数 ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210622054518176.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3d1eGludGRyaA==,size_16,color_FFFFFF,t_70) ## 2.6、流失用户数 ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210622055430202.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3d1eGludGRyaA==,size_16,color_FFFFFF,t_70) ## 2.7、最近连续3周活跃用户数 ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210622055638418.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3d1eGludGRyaA==,size_16,color_FFFFFF,t_70) ## 2.8、最近七天内连续三天活跃用户数 ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210622055801435.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3d1eGludGRyaA==,size_16,color_FFFFFF,t_70) ## 2.9、需求逻辑 ### 2.9.1 如何分析用户活跃? 在启动日志中统计不同设备id出现次数。 ### 2.9.2 如何分析用户新增? 用活跃用户表 left join 用户新增表,用户新增表中mid为空的即为用户新增。 ### 2.9.3 如何分析用户1天留存? 留存用户=前一天新增 join 今天活跃 用户留存率=留存用户/前一天新增 ### 2.9.4 如何分析沉默用户? (登录时间为7天前,且只出现过一次) 按照设备id对日活表分组,登录次数为1,且是在一周前登录。 ### 2.9.5 如何分析本周回流用户? 本周活跃left join本周新增 left join上周活跃,且本周新增id和上周活跃id都为null ### 2.9.6 如何分析流失用户? (登录时间为7天前) 按照设备id对日活表分组,且七天内没有登录过。 ### 2.9.7 如何分析最近连续3周活跃用户数? 按照设备id对周活进行分组,统计次数大于3次。 ### 2.9.8 如何分析最近七天内连续三天活跃用户数? - 1)查询出最近7天的活跃用户,并对用户活跃日期进行排名 - 2)计算用户活跃日期及排名之间的差值 - 3)对同用户及差值分组,统计差值个数 - 4)将差值相同个数大于等于3的数据取出,然后去重(去的是什么重???),即为连续3天及以上活跃的用户

资源下载

更多资源
Mario

Mario

马里奥是站在游戏界顶峰的超人气多面角色。马里奥靠吃蘑菇成长,特征是大鼻子、头戴帽子、身穿背带裤,还留着胡子。与他的双胞胎兄弟路易基一起,长年担任任天堂的招牌角色。

Nacos

Nacos

Nacos /nɑ:kəʊs/ 是 Dynamic Naming and Configuration Service 的首字母简称,一个易于构建 AI Agent 应用的动态服务发现、配置管理和AI智能体管理平台。Nacos 致力于帮助您发现、配置和管理微服务及AI智能体应用。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据、流量管理。Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。

Spring

Spring

Spring框架(Spring Framework)是由Rod Johnson于2002年提出的开源Java企业级应用框架,旨在通过使用JavaBean替代传统EJB实现方式降低企业级编程开发的复杂性。该框架基于简单性、可测试性和松耦合性设计理念,提供核心容器、应用上下文、数据访问集成等模块,支持整合Hibernate、Struts等第三方框架,其适用范围不仅限于服务器端开发,绝大多数Java应用均可从中受益。

Rocky Linux

Rocky Linux

Rocky Linux(中文名:洛基)是由Gregory Kurtzer于2020年12月发起的企业级Linux发行版,作为CentOS稳定版停止维护后与RHEL(Red Hat Enterprise Linux)完全兼容的开源替代方案,由社区拥有并管理,支持x86_64、aarch64等架构。其通过重新编译RHEL源代码提供长期稳定性,采用模块化包装和SELinux安全架构,默认包含GNOME桌面环境及XFS文件系统,支持十年生命周期更新。

用户登录
用户注册