1、Java基础
谈谈你对Java平台的理解?
典型回答:
首先Java是一种面向对象的语言,最显著的特性就是“一次编译,到处运行”,具有跨平台的能力。另外就是具有垃圾回收集,大部分情况下我们程序员不需要去关心内存的分配和回收。
谈谈你对接口和抽象类的区别?
接口特点:
- 接口是对行为的一种抽象,是抽象方法的集合,定义的方法是没有方法具体实现,但可以定义default默认方法。
- 接口中所有方法访问权限都是自动声明为public,及接口中定义的成员变量也会自动变为
public static find
修饰的静态常量。 - 接口不能实例化
抽象特点:
- 抽象类是使用abstract关键字修饰的class,这样的目的主要是代码的重用,抽象类不能实例化的类的。
面向对象的设计:
- 封装:目的就是隐藏事务内部的实现细节,提高安全性和简化编程
- 继承:是指子类继承父类,这样可以提高代码的重复使用的机制
- 多态:一个对象可能有多种形式的体现,重写(override)和 重载(Overload)
浅拷贝和深拷贝的区别?
浅拷贝:当复制引用属性时,仅仅复制的内存地址,没有复制指向的对象。如果原地址发生改变,那么浅复制出来的对象也会相应的改变。
深拷贝:深拷贝就要完整复制属性指向的对象,在计算机中开辟一块新的内存地址用于存放复制的对象。
什么是反射机制?
反射机制就是程序在运行时能够获得自身的信息,只要给定一个类的名字,就可以通过反射获得类的所有信息。
反射解决什么问题:提高性能、提高程序的灵活性和扩展性,降低耦合度。
通过一个全限类名创建一个对象:Class.forName(“全限类名”);
2、ArrayList和LinkedList区别?
ArrayList的底层是使用动态数组来存储元素的,而LinkedList内部是使用双向链表来存储元素;对于频繁查询适用ArrayList,时间复杂度O(1)、对于添加如果是从数组末尾添加时间复杂度也是O(1)、如果将元素按照指定位置添加,时间复杂度是O(n),最坏的情况下,数组扩容时需要复制一次的,这些性能降低很多、指定元素删除也是一样O(n);对于LinkedList来说,get的方法是时间复杂度是O(n)的,添加是末尾的时间复杂度是O(1),指定添加时间复杂度是O(n),
3、为什么ConcurrentHashMap是怎么实现高效线程安全?
Java提供不同层面的线程安全支持,传统的集合内部,比如有Hashtable,vector,还有工具类提供的包装方法来获取一个同步的包装容器,但是在高并发情况下,性能还是很低的,所以也是不推荐使用的。而是使用并发包提供的线程安全容器类,比如:ConcurrentHashMap、CopyOnWriteArrayList以及线程安全队列如ArrayBlockingQueue、SynchronousQueue。
1.7之前,底层采用数组+链表的形式存储键值对的,但是为了高并发,然后把原来的整个table划分为n个segment,然后当对某个segment加锁时就不会影响其他segment的读写啦。然而HashEntry内部使用volatile关键字修饰value字段保证可见性。分段锁的设计是当进行并发操作的时候,只需要锁定相应的段即可,这样就可以避免类似Hashtable整体同步的问题,从而大大提高性能。
总结:ConcurrentHashMap1.7是使用Segment+HashEntry来进行保证线程安全的。ConcurrentHashMap的get方法是否要加锁,为什么?不需要,get方法采用了unsafe方法,来保证线程安全。
1.8之后,底层数据结构:数组+链表+红黑树,实现并发安全使用CAS+synchronized及unsafe类底层进行优化,1.8以后的锁的颗粒度,是加在链表头上的,就拿put()方法来说吧,当在并发情况,首先key和value都是不允许为null的,然后如果表没有初始化就进行初始化,如果已经初始化了,那么就找到当前key所在桶,在判断桶如果为空的话,就会通过CAS原子操作把节点插入到当前位置,桶不为空的话,则判断节点的 hash 值是否为 MOVED(值是-1),若为-1,说明当前数组正在进行扩容,则需要当前线程帮忙迁移数据。
如果多线程环境下使用Hashtable,会出现怎样问题?
Hashtable的缺点就是每个方法都是被synchronized,就比如常见的put和get的操作,不管你任务操作都会使用到synchronized关键字加锁,像读取操作,互相之间并不影响,完成可以同时进行的,所以效率低了。总结:Hashtable锁的是整张表,因此效率低,
4、JMM
Java 内存模型是Java Memory Mode的缩写,是一种抽象的规则,是指在多线程并发时候,如何解决多线程之间的通信问题,比如保证各种操作原子性、可见性、有序性。
happens-before 关系是用来描述两个操作的内存可见性的。如果操作 X happens-before 操作 Y,那么 X 的结果对于 Y 可见。
Java 内存模型底层是通过内存屏障(memory barrier)来禁止重排序的。
JMM关于同步的规定:
- 线程解锁之前,必须把共享变量的值刷新回主内存中
- 线程加锁前,必须读取主内存的最新值到自己的工作区内存中
- 加锁解锁是同一把锁
JMM 特性:可见性、原子性、有序性
5、CAS 知道吗?谈谈你的理解?
cas是:比较交换,是CPU并发原语。
假设现在有A和B两个线程同时去操作主内存中共享变量,比如该值为5吧。A、B现在要去操作必须读回自己的工作内存区才能进行操作的。现在A线程调用getIntVolatile拿到5这个值了,这时候挂起,然后B线程getIntVolatile拿到5这个值,成功修改为6,然后刷新回主内存中,这时候主内存的值已经被B线程修改6了。然后这时候A线程恢复了,然后执行weakCompareAndSetInt方法去和主内存进行比较,发现自己的值和主内存不一样,说明该值被别的线程修改了,那么A线程修改失败,只能重写读取主内存的新值到自己工作内存区再次修改。这就是比较交换。CAS的缺点就是ABA问题。ABA问题就是一直自旋,一直得不到。
底层原理:最核心底层是调用unsafe类的是原生类,由于CAS是一种系统原语,最最底层就是CAS是CPU的原子指令,所以不会造成数据的不一致性,从而保证原子性,因此线程安全。
6、垃圾回收算法
- 标记-清除算法:会产生大量不连续的内存碎片
- 标记-复制算法:将可用内存分为相等的两块,每次只用其中一块(新生代)
- 标记-压缩算法:将存活的对象都向一端移动(老年代)
7、垃圾收集器(重点)
4种主要垃圾收集器:serial、parallel、CMS、G1
(1)串行垃圾回收器(Serial):它为单线程环境设计且只使用一个线程进行垃圾回收,会暂停所有用户线程。不使用于服务器环境。
(2)并行垃圾回收器(Parallel):多个垃圾收集线程并行工作,此时用户线程是暂停的,适用于科学计算/大数据处理首台处理等弱交互场景。
(3)并发垃圾回收器(Concurrent Mark Sweep简称CMS):用户线程和垃圾收集线程同时执行,不需要停顿用户线程。互联网公司多用它,适用于对响应时间有要求的场景
(4)G1垃圾回收器(Garbage 1):G1 垃圾回收器将堆内存分割成不同的区域然后并发的对其进行垃圾回收。
如何查看默认垃圾收集器?生产上如何配置垃圾收集器?
查看 Java 虚拟机默认垃圾收集器命令:java -XX:+PrintCommandLineFlags -version
,一下是JDK11
> java -XX:+PrintCommandLineFlags -version-XX:G1ConcRefinementThreads=8 -XX:GCDrainStackTargetSize=64 -XX:InitialHeapSize=268435456 -XX:MaxHeapSize=4294967296 -XX:+PrintCommandLineFlags -XX:ReservedCodeCacheSize=251658240 -XX:+SegmentedCodeCache -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseG1GC openjdk version "11.0.12" 2021-07-20 LTSOpenJDK Runtime Environment Corretto-11.0.12.7.2 (build 11.0.12+7-LTS)OpenJDK 64-Bit Server VM Corretto-11.0.12.7.2 (build 11.0.12+7-LTS, mixed mode)
回顾4大垃圾回收算法:引用计数/复制拷贝/标记清除/标记整理
垃圾收集器算法:Serial/parallel/CMS/G1
Java的GC回收类型主要有以下几种:
// 查看默认垃圾收集器,Java11默认使用UseG1GCjava -XX:+PrintCommandLineFlags -version
UseSerialGC、UseParallelGC、UseConcMarkSweepGC、UseparNewGC、UseparallelOldGC、UseG1GC
垃圾收集器使用情况:
年轻代:Serial Copying、Parallel Scavenge、parNew
老年代:Serial MSC(Serial Old) 、Parallel Compacting(Parallel Old) 、 CMS
控制台打印说明:
- DefNew = Default New Generation
- Tenured = Old
- parNew = parallel New Generation
- PSYoungGen = Parallel Scavenge
- ParaOldGen = Parallel Genneration
新生代细讲
年轻代:Serial Copying、Parallel Scavenge、parNew
(1)串行GC(Serial)/(Serial Copying)
一个单线程的收集器,在进行垃圾收集时候,必须暂停其他线程(STW)所有工作线程直到垃圾收集结束。
配置参数:
-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseSerialGC
(2)并行GC(ParNew)
使用多线程进行垃圾回收,在垃圾收集时,会Stop-the-World暂停其他所有线程的工作线程直到收集结束。常用应用场景就是配合老年代的CMSGC工作
-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseParNewGC
(3)并行回收GC(Parallel)/(Parallel Scavenge)
也是一种新生代垃圾收集器,使用复制算法,也是一个并发的多线程的垃圾收集器。(新老年代都并行化收集)
JVM参数:-XX:+UseParallelGC or -XX:+UseParallelPldGC
-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseParallelGC
注:相互激活的,只要你在老年代或年轻代设置,会相互激活
老年代细讲
老年代:Serial MSC(Serial Old) 、Parallel Compacting(Parallel Old) 、 CMS
(1)串行GC(Serial Old)/(Serial MSC)
-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseParallelOldGC
(2)并发GC(Parallel Old)/(Parallel MSC)
(3)并发标记清除GC(CMS),用来老年代
CMS收集器(Concurrent Mark Sweep:并发标记清除)并发收集停顿时间短,它是与用户线程一起执行的。
开启该收集器JVM参数:-XX:+UseConcMarkSweepGC 开启该参数后自动将 -XX:+UseParNewGC打开
-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseConcMarkSweepGC
总结:GC 垃圾收集器如何选择
组合选择:
- 单CPU或内存小,单机程序:-XX:+UseSerialGC
- 多CPU,需要最大吞吐量:-XX:+UseParallelGC 或 -XX:+UseParallelOldGC
- 多CPU,最求低停顿时间,需快速响应如互联网应用:-XX:+UseConcMarkSweepGC 或 -XX:+ParNewGC
垃圾收集器 G1(Java11默认使用)
G1 是一种服务器的垃圾收集器,引用在多处理器和大容量内存环境,实现高吞吐量,尽可能满足垃圾收集暂停时间要求。
特点:
- G1 在Stop The World添加预测机制,用户可以指定期望停顿时间。
- G1 整体上采用标记-整理算法,局部是通过复制算法,不会产生内存碎片
JVM参数配置:
-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseG1GC
G1收集器运作过程:
- 初始标记(Initial Making)
- 并发标记(Concurrent Marking)
- 最终标记(Final Making)
- 筛选回收(Live Data Counting and Evacuation)
G1 比 CMS 优势
(1)G1 不会产生内存碎片
(2)可以精确控制STW。该收集器是把整个(新生代、老年代)划分多个固定大小的区域,每次根据允许停顿的时间去收集垃圾最多的区域。
8、Synchronized 和 Lock有什么区别?用新的Lock有什么好处?
- Synchronized 是关键字,属于JVM层面的,底层通过(monitorenter & monitorexit)两个指令来完成的。monitorenter(底层通过monitor对象来完成的,其实 wait/notify 等待方法也依赖于 monitor对象,只有在同步块或方法才能调用wait/notift等方法)
- Lock是具体类(在java.util.concurrent.locks包下)是API层面的锁
- Synchronized 是不需要手动释放锁,系统自动让线程释放对锁的占用;而ReentrantLock 需要手动释放锁,所以有导致出现死锁的现象(Lock和UnLock配合try/finally语句块来完成)。
- Synchronized 不可以中断,除非抛出异常或者正常运行完成;而ReentrantLock可中断的,可以设置超时方法tryLock(long timeout,TimeUnit unit)、lockInterruptibly()放代码块中,调用interrupt 方法中断。
- synchronized是非公平锁;而ReentrantLock可以是非公平和公平锁,默认非公平锁。
- 锁绑定多个条件condition:synchronized没有;Reentrant用来实现分组唤醒需要唤醒的线程,可以精确唤醒,synchronized要么随机唤醒一个要么唤醒全部。
ReentrantLock新的有什么好处?案例演示
锁绑定多个条件condition,condition是ReentrantLock独有的。
题目:多线程之间顺序调用,实现A > B > C 三个线程启动
要求:
AA打印5次,BB打印10次,CC打印15次,紧接着10轮。
9、谈一谈你对 Volatile 的理解
Volatile 是java虚拟机提供的轻量级的同步机制,它可以使在多线程共享变量时的保证可见性,及禁止指令重排,但是不保证原子性的。
解答上面三个特性后,面试官可能会引入:什么是可见性?为什么要禁止指令重排?那如何做到保证原子性?那你在哪些地方使用过Volatile?
如何解决原子性:synchronized
,但是我们不是为了number++
而使用synchronized
,太重了。那我们使用什么呢?atomic
下面正式我们了解的。atomic底层实现是CAS,而CAS底层是unsafe类实现。
10、Redis
1)、你说一下Redis有哪些常见类型?
快速回答:string、list、hash、set、zset五种常见数据类型,我相信几乎谁都知道吧。
但是,如果你要大部人脱颖而出突出与其他人不同,你是不是都来点不一样的呢?那是必须的,还需要加上这几种数据结构bitmap、HyperLogLog、Geo、Pub/Sub、stream。
如果你还想加分,那你说还玩过Redis Module,像BloomFilter,RedisSearch,Redis-ML,这个时候面试官得眼睛就开始发亮了,心想这个小伙子有点东西啊。
String字符类型、Hash散列类型、List列表类型、Set集合类型、Zset有序集合类型、Bitmap位图、HyperLogLog统计、GEO地理、Stream流(用于对日志数据结构进行建模)
Redis数据结构应用场景:
细节:Redis命令不区分大小写,但是key是区分的;help命令帮助你对哪些不熟悉的命令:
127.0.0.1:6379> help @string
(1)String场景:商品编号、订单号采用INCR命令生成、点赞数、文章阅读量这些
(2)Hash场景:购物车
# shopcar:uid101淘宝的购物车,2048是你加入购物车的商品ID,1表已加入购物,0就没有加入
hset shopcar:uid101 2048 1
# 添加商品数量,表示添加数量
hincrby shopcar:uid101 2048 1
(3)List场景:发布与订阅及消息队列、慢查询。(比如微信文章公众号)
(4)Set场景:无序无重复。(重要喔社交类的网站)
复习基本操作:
-
添加元素:sadd key member
-
删除元素:srem key member
-
获取集合所有元素:smembers key
-
判断元素是否在集合:sismember key member
-
获取集合中的元素个数:scard key
-
从集合中随机弹出一个元素,元素不删除:srandmember key 2[你想随机抽出几个]
-
从集合中随机弹出一个元素,出一个删除一个:spop set01 2[你想随机抽出几个]
-
集合的差集:属于A但不属于B的元素构成集合(sdiff key)
-
集合的交集:sinter key
-
集合的并集:sunion key
场景:
- 微信抽奖小程序
功能 | 对应使用命令 |
---|---|
用户ID,按“点击参与” | sadd key 用户ID |
显示“已有3804人参与” | Scard key(统计) |
抽奖“一二三等奖” | spop key 3(随机抽奖3个人,获得奖者后就删除);srandmember key 2(随机抽奖2人,元素不删除) |
- 微信朋友圈点赞:点赞就是添加,取消就是删除
- 微博好友关注社交关系:如共同关注的人(求交集)
- QQ内推可能认识的人:差集运算(sdiff key1 key2:表示key2可能认识key1的人;反之sdiff key2 key1)
(5)Zset场景:
基本操作命令:
- 向有序集合加入一个元素和该元素的分数:add key score member [score member…]
- 按照元素大小从小-大排序
- 返回索引start-stop之间元素:zrange key start stop
- 获取元素的分数:zscore key [member …]
- 删除元素:zrem key member [member …]
- 获取指定分数范围的元素:zrangebyscore key min max 【 withscores 】【limit offset count】
- 增加某个元素的分数:zincrby key increment member
- 获取集合中元素的数量:zcard key
- 获取指定分数范围内元素个数:zcount key min max
- 按照排序名范围删除元素:zremrangebyrank key start stop
场景:
- 商品销量排行榜,案例如下:
案例 | 操作命令 |
---|---|
商品编号101销量为8,商品编号102销量为90 | zadd goods:sellsort 9 101 90 102 |
如有客户买了5件商品101,那么我们在编号101销量增加5 | zincrby goods:sellsort 5 101 |
求商品销量前10名 | zrange goods:sellsort 0 10 withscores |
- 抖音、微博等热搜榜
(6)bitmap:
概念:bitmap称为位图,1个字节(1byte)=8位(8bit),1个byte有8个bit,每个小格子只能存放0或1,用来判断Y/N状态。当存入超过8位也就是等于一个字节(1byte)就再次扩容一组为8位的一个字节。(使用type x查看底层string类型,实质对应的是二进制ASCII编码)
Bitmap支持的最大位数是2^32位,他可以极大节约存储空间,使用512M内存就可以多达42.9亿字节信息。
场景:登录状态统计Y/N(每次签到)、签到统计、上班打卡、电影广告是否被点击过
案例:日活量、京东签到送豆豆(登录签到就是1,未签到为0)
基本命令:
命令 | 作用 | 时间复杂度 |
---|---|---|
Setbit key offset val | 给指定key的值的第offset赋值 | O(1) |
Getbit key offset | 获取指定key的第offset位 | O(1) |
Bitcount key start end | 返回指定key中[start,end]中为1的数量 | O(n) |
Bitop operation destkey key | 对不同的二进制存储数据进行位运算(and/or/not/xor) | O(n) |
Strlen | 取出字节长度(8bit=1byte) | |
案例:要求员工月活统计功能。
假设某员工(sing:u1:202110)在1-5、25、30号来打卡设置如下,然后统计出来
# 1号来设为状态为1127.0.0.1:6379> setbit sing:u1:202110 1 1(integer) 0127.0.0.1:6379> setbit sing:u1:202110 2 1(integer) 0127.0.0.1:6379> setbit sing:u1:202110 3 1(integer) 0127.0.0.1:6379> setbit sing:u1:202110 4 1(integer) 0127.0.0.1:6379> setbit sing:u1:202110 5 1(integer) 0127.0.0.1:6379> setbit sing:u1:202110 25 1(integer) 0127.0.0.1:6379> setbit sing:u1:202110 30 1(integer) 0# 统计,类似mysql的select count(*)127.0.0.1:6379> bitcount sing:u1:202110(integer) 7127.0.0.1:6379> type sing:u1:202110string
2、分布式锁
你了解分布式锁吗?
问题:
- Redis除了拿来做缓存,还可以怎么用呢?
- Redis做分布式锁时候有需要注意哪些问题?
- 如果redis是单点部署,会带来哪些问题?你如解决单点问题?
- 集群模式下,比如主从模式,有没有什么问题?
- 那你简单介绍一下Redlock?你简历上写redisson是什么?
- Redis分布式锁如何续期?看门狗知道吗?
分布式锁使用场景:多个服务键+保证同一时刻内+同一用户只能有一个请求(防止关键业务出现数据冲突和并发错误)
2)、Redis 缓存过期策略?
(1)如何查看Redis最大占用内存?通过配置文件,默认是使用最大内存的的。
(2)一般生产上你如何配置?Redis推荐配置为最大物理内存的四分之三。(0.75)
(3)你如何设置Redis内存大小?通过配置文件或者命令的方式
配置文件:
# maxmemory <bytes> 注意转换maxmemory 104857600
命令行方式:
config get maxmemory #查看Redis内存大小config set maxmemory 1 #设置
还有可以通过:info memory
(4)如果Redis内存满了会出现什么情况?会出现OOM
面试问你这个问题,其实它是想问你,当Redis满了之后,如何避免类似情况呢?此时引出内存淘汰策略。
(5)Redis 缓存淘汰策略
Redis内存淘汰策略,默认有8种。
Redis的过期策略,是有定期删除+惰性删除两种
- 定时删除:对CPU不友好,用处理器性能换取存储空间
- 懒性删除:对内存不友好,用存储空间换取处理性能
- 定期删除:指定每隔一段时间执行一次删除过期键操作,采用随机抽取的策略,利用过期数据占比的方式控制删除频度,会导致很多Key到过期时间并没有被删除。
上面都有缺点,此时我们使用Redis提供的8种内存淘汰策略,本次学习版本6.2.5:
- noeviction:不会驱逐任何Key
- allkeys-lru:对所有Key使用LRU算法进行删除
- volatile-lru:对所有设置过期时间的Key使用LRU算法进行删除
- allkeys-random:对所有Key随机删除
- volatile-random:对所有设置了过期时间的Key随机删除
- volatile-ttl:删除马上要过期的Key
- allkeys-lfu:对所有Key使用LFU算法进行删除
- volatile-lfu:对所有设置了过期时间的Key使用LFU算法进行删除
LRU means Least Recently Used #最近最少使用(时间上)
LFU means Least Frequently Used #最近最少使用(频率度)
连环问:你公司使用哪一种?或你平时使用哪一种?
一般使用allkeys-lru
,如何配置呢?通过配置文件maxmemory-policy allkeys-lru
,或命令行方式:config set maxmemory-policy allkeys-lru
常见缓存淘汰算法: FIFO 先进先出、LRU 最近最少、和 LFU 最近使用频率最低
什么是LRU算法?
最近最少使用的数据给以淘汰,是一种常用的页面置换算法。LRU核心思想:哈希双向链表实现
方式1:使用最快方式写
public class LRUCache<K,V> extends LinkedHashMap {
private int capacity; // 容量大小,也就是缓存允许最大数量
public LRUCache(int capacity) {
super(capacity,0.75F,true);
this.capacity = capacity;
}
@Override
protected boolean removeEldestEntry(Map.Entry eldest) {
return super.size() > capacity;
}
public static void main(String[] args) {
LRUCache lruCache = new LRUCache(3);
lruCache.put(1,"a");
lruCache.put(2,"b");
lruCache.put(3,"c");
System.out.println(lruCache.keySet());
lruCache.put(4,"d");
System.out.println(lruCache.keySet());
}}
方式2:面试官说,不允许使用LinkedHashMap,请你手写一个。
你是否可以在
O(1)
时间复杂度内完成这两种操作?
首先我们想到使用HashMap确定查找,双向链表模拟AQS
3)持久化
Redis支持RDB(Redis DataBase)和AOF(Append Only File)两种持久化机制,持久化功能有效地避免因进程退出造成的数据丢失问题,当下次重启时利用之前持久化的文件即可实现数据恢复。(Redis的数据是存放在内存中的,假如突然宕机,数据就会丢失,所以我们需要一种机制来确保Redis即使在宕机时数据不会丢失,此时Redis持久化机制登场)。
建议看官方文档说明。
RDB
官方介绍:在指定的时间间隔内将内存的数据集快照写入磁盘,也就是行话讲的Snapshot快照,它恢复时是将快照文件直接读到内存里。
RDB原理?
在保存RDB文件时,Redis会单独创建(fork)的一个子进程来进行持久化,此时父进程不需要做其他IO操作。fork是指redis通过创建子进程来进行RDB操作,cow指的是copy on write,子进程创建后,父子进程共享数据段,父进程继续提供读写服务,写脏的页面数据会逐渐和子进程分离开来。
RDB的缺点:最后一次持久化后的数据可能丢失,所以一般我们都是用RDB做镜像全量持久化,AOF做增量持久化,进行这样一个配合使用。
fork:作用就是复制一个与当前进程一样的进程。新的进程的所有数据都是和原进程一致的,但是是一个全新的进程,并作为原进程的子进程。
如何触发RDB快照?
(1)配置文件(默认使用出厂配置的)
################################ SNAPSHOTTING ################################
# Unless specified otherwise, by default Redis will save the DB:
# * After 3600 seconds (an hour) if at least 1 key changed
# * After 300 seconds (5 minutes) if at least 100 keys changed
# * After 60 seconds if at least 10000 keys changed
#
# You can set these explicitly by uncommenting the three following lines.
# 广商默认一下三种(可以修改)
# 1、1小时内,key改动一次,就进行触发快照机制
# 2、5分钟内,key改动100次,则触发快照机制
# 3、1分钟内,key改动10000次,则触发RDB快照机制
save 3600 1
save 300 100
save 60 10000
设置完值之后,会产生dump.rdb
文件,值得注意的是:产生的dump.rdb文件,一定一定备份到另一台备份机器上,否则你宕机了,文件就丢失,还有就是第一次触发完RDB后,第二次触出发RDB时,会将第一次产生的dump.rdb覆盖掉。
(2)如何恢复呢?
从备份机器将dump.rdb移动到Redis安装目录,并且启动服务器即可恢复。(这样就可以从新读会内存中)
(3)命令save或者bgsave都会迅速生产dump.rdb文件备考
- 使用save时,它只管保存,其他的不管,全部阻塞
- 使用bgsave时,Redis会在后台异步进行快照(也就是说,进行快照同时还可以响应客户端请求)
- save 和 bgsave区别?save命令会阻塞服务器,而 bgsave命令不会
RDB优缺点?
优点:适合大规模的数据恢复;对数据完整性和一致性要求不高
缺点:会丢失最后一次快照后的所有修改;Fork时,内存中的数据被克隆一份,2倍的膨胀性能。
AOF(Append Only File)
AOF持久化是通过保存Redis服务器所执行的写命令来记录数据库状态,只允许追加文件但不可以修改文件。redis重启后根据保存的记录指令执行完成数据的恢复。
配置文件
# 1、默认AOF持久化是关闭的
# appendonly no
# 开启
appendonly yes
# 3、appendfsync
# 3.1 always:同步持久化每次发生数据变更会被立即到磁盘,这种方式性能较差但保证数据的完整性
# 3.2 everysec:出厂默认推荐,异步操作,每秒记录,如果一秒内宕机,有数据丢失
# 3.3 no
# appendfsync always
appendfsync everysec
# appendfsync no
开启配置文件后,就会生成appendonly.aof,当你宕机时,重启redis服务器,会重新执行appendonly.aof里面所有记录恢复数据。
AOF 和 RDB共存,是如何加载的?
首先加载appendonly.aof文件,如果appendonly.aof出现错误/异常,那么我们可以执行:redis-check-aof --fix appendonly.aof
即可恢复。
AOF 的配置策略是什么?
# 3、appendfsync
# 3.1 always:同步持久化每次发生数据变更会被立即到磁盘,这种方式性能较差但保证数据的完整性
# 3.2 everysec:出厂默认推荐,异步操作,每秒记录,如果一秒内宕机,有数据丢失
# 3.3 no
# appendfsync always
appendfsync everysec
# appendfsync no
ReWrite
(1)什么是ReWrite?
aof采用文件追加方式,文件会越来越大为了避免出现此情况,新增一种机制。当AOF文件的大小超过所设定的阈值,Redis就会启动AOF文件的内容压缩,只保留可以恢复数据的最小指令集,可以使用命令bgrewriteaof
。
(2)重写原理
AOF 文件持续增长而过大时,还是会fork出一条新进程来将文件重写,然后遍历新进程的内存中数据,每一条记录有一条的set语句。
(3)触发机制
Redis会记录上一次重写时的AOF大小,默认配置时当AOF文件大小是上次rewrite后大小的一倍且文件大于默认64M时触发。
############################## APPEND ONLY MODE ###############################
...
# Specify a percentage of zero in order to disable the automatic AOF
# rewrite feature.
# 默认64mb,大型互联网是根本不够用,会很大的
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
AOF优缺点
优势:
- 每秒同步:appendfsync always 同步持久化,每次发生数据变更会被立即记录到磁盘,性能差但数据完整性好
- 每修改同步:appendfsync everysec 异步操作,每秒记录,如果1秒内宕机,有数据丢失
- 不同步:appendfsync no 从不同步
劣势:
- 相同数据集的数据而言,AOF文件要远大于RDB文件,恢复速度慢于RDB
- AOF运行效率慢于RDB,每秒同步策略效率较好,不同步效率和RDB相同
RDB 和 AOF 总结
(1)RDB持久化方式,能够在指定时间间隔内将数据进行快照
(2)AOF持久化方式,记录每次对Redis服务器写的操作,当重启服务器时会重写执行这些命令来恢复原始的数据。
(3)同步开启两种方式:同时开启时,Redis重启时候会优先加载AOF文件来恢复原始数据(先加载的原因:通常情况下AOF文件保存的数据比RDB要更加完整性)。
(4)可以只使用AOF?不建议,因为RDB更适合用于备份数据库快速重启,而且不会有AOF可能潜在bug,
常见缓存淘汰策略
常见缓存淘汰算法:FIFO先进先出、LRU最近最少使用、LFU最近使用频率最低
项目
1、讲一下秒杀流程
- 用户在秒杀商品详情页面点击秒杀按钮
- 向服务器端请求秒杀路径,主要逻辑为生成随机path值存入redis中,根据此path值拼凑秒杀路径。
- 访问拼凑的秒杀路径,先验证路径中path是否在redis中存在,如果不存在直接返回错误。
- 利用本地缓存,redis缓存做预减库存对请求做分层过滤
- 在一个事务中完成减库存下订单的过程
2、分布式Session是怎么实现的
- 用户登录后生成随机字符串,并向cookie中写入此字符串。
- 在Redis中记录此字符串和用户信息的映射
- 当用户再次访问网页时,取出cookie中对应字段值,根据此字段值访问Redis得到用户相关信息
6.如何解决重复下单?
- 执行减库存下订单逻辑前,判断是否在订单表中含有用户秒杀此商品的记录
- 利用唯一索引,在订单表中创建user_id和good_id组成的唯一索引,这样在重复插入数据的时候会插入失败,之前的减库存操作在事务中也会回滚。
- 如何防刷?
- 对一个商品秒杀时Redis会记录一个用户对一个商品的秒杀按钮的点击次数,如果用户对按钮点击次数超过5次直接返回多次请求提示
- 但是并没有对所有流量进行限流(具体见项目的不足第二条)
- 消息队列的作用?
- 削峰,减少同一时刻并发量
- 入队后直接返回用户排队中消息,提高用户体验
- 为了提升下单的效率,并且防止下单服务的失败。需要将下单这一操作进行异步处理。最常采用的办法是使用队列,队列最显著的三个优点:异步、削峰、解耦。这里可以采用rabbitmq,在后台经过了限流、库存校验之后,流入到这一步骤的就是有效请求。然后发送到队列里,队列接受消息,异步下单。下完单,入库没有问题可以用短信通知用户秒杀成功。假如失败的话,可以采用补偿机制,重试。
如何保证消息的可靠性?
生产者到RabbitMQ:事务机制和Confirm机制,注意:事务机制和 Confirm 机制是互斥的,两者不能共存,会导致 RabbitMQ 报错。
RabbitMQ到消费者
:basicAck机制、死信队列、消息补偿机制。如何保证消息不被重复消费?
正常情况下,消费者在消费消息的时候,消费完毕后,会发送一个确认消息给消息队列,消息队列就知道该消息被消费了,就会将该消息从消息队列中删除;但是因为网络传输等等故障,确认信息没有传送到消息队列,导致消息队列不知道自己已经消费过该消息了,再次将消息分发给其他的消费者。
解决:保证消息的唯一性,就算是多次传输,不要让消息的多次消费带来影响;保证消息等幂性;比如:在写入消息队列的数据做唯一标示,消费消息时,根据唯一标识判断是否消费过;
- 压测?
- JMeter压测
- 10个线程一秒5000并发量,未优化前1300QPS,优化后2100QPS
- 虚拟机4核4G,未实际部署
- 库存预减用的是哪个redis方法?
- 使用Jedis封装相关方法,减库存使用decr方法
- 缓存和数据库数据一致性如何保证?
- 对于库存数据不需要保证,缓存中的库存只为了过滤请求,即使多放进来一些请求我们也可以在数据库层面保证不超卖。
- 对于商品信息的静态数据也不需要保证数据一致性,因为不会变
- 如果项目中的redis服务挂掉,如何减轻数据库的压力
- 设置本地缓存
- 设置限流降级功能
- 做好参数校验
- 假如减了库存但用户没有支付,怎么将库存还原继续进行抢购
- 订单超时未支付则删除订单,增加库存数量,恢复Redis缓存和本地缓存的数量
- 但是对于秒杀项目之所以采用下订单减库存而不是付款减库存不就是因为秒杀商品秒到就是赚到大概率不会不付款嘛。另外即使不付款,那就不会发货,只会少卖不会超卖对于商户也不会有什么损失吧。
- 系统瓶颈在哪?
数据库
服务端网络,CPU和内存等硬件资源
对于服务端网络带宽可以向isp购买,服务器端硬件资源的话可以尽可能的加
另外可以减少耗费CPU和内存的操作,比如编码操作,序列化操作,频繁创建大对象的操作,防止出现内存泄漏
以上可以通过查看服务器运行时资源占用情况判断。
- 如何再优化?
彻底的动静分离上CDN
可以考虑把第二条的东西说说吧
我不知道了啊。。。
- 项目难点及问题解决?
- 数据一致性:防止超卖和重复下单
- 如何应对高并发:你懂得
- 如何保持高可用:你懂得
- 接口防刷
秒杀流程图:
更多文章收录于GitHub:https://github.com/metashops/GoFamily
Go 语言学习路线