今日,我自不量力的面试了某大厂的 Java 开发岗位,迎面走来一位风尘仆仆的中年男子,手里拿着屏幕还亮着的 Mac。他冲着我礼貌的笑了笑,然后说了句“不好意思,让你久等了”,然后示意我坐下,说:“咱们开端吧,看了你的简历v l 8 Z &,觉得你对 Redis 应该把握的不错,咱们今日就来讨论下 Redis……”。我想:“来就来,兵来将挡水来土掩”。
Redis 是什么
面试官! O b ? q 0 J 6:你先来说下 RediP h h q B t Ss 是什么吧!
我:(这不便是总结下 Redis 的界说和特色嘛)Redis 是 C 言语开发的( k ^一个开源的(遵照 BSD 协议)高功# M R w Z 0 q 用键值对(key-value)的内存数据库,能够用作数据库、缓存、音讯中间件等。
它是一种 NoSQL(not-only sql,泛指非联系型数据库)的数据库。
我顿了一下,接着说,Redis 作为一个内存数据库:
-
功用优异,数据在内存中,读写速度十分快,支撑= e y并发 1q & # k * ) * 5 t0W QPS。
-
单进程单线程,是线程安全的,选用 IO 多路复用机制。
-
丰厚的数据类型,支撑字符串(strings)、散列(hashes)、列表(lists)、调集(sets)、有序调集(sorted sets)` 5 D m n等。
-
支撑数据耐久化。
能够将内存中数据保存在磁盘中,重启时加载。
-
主从仿制,岗兵,高可用。
-
能够用作分布式锁。
-
能够作为音讯中间件运用,支撑发布订阅= [ 4 f q 。
五种数据类型
面试官:l ^ ? 5 9 g N / B总结的不错,看来是早有预备啊。刚来听你提到 Redis 支撑五种数据类型,那你能简略说下这五种数c k S & I F f j b据类型吗?
我:当然能够,可是在说之前,我觉得有必要先来T % * r M S了解下 Redis 内部_ S % { & W ^ n内存办理是怎样描述这 5 种数据类型的。
说着,我拿着笔给面试官画了一张图:
我:# . t A S首要 Redis 内部运用一个 redisObject 目标来表明一切的 key 和 value。
redisOb– L z : tject 最主要的信息如上图所示:type 表明一个 value 目标具体是何种数据类型,encoding 是不同数据类型在 Redis 内部的存储办法。
比方:tY M ? =y_ P E Q [pe=string 表明 value 存储的是一个普通字符串,那么 encodi5 u )ng 能够是 r~ z t c l s i Z Daw 或许 int。i + ~ Q S b n ,
我顿了一下,@ Z i : m接着说,下面我简略说下 5 种数据类型:
①String 是 Re$ p z $di@ d i j = ` ? }s 最基本z x h { x 3 P – m的类型,能够理解成与 Memcached如出一辙的类型,一个 Key 对应一个 Value。Vald 7 a * zue 不仅是 String,也能够是数字。8 / ? O L ; z 6
String 类型是二8 V Y f H W $ 3进制安全的,r T & f意思是 Y C r : u = _ Redis 的 String 类型能够包含任何数据,/ b 9 _ 5比方 jpg 图片或许序列化的目标。9 I 2 _ k $StrJ k 9 } & / b king 类型的值最大能存储 512M。
②Hash是一个键值(key-value)的调集。Redis 的 Hash 是一个 String 的 Key 和 Value 的映射表,Hash 特别适宜存储目标。常用指令:hge2 | r f Z , | . Gt,hsv 2 9 ] (et,hgetall 等。
③List 列表是简略的字符串列表,依照刺进次序排序。能够添加一个元素到列表的头部(左边)或许尾部(右边) 常用指令:lpusW R } K k ch、rpush、lpop、rq . E 0 ? cpop、lrange(获取列表片段)等。
应V = Z _ ^ T 6 +用场景:List 应用场景十分多,也是 Redis 最重要的数据结构之一D ) V – ,比方 Twitter 的重视列? S B m ! { C表,粉丝列表都能够用 List 结构来完结。
数据结构:List 便是链表,能够用来当音讯队列用。Redis 供给了 L) 5 ! 8 Fist 的 Push 和M | ` T [ [ 0 P_ y o l * O U + eop 操作,还供给了操作某一段的 API,能够直接查询或许删g ! z u _ 2 W去某一段的元素。
完结办法:Redis List 的是完结是一个双向链表,既能够支撑反向查找和遍历,更便利操作,不过带来了额定的内存开支。
④Set 是6 v f ] # [ | 1 String 类o ) o J B * T型的无序调集。调集是经过 hashtable 完结的。Set 中的元素是没D f n有次序的,2 k K h A _并且是没有重复的。常用指令:sdd、spop、smembers、sunion 等。
应用场景:Redis Set 对外供给的功用和 List 相同是一个列表,特殊之处在于 Set 是主动去重的,并且 Set 供给了判别某个成员是否在一个 Set 调集中。
⑤Zset 和 Set 相同是 String 类型元素的调集,且不允许重复的元素。常用指令:zadd、zrange、zrem、zcard 等。
运用场景:Sorted Sl { Het 能够经过用户额定供给一个优先级(score)的参数来为成员排序,并且是刺进有序z X b o r , ^ I L的,即主动排序。
当你需求一个有序的并且不重复的调集r C K列表,那么能够选择 Sorted Set 结构。
和 Seu ! 8 c $ C &t 比较,Sorted Set相关了v c % $一个 Doubleb v O R h c ) I w 类型权重的参数 Score,使得调集中的元素能够& G L依照 Score 进行有序i P ; 7 [ 8摆放,Redis 正是经过分数来为调集中的成员进行从小到大的排序。
完结办法:Redis Sorted Set 的内部运用 HashMap 和跳动表(skipList)来确保数据的存储和有序,HashMap 里放的是成员到 Score 的映射。
而跳动表里存放的是一切的成员,排序依据是 HashMapW U O L ? ; 里存的 Score,运用跳动表的结构能够获得比较高的查找功率,并且在完结上比较简略。
数B s E 6 | E Y v据类型应用场景总结:
面试官g C K q T C:想不到你平常也下了不少工夫,那 Redis 缓: A 6 Q m ? $ ^存你必定用过的吧?
我:用过的。
面试官m D n Y w . ( # f:那你跟我说下你是怎样用的?我是结合 Spring Boot 运用的。一般有两种办法,一种是直接经过 Redisz s T ) h b OTemplat. c $ % M 5 Ae 来运用,另q F L 1 h 2 ^ U一种是运用 Spring Cache 集成 Redi= / K T `s(也便是注解的办法)。
Redis 缓存
直接经过 RedisTemplate 来运用,运用 Spring Cache 集成 Redis pom.xml 中参加以下依靠:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactl ^ p 0Id>
</dependency>
<dependency>
<grou( { rpId>L m 7 : :;org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<dependency>
<groupId>orgR T x } M I j.springframework.boot</groupId>
<artifa: @ 7ctId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<grou; ! a ! R ~ #pId>org.springframework.session</grom z I 5upId>
<artif^ ) 2 y ~ ( C ]actId>spring-session-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifact{ y T 5 h F @ % iId>lombok</artifacu 6 0 7 J n f ptId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.bootw 0 ^ Y<n ^ t C { N 8 . s;/groupId>
<artifactId>spring-boot-starter-test</aN % s l $ artifactId>
<scov , 2 u E spe>test</scope>
</dependency>
</dep{ L % j ;endencies>
spring-boot-starter-data-h 9 $ C G | Lre6 W _ G ; 9 # ldis:在 Spring Boot 2.x 以后底层不再运用 Jedis,而是换成了 Lettuce。commons-pool2:用作 Redis 衔接池,如不引进启动会报错。C 8 / )
spring-session-da} n l d g l Qta-redis:SF $ C n K R 7 T Spring Session 引进,用作同享 Session。
装备文件 application.yml 的装备:
server:
port: 8082
servlet:
session:
timeout: 30ms
spring:
cacF : , .he:
type: rM 4 Cedis
redis:V 6 , b l 6 s
host: 127.0.0.1
port: 6379
password:
# redis默许状况下有16个分片,g g 9 v ` W Q 7 E这儿装备具体运用的分片,默许为N g m0
database: 0
lettuce:
pool:
#y f 7 z s F - 2 K 衔接池最大衔接数(运用负数表明没有限制),默许8
max-active: 100
创立实体类 User.javP ; { 9a:
public) P M ( k I . 5 f class User implements Seria` P 1 v Clizable{
private static final ld V j p K @ong serialVersion: e v v 3 e w 9UID = 662692455422902539L;
private Integer id;
private String name;
private Integer age;
pu# h K cblic User(Y O E 3 b K) {
}
public Ug { [ I _ser(Integer id, String name, Integer age) {
this.id& 8 # N K = id;
this.name = name;
this.age = age;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public vX ~ N z M z a @oid setName(String name) {
this.name = name;
}
public InteT I ? n U g 1ger getAge() {
return age;
}
public void setAg5 ^ D - :e(Integer age) {
this.age = ag% ( E q Xe;
}
@OverriU H 1 U 3de
public String toString() {
return "User{" +
"id=" + id +
", name='" + name + ''' +
", age=" + age +
'}';
}
}
RedisTemplate 的运用办法
默许状况下的模板只能支撑 RedisTemplate<String, String>q i R u d,也便是只能存入字符串,所以自界说模板很有必要。
添加装备类 ReY J Y : L x 4 ;disC@ D HacheConfig.java:
@Configuration
@AutoConfigureAfter(RedisAutoConfigl ! _ S ` @ 7 8 Huration.class)
public class RedisCacheConfig {
@Bean
public RedisTemplate<String, Serializable> redisCacheTemplatE R 5 5 c E ,e(Lettucep W d : m D r 8 =ConnectionFactory connectionFactory) {
RedisTemplate<String, SeriaW 0 e G b 2 h plizable> template = new RedisTem7 / % ; o c o [plate<>();
template.setKeySerializer(new StringRedisSerializer());
temJ v 3 S : r 5 * Vplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
template.setConnectionFactory(connectionFactory);
retu= ! 4 l t 1 v irn template;
}
}
测验类:
@Ra K f a *estController
@RequestMapping("/user")
public class UserCon` { M N ; x jtroll| ; { v m ) cer {
public static Logger5 # 3 n [ x logger = LogManager.getLogger(UE l #serController.class);
@Autowire( g J + @ rd
private StringRedisTemplate? ; O w x 3 ^ stringRedisTemplate;
@Autowired
priv. S F ^ate RedisTemplate<String : K 3 Pg, Serializable> redisCac} c ? k 4 E T 4heTemplate;
@RequestMapping("/test")
pubH H # d l 7 r slic voidr 0 v test() {
redis| $ j T H t ` : *CacheTemplate.opsForValue().set("userkey", new User(1, "张三", 25));
User user = (User) redisCacheTemplate.opsForValue().get(K 4 : / Z / E W"userkey");
logger.info("当时获取目标:{}", user.toString());
}
然后在浏览器拜访,调查后台日志 http://localhost:8082/user/test
运用 Spring Cache 集成 Redis
Spring Cache 具有很好的灵活性,不仅能够运用 SPEL(spring expression language)来界说缓存的 Key 和各种 Condition,还供给了开箱即U x F – } $用的缓存暂时存储计划,也支撑和主流的专业缓存如 EhCache、Redis、Guava 的集成。
界说接口 UserService.java:
public intG h i H A j ^ ^ lerface UserService {
User save(User user);A 9 g x | 0
void delete(int id);
User get(Integer id);
}
接口完结类 UserServiceImpl.java:
@Service
public class UserServiceImpl implementsV P R UserService{
public static] q d 8 K Z Logger logger = LogManager.getLogger(UserServiceImpl.class);
prid & l C m g l s Hvate static Map<Integer, User> userMap = new HashMap<>();
static {
userMap.put(1, new User(1, "肖战", 25K ~ 1));
userMap.put(2, new User(2, "王一博", 26));
userMap.put(A * j % X } L 9 w3, new User(3, "杨紫", 24));
}
@CachePut(value ="user", key =6 p ; i Q O O _ ^ "#user.id")
@Override
public User save(User user) {
userMap.put(usR { z fer.getId(), user);
logger.info% ; E ! g 2("进入save办法,当时存储目标:{}", user.toString())U G &;
return user;
}
@CacheEvict(valuP 1 n E , se="user) p S I - y 6 k", key = "#id")
@Override
public void delete(io | / h / 4nt id) {
userMap.remove . ) N [ I(id);
logger.info("进入delete办法,删去成功");
}
@Cacheable(value = "7 & S O T l 2user", key = "#id")
@Ovg f Q e _ g M ~ Eerride
py a = z y t ` ( 4ublic User get(Int6 ; ) z $ Leger id) {
logger.info("进入get办法,当时获取目标:{}", userMap.get(id)==null?null:userMap.ge4 V e / a st(iR n | q : ;d).toString());
return] I + Z userMap.get(id);
}
}
为了便[ u u _ Y l t 8利演示数据库的操作,这儿直接界说了一个 Map<Integer,User> userMap。
这儿的中心L h 8 D S W u X &是p k 4三个注解:
- @Cachable
- @CachePut
- @CacheEvict
测验类:UserConj Y U } Rtroller
@Res^ + M tCon. u x F A X U Ctroller
@RequestMapping("/user")
public class UserController {
public static LoT ) } ? 5 . T / fgger logger = LogManager.getLogger(Uset B (rController.class);
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Autowired
private RedisTemplate<String@ 4 H $ U ( k j t, Serializab) ^ ^ ~ - ` W L zle> redisCacheTemplate;) S p X + - 1 o u
@Autowired
private UserService userService;
@& u x o ; 7 i iRequestMapping("/test")
public void test() {
redisCacheTemplate.opsForValue().set("userkey", new User~ M @ N g Z &(1, "张三( # r q", 2F d , M f C5));
User user = (User) redisCacheTemplate_ c Z V ?.opsForValue().get("userkey");
logger.info("当0 q {时获取目标:{}", user.toString());
}
@RequestMapping("/add")
public9 k k = ; T z void add() {
User user = userServicex q : (.save(new User(4, "李现", 30));
logger.info("添加的用户信息:{}",user.toSA 9 = N Htring());
}
@RequestMapping("/delete")
public void delete() {
userService.delete(4);
}
@RequestMapping("/get/{id}")
public void get(@PathVariable("id") String idStr) throws Exception{
if (StringUtils.isBlank(idStr)) {
throw new Ex} L K U + p =cepti` $ ^ N ; k l Won("id为空");
}
Integer id = I- I C & A z Y .nteger.parseInt(idStr);
User user = userService.get(id);
logger.info("获取的用户信息:{}",userf a I J f d (.toString());
}
}
用缓存要注意,启动类要加e K (上一个注解Y ; 9 f W | } = R敞开缓存:
@SpringBootApplication(exclude=DataSourceAutoConfiguration.class)
@EnaN ; 0 y c CbleCaching
public class App. z $ e O 8lication {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
①先调用添加接口:http://localhost:8082/user/add
②再调用查询接口,查询 id=4 的用户信息:
能够看出,这儿现已从缓存中获取数据了,由于上一步 add 办法现已把 i+ + K N ? ]d=4 的用户数据放入了 Redis 缓存
③调用删去办法,删去 id=4 的用户信息,一起清除缓存:
④再次调用查询接口,查询 id=4 的用户信息:
没有了缓存,所以进入了 get 办法,从 userMap 中获取。
缓存注解
①@Cacheable依据办法的恳求参数对其成果进行缓存:
- Key:缓存的 Key,能够为空,假如指定要依照 SPEL 表达式编写,假如不指定,则依照办法的一切参数进行组合。
- Value:缓存的称号,有必要指定至少一个(如 @Cacheable (v # J k ealue=’user’)或许 @Cacheable(value={M B M‘user1′,’user2’}))
- Condition:缓存的条件,能够为空,运用 SPEL 编写,回来 true 或许 false,只Y y k有为 true 才进行缓存。
②@CachePt = Y I T p k Yut
依据办法的恳求参数对其成果进行缓存,和 @Cacheable 不同的是u / P q m X T S t,它每次都会触发实在办法的调用。参数描述见上k / i k = f M –。
③@CacheEvict
依据条件对缓存X o +进行清空:
- Key:同上。
- ValueV ` s:同上。
- Condition:同上。
- allEntries:是否清空一切缓存内容,缺省为 false,假如指定为 true,则办法调用后将& @ * r .当即清空一切缓存。
- beforeInvocation:是否在办法履行前就清空,缺省为 false,假如指定为 true, 0 C I则在办法还没有履行的时分就清空缓存。缺省状况下,假如办法履行抛出异常,则不会清空缓存。
缓存问题
面试官:看了一下你的 Demo,简略易懂。那你在实际项目中运用缓存有遇到什么问题或d 4 K o Q C U 1许会遇到什么问题你知道吗?
我:缓存和数据库数据共同性问题:分布式环境下十分简略呈现缓存和数据库间数k _ ^据共同性问题,针对这一点,假如项# s Z F # I目对缓存的要求是强共同性[ g D | | 1的,那么就不要运用缓存。
咱们O w H只能采取适宜的战略来下降缓存和数据库间数据不共同的概率,而无法确保两者间的强共同性。
适宜的战略包含适宜的缓存更新战略,更新数据库后及时更新缓存、缓存失利时添加重试机制。
面试官:Redis 雪崩了解吗?
我:我了解的,目前电商主页以及热门数据都会去做缓存,一般缓存都是定时使命去改写,或许查不到之后去更新缓存的,定时使命改写就有一个问题。
举个栗子:假如主页一切 Key 的失效时刻: $ a 8 U p z ` W都是 12 小时,正午 12 点? x ~ x |改写的,我零点有个大促活动大0 Q –量用户涌入,假设每秒 6000 个恳求,本来缓存– # ] I % ? 5能够抗住每秒 5} @ Q s R000 个恳求,可是缓存中一切 Key 都失效了。
此刻 6000 个/秒的恳 0 } ? O求全部落在了数据库上,数据库必定扛不住,实在状况或许 DU ~ W } s 5BA 都没反应过来直接挂了。此刻,假如没什么特其他计划来处理,DBA 很着急,重启数据库,可是数据库立马又被新流量给打死了。这便是我理解的缓存雪崩。
我心想:同一时刻大面积失效,瞬间 Redis 跟没有相同,那这个数量级n 2 l其他恳求直接打到数据库简直是灾祸性的K 9 ) c。
你想想假如挂的是一个用户服务的y O + ) H + j l库,那其他依靠他的库一切接口k S o B x简直都会报错。
假如没做熔断等战略基本上便是瞬间挂一片的节奏,你怎样重启用户都会把你打挂,等你重启好的时分,用户早睡觉去了,临睡之前,骂骂咧咧“* ~ l什么垃圾产品”。
面试官摸摸了自己的| w q头发:嗯,还不错,那这种状况你都是怎样应对的?
我:处理缓存雪崩简略,在批 _ b : O . A S量往 Redis7 L v 4 1 P m % v 存数据的时分,把每个 Key 的失效时刻都加个随机值就好了,这样能够确保数据不会再同一时刻大面积失效。
setRed= # x 6 P C 9 7 dis(key,value,time+Math.random()*10000);
假如 Redis 是集群布G ~ S置,将热门数据均匀分布在不同的 Redis 库中也能防止全部失效。
或许设置热门数据永不过y B 5 [ b x A 7期,有更新操作就更新缓存就好了(比方运维更新了主页产品,那你刷下缓存就好) @ p V了,) i 1 J不要设置过期时刻),电商主页的数据也能够用这个操作,保险。
面试官:那你了解缓存穿透和击穿么,能够说说他们跟雪崩的差异吗?
我:嗯,了解,先说下? v 4 L A缓存穿透吧,缓存穿透是指缓存和数F | ~ x E据库中都没有的数据,而用户(黑客)不断建议恳求。P ` e @ o J W l
举个栗子:咱们数据库的 id 都是从 1 自增的,假如建议 id=-1 的数据或许 id 特别大不存在X $ N的数据,这样的不断攻击导致数据库压力很大,严峻会击垮数据库。
我又接着说:至于缓存击穿嘛,这个跟缓存雪崩有点像,可是又有一点不相同,缓存雪崩是由于大面积的缓存失效,打崩了 DB。
而缓存击穿7 K = r G ] k ! T不同的是缓Z Z A o s G f存击穿是指一个 Ke8 ( ) d U 5 hy 十分热门,在不停地扛着大d | t o a量的恳求,大并发h & y + K A k O o集中对这一个点进行拜访,当这个 Key 在失效的瞬间,继续的大并发直接落到了数据库上,就在这个 Key 的点上击穿了缓存。
面试官显露欣喜的眼光:那N Y . [ =他们别离怎样解决?
我:G ? C [缓存穿透我会在接口层添加校验,比方用5 ` T j ! U B户鉴权,参数做校验,不合法的校验直接 return,比方 id 做根底校验,id<=0 直接阻拦。
面试官:那你还有其他办法吗?
我:我记得 Redis 里还有一个高档用法布隆过滤器(Bloom Filter)这个也能很好的预防缓存穿透的发作。
它的原理也很简略,便是利用高效的数据结构和算法快速判别出你这个 Keyf q M – I ; ( ] 是否在数据L l X h { 库中存在,不存在你 return 就好了,存在你就去查 DB 改写 KV 再 return。
缓存击穿的话,设置热门数据永不过期,或许加上互斥锁就搞定了。k % 0 | O X = 4作为暖男,代码给你预备好了,拿走不谢。
public static String getData(String key) thr! 9 = : vows InterruptedException {
//从Redis查询数据
Stv 2 j zring result = getDataByKV(key)( D 2 K @ S A - L;
//参数校验
if (String& 9 R X E : W 6Utilsp ` h E.isBlank(result)) {
try {
//获得锁
if (reenLock.tryLock()) {
//去数据库查询
result = getDataByDB(key);
//校验
if (StringUtils.isNotBlank(resultN X R G d n c v 5)) {
//插进缓存^ L O p n
setDataToKV(key, result);
}
} else {
//睡一会再拿
Thread.sleep(100L);
result = getData(key);
}
} finally {
//开释锁
reenLock.unlock();
}
}
return result;
}
面试官:嗯嗯,还不错。
RediY j O ~ J s /s 为何| ! ] ]这么快
面试官:Redis 作为缓存我` y A d & t们都在用,那 Red# 0 E W @ie g M D M c ls 必定很快咯p ( q y 6 Z s j N?
我:当然了,官方供给的数据能够到达 100000+ 的 QPS(每秒内的查询次数),这个数据不比 Memcached 差!
面试官:Redis 这么快,它的“多线程模型”你了解吗?(显露邪魅一笑)
我:您是想问 Redis 这么快,为什么仍` ! W W是单线程的吧。Redis 确实是单进程单线程的模型,由于 Redis 完全是依据内存的操作,CPU 不是 Redis 的瓶颈,Redis 的瓶颈最有或许是机器内存的巨细或许网络带宽。
既然单线程简略完结,并且 CPU 不会成为瓶颈,那就水到渠成的选用单线程的计划了(究竟选用多线程会有许多费事)。
面试官:嗯,是的。那你能说说 Redis 是单线程的,为什么还能这么快吗?
我:能够这么说吧,总结一下有如下四点:
-
Redis 完全依据内3 . ; g X存,绝大部分恳求是朴实的内存操作,十分迅速,数据存在内存中,类似于 HashMap,HashMap 的优势便是查找和操作的时刻复杂度是 O(1)。
-
数据结构简略,对数据操作也简略。
-
选用单线程,防止了不必要的上下文切换# / * (和竞争条件,不存在多线程导致的 CPU 切换,不必去考虑各种锁的问题,不存在加锁开释锁操作,没有死锁问题导致的功用消耗。
-
运用多路复用 IO 模型,非阻塞v Z L R * 3 = f p IO。
Redis 和 Memcached 的差异
面试官:嗯嗯,说的很具体。那你为什么选择 Redis 的缓存计划而不必 Memcached 呢?
我:原因有如下四点:
-
存储办法上:Memcache 会把数据全部存在内存之中,断电后会挂掉,数据不能超越内存巨细。Redis 有部分数~ o c据5 4 n $ H存在硬盘上,这样能确保数据的耐久性。
-
数据Y | n支撑类型上:Memcache 对数据类型的支撑简略,只支撑简略的 k] 5 jey-value,,而 Redis 支撑五种数据类型。
-
运用底层模型不同:它们之间底层完结办法以及H D 1 t # [与客户端之间通讯的应用协议不相同。Redis 直接自己构建6 Y B x t J a了 VM 机制,由于一般的体系调用体系函数的话,会浪费必定的时刻去移动和恳求。
-
Value 的巨细:Redis 能够到达 1GB,而 Memcache 只有 1MB。
筛选战略
面试官:那你说说6 8 ~ g N @ y t 3你知道的 Redis 的筛选战略有哪些?
我:Redis 有六种筛选战略,如下图:
弥补一下:Redis 4.0 参加了 LFU(least frequency use)筛选战略,包含 volatile-lfu 和 allkeys-lfu,经过计算) 1 0拜访频率,将拜访频率最少,即最不常常运用的 KV 筛选。
耐久化
面试官:你对 Redis 的耐久化k . + P d A机制了解吗?能讲一下吗?
我:Redis 为了确保功率,数据缓存在了内存中,可是会周: t L m z T v Q }期性i . M的把更新的数据写入磁盘或许把修正J 2 @ I D操作写入追加的记录文件中,以确保数据的耐久化。
Redis 的耐久化战略有两种:
-
RDB:快照方法是直接把内存中的数据保存到一个 dump 的文件中,定时保存,保存t S q M , A z战略。
-
AOF:把一切的对 Redis 的服务器进行修正的指令都存# P ~到一个文件里,指令的调集。Redis 默许是快照 RDB 的耐久化办法。
当 Redis 重启的时分,它会优先运用 AOF 文件来复原X U v , z 9 + `数据集,m D a k 6 ? q由于 AOF 文件保存的数据集通常比 RDB 文件所保存的数据集更完整。你甚至能够封闭耐久化功用,让数据只在服务器运( o [转时存。
面试^ i | ` N p [ y d官:那你再说下 RDB 是怎样作业的?
我:默许 Redis 是会以快照”RDB”的方法将数据耐久化到磁u Z I 2 7盘的一个二进制文件 dump.rdb。
作业原理简略说一下:当 Redis 需求做耐久化时,Redis 会 fo9 4 O m Z ` xrk 一个子进程,子进程将数据写到磁盘上一个暂时 RDB9 3 $ m i l # 文件中。
当子进程完结写暂时文件后,将原来的 RDB 替换掉,Y ] b S r a n ]这样的好处是能够 copy-on-write。
我:RDB 的长处o % O 2 K是:这种文件十分适宜用于备份:比方,你能够在最近的 24j c l { w g 小时内,每小时备份一次,并且在每个月的每一天也备份一个 RDB 文件。
这样的话,即使遇上问题,也能够随时将数据集复原到不同b J 6 P e 6 = v的c { / 1版别。RDB 十分适宜灾祸康复。
RDB 的缺点是:假如你需求尽量防止在服务器毛病时丢掉数据,那么RDB不适宜你。
面试官:那你要不x F S m v = @再说下 AOF?
我:L z _ C :(说就一同说下吧)运用 AOF 做耐久化,每一个写指令都经过 write 函数追加到 apQ A = Q ) : w 1pendonly.aof 中,装备办法如下:
appenx O J m G : 4 5dfsync yes
appeJ & r mndfsy& { z : , M z Tnc always #每次有数据修正发M M ^ n k A g n作时都会写入AOF文件。
appendfsw e 7ync evera = - # E vysec #每秒钟同步一次,该战略为AOF的缺省战略。
AOF 能够2 8 ^ / N ~ /做到全程耐久化,只需求在装备中敞开 appendonly yes。这样 Redis 每n 9 ` h 9履行一个修正数据的指令,都会把它添加到 AOF 文件中,当 Redis 重启时,将会读取 AOF 文件进行重放,康复到 Redis; & ? 0 . t d C 封闭前的最后时刻。
我顿了一下,继续说:运用 AOF 的长处是会让 Redis 变得十分耐久。能够设置不同的 Fsync 战略,A} + )OF的默许战略是每秒钟 FsyncA = O 一次,在这种装备下,就算发作毛病停机,也最多丢掉一秒钟的数据。
缺点是对于相同的数据集来说,AOF 的文件体积通常要大于 RDB 文件的体积。依据所运用的P / a $ Fsync 战略,AOF 的速度或许会慢于 RDB。
面试官又问:你说了这么多,那我该用哪一个呢?
我:假如你十分关心你的数据,但仍然能够承受数分钟内的数据丢掉,那么能够额只运用 RDB 耐久。
AOF 将 Redia { [ b m j I ^s 履行的每一条指令追J K b d V A /加到磁盘中,处理巨大的写入会下降Redis的功用,不知道你是否能够接受。
数据库备份和灾祸康复j E d 0 z y 7:定y | K时生成 RDB 快照十分便于进行数据库备份,并且 RDB 康复数据集的速度也要比 AOF 康复的速度快。
当然了,RediG W I v _ & us 支撑一起敞开 RDB 和 AOF,体系重启后,Redis 会优先运用 AOF 来康复数据,这样丢掉的数据会最少。
主从仿制
面试官:Redis 单节点存在单点毛病问题,为了解决单点问题,一般都需求r R s = l Q s L y对 Redis 装备从节点,然后运用岗兵来监听主节点的存活状况,假如主节点挂掉,从节点能继续供给缓存功用,你能说说 Redis 主从仿制的进程和原理吗?` y w f F #
我有点懵,这个说来就话长了。但幸好提前预备了:主从装备结合岗兵形式能解决单点毛病问题,进步 Redis 可用性。
从节点仅供给读操作,主节点供给写操作。对于读多写少的状况,可给主节点装备多个从节点,从而进步呼应功率。
我顿了一下,接着说:关于仿制进程,是这样的:
- 从s w W % / ; K节点履行 slaveof[mastew 4 a ^ t yrIP][masterPort],保存主节点信息。
- 从节点中的定时使命发现主节点信息,L n o O N . g t树立和主节点的 Socket 衔接。
- 从节点发送 Ping 信号r u ] = / W S D,主节点回来 Pong,两边能相互通讯。
- 衔_ Y }接树立后,主节点将一切数据发送给从节点(数据同步)。
- 主节点@ , S f n * 把当时的数据同步给从节点后,便完结了仿制的树立进程。接下来,主节点就会继续的把写指令发送给从节点,确保主从数据共同性。
面试官:那你能具体说下数据同步的进程吗?
(我心想:这也问的太细了吧)我:p [ ,能够。Redis 2.8 之前运用 syncy Q , ` j H[runId][offset] 同步指令,Redis 2.8 之后运用 psync[runId][offset] 指令。
两者不同在于,Sync 指g t g ? } k E +令仅支撑全量仿制进程,Psync_ z , ~ 支撑全量和部分仿制。
介绍同步之前,先* a 5 7 n |介绍几个概念U 8 Z , Z q g K:
-
runId:每个 Redis 节点启动都会生成仅有的 uuid,每W F & d ?次 Redis 重启后,runId 都会发作变化I U o x t h f。
-
offset~ – E V ^ E m T:主节点和从节点都各自维护自己的主从仿制偏移量 offset,当主节点有写入指K 5 p令时,offset=offset+指令的字节长度。
从节点在收到主节点发送的指令后,也会添加自己的 offset,并把自己的 offset 发送给主节点。
这样,主节点一起保存自己的 offset 和从节点的 offset,经过比照 offd r 5 t g Uset 来判别主从节点数据是否共同。
-
repl_backl G Z eog_size:保存在主节点上的一个固定长l N M j V 4 ! [度的先进f u + –先出队列,默许巨细是 1MB。
主节点发送数据给从节点进程中,主节点还会进行一些写操作,这时分的– L ) U数据存储在仿制缓冲区中。
从节点同步主节点数据完结后,主节点将缓冲区的数据继续发送给从节点,用[ G K E V于部分仿制。
主节点呼应写K 2 u @ D指令时,不但会把命名发送给从节点,还会写入仿制积压缓冲区,用于仿制指令丢掉的数据弥补。
上面是 Psync 的Q b Q g履行流程,从节点发送1 C [ ~ { psync[runId][offset] 指令,主节点有三种呼应:
- FULLREK V O PSYNC:第一次衔接,进行全量仿制
- CONTINUE:进行部分仿制
- ERR:不支撑 psync 指令,进行全量仿制
面试官:很好,那你能具体说下全量仿制和部分仿制的进程吗?
我:能够!0 i + q ! f K 7
上面是全量仿制的` 5 t f D G + h流程。主要有v X g , { q Z以下几步:
-
从节点发送 psync ? -1 指令(由于第一次q b D #发送,不知道主节点的 runId,所n 6 a r G认为?,由于是第一次仿制,所以 offset=-u H U U &1)。
-
主节点发现从节点是第一次仿制,回来 FULLREt D g + fSYNC {runId} { ~ N Doffset},runId 是主节点的 runId,offset 是主节: v @ g 7点目前的 offset。
-
从节点接收主节点信息后,保存到 info 中。
-
主节点在发送 FULLRESYNC 后,启动 bgsave 指令,生成 RDB 文件(数据耐久化)。
-
主节点发送 RDB 文件给从节点。到从节点加载数据完结这段期间主C 3 G } Y c S N节点的写指令放入缓冲区。
-
从节点~ H B清理自己的数据库数据。
-
从节点加载 RDB 文件,将数据保存到自己的数据库中d M A &。假如从节点敞开了 AOF,从节点会异步重写 AOF 文件。
关于部分仿制有以下几点说x F y T V z _ , o明:
①部分仿制主要是 Redis 针对全量仿制的过高开y U X H R支做出的一种优化办法,运用 psync[runId][offset] 指令完结[ K & + R D。
当从节点正在仿制主节点时,假如呈现网络闪断或许指令丢掉等异常状况时,2 X V从节点会向主节点要求补发丢掉的指令数据,主节点的仿制积压缓冲区将[ K r ` N O这部分数据直接发送给从节点。
这样就能够坚持主从节点仿制的共同性。补发的这部分数据一般远远小于全量数据。
②主从衔= z A $ F M 5 G b接c i ` 3 ? K ?中断期间主节点依然呼应指令,但因仿制衔接^ K x 2中断指令无法发送给从节点,不过主节点内的仿制积压缓冲区依然能够保存最近一段时刻的写指令数据。
③当主从衔接康复后,由于从节点之前保存了本身[ { 2 d ! B已仿制的偏移量和主节点的运转 ID。因此会把它们当做 psync 参数发送给主节点,要求进行部分仿制。
④主节点接收到 psync 指令后首要核对参数 r; m M p O WunId 是否与本身共同,假如共同,说明之前仿制的是当时主节点。
之后依据参数 offset 在仿制积压缓冲区中查找,假如 offf u ^ 6 _ 2 4 7set 之后的数据存在,则对从节点发送+COUTINUE 指令,表明能够9 c y x 6 + { ^ Y进行部分仿制。由于缓冲区巨细固定,若发作缓冲溢出,则进行Y ; % o & d O A全量仿制。
⑤主节点依据偏移量把仿制积压缓冲区里的数据发送给从节点,确保主` u } b q ? J 9 z从仿制进入正常状况。
岗兵
面试官:那主从仿制会存在哪些问题呢?
我:主从仿制会存在以下问题:
-
一旦主节点宕机,从节点晋升为主节点,一起需求修正应用方的主节点地址,还需求指令一切从节点去仿制新的主节点,整个进程需求人工干预。
-
主+ v a F [ Y V V Y节点的写能力遭到单机的限制。
-
主节0 C d ? 9 i J y点的存储能力遭到单机的限制。
-
原生仿制的弊端在前期的版别中也会比较突出,比方:Redo E 1 I B B X (is 仿制中断后,从节点会建议 psync。
此刻假如同步不成功,则会进行全量同步,主库履行全量备份的一起,或许会形H ; t , 9 c ? 3成毫秒或秒级的卡顿。
面m ) | Z试官:9 D A Z m , T a那比较主流的解决计划是什么呢?
我:当然是岗兵啊。
面试官:那么问题又来了。那你说下岗兵有哪些功用?
我:如图,是 Redis Sentin2 9 p H ] E Iel(岗兵)的架构图。Redis Sentinel(岗兵)主要功用包含主Y j ; C Q q节点存活检测、主从运转状况检测、主动毛病搬运、主从切换。
Redis SentinQ H % h rel _ al 最小装b ^ #备是一主一从。Redis 的 Sentinel 体系能够用来办理多个 Redis 服务器。
该体u J ! N p n系能够履行以下四个使命:
-
监控:不断检查主服务器和从服务器是否正常运转。
-
通知:当被监控的某个 Redk E 9 g y 8 u J &is 服务器呈现问题,Sentinel 经过 API 脚本向办理员或许其他应用程序发出通知。
-
主动毛病搬运:当主节点不能正常作业时,Sentinel 会开端一次主动的毛病搬运操作,它会将与失效主节点是主从联系的其中一个从节点晋级为新的主节点,并且将其他的从节点指向新的主节点,这样人工干预就能够免了。
-
装备供给者:在 RedP 5 1 – P k % 6is Sentinel 形式下,客户端应用在初始化时衔接的是 Sentinel 节! F D x + % v 4 G点调集,从中获取主节点的信息。
面试官:那你能说下岗兵的作业原理吗?
我:话不多说,直接上图:
①每个 Sentinel 节点都需求定期履行以下使命:每个 Sentinel 以每秒k & – t } S一次的频率,向它所知的主服务器、从服务器以及其他的 Sentinel 实例发送一个 PING 指令。(如上图)
②假如一个实例间隔最后一次有用回复 PING 指令的时刻超越 down-after-milliseconds 所指定的u _ H z m W G I值,那么这个实例会被 Sentinel 符号为片面下线。(如上图)
③假如一/ H a l F个主服务器被符号为片面下线,那么正在监视这个服务器的一切 Sentinel 节点,要以每秒一次的频率确认o B , z @ 8主服务器确h 5 E – { $ l h [实进入了片面下线状况。
④假如一个主服务器被符号为片面D W a & &下线,并且有满足数量的 Sentinel(至少要到达装备文件指定的数量)在指定的时刻范围内同意这一判别,那么这个主服务器被q 3 符号为客观下线. r w。
⑤一般状况下,每个 Sentinel 会以每 10 秒一次的频率向它已知的一切主服务器和从服务器发送 INFO 指令。
当一个主服务器被符号为客观下线时J c & w r . o L 4,Sentinel 向下线主服务器的一切从服务器发送 INFO 指令的频率,会从 10 秒一次改为每秒一次。
⑥Sentinel 和其他 Sentinel 协商客观下线的主节点的状况,假如处于 SDOWN 状况,则投票主动选出新的主节点,将剩余从节点指向新的主节点进行数据仿制。
⑦当没有{ L | f |满足数量的 Sentinel 同意主服务器下线时,主服务器的客观下j 0 6 q k线状况就会被移除。
当主服务器重新向 Sentinel 的 PING 指令回来有用回复时,主服务器的片面下线状况就会被移除k P – P M ^ b P d。
面试官:不错,面试前没少下工夫啊,今日 Redis 这关你过l k 4 ( r J了,明天找个时刻咱们再聊聊其他的。(显– ) { }露欣喜的浅笑)
我:没问题。
总结
本文在一次面试的进程中叙述了 Redist N R ) v 是什么,Redis 的特色和功用,Redis 缓存的运用,Redis 为什么能这么快,Redis 缓存的筛选战略,耐久化的两种办法,Redis 高可用部分的主从仿制和岗兵的基本原理。
只要功夫: | A m 8 6深,铁杵磨成针,平常预备好,面试不必慌。虽然面试} 5 # 3 p j 9 3不必定是这样问的,但M 0 R / H g e万变不离其“宗”。
转载自微信大众号
原文链接:mp.weixin.qq.com/s/eI2yXPKOn…@ ; & * / h b
共同进步,学习共享1 b 5 b X u
欢迎我们重视我的大众号【惊涛骇浪如码】,海量Java相关文章,学习材料都会在里面更e @ D新,整理的材料也C X D { [会放在里面。
觉得写的还不错的就点个赞,加个重视呗!点重视7 { _ p h f V 5,不迷路,继续更新!!v ] / 4 3 Q u!