1、redis持久化
请看我的这篇文章 redis持久化
2、redis单线程为什么这么快
- 纯内存操作
- 单线程操作,避免频繁上下文切换
- 采用IO多路复用模型
- 纯ANSI C语言编写
2.1、linux5种IO模型
- 同步阻塞BIO
- 同步非阻塞NIO
- IO多路复用
- 事件驱动
- 异步非阻塞AIO
2.2、NIO和IO多路复用的对比
- NIO实际就是多路复用IO
- 在IO多路复用模型中,会有一个线程不断去轮询多个socket的状态,只有当socket真正有读写事件时,才真正调用实际的IO读写操作
- 在Java NIO中,是通过selector.select()去查询每个通道是否有到达事件,如果没有事件,则一直阻塞在那里,因此这种方式会导致用户线程的阻塞
- 多路复用IO比NIO的效率高的原因:在非阻塞IO中,不断地询问socket状态时通过用户线程去进行的,每调用一次就得在用户态和核心态切换一次,而在多路复用IO中,轮询每个socket状态是内核在进行的,这个效率要比用户线程要高的多
2.3、cpu状态切换
- 用户态:运行应用程序
- 核心态:运行操作系统程序,操作硬件
2.4、redis线程模型
Redis内部实现采用epoll+自己实现的简单的事件框架。 epoll中的读、写、关闭、连接都转化成了事件,然后利用epoll的多路复用特性, 绝不在io上浪费一点时间
简单来说,就是。我们的redis-client在操作的时候,会产生具有不同事件类型的socket。在服务端,有一段I/0多路复用程序,将其置入队列之中。然后,IO事件分派器,依次去队列中取,转发到不同的事件处理器中
2.4.1、IO多路复用的封装
- select是POSIX提供的, 一般的操作系统都有支撑
- epoll 是LINUX系统内核提供支持的
- evport是Solaris系统内核提供支持的
- kqueue是Mac 系统提供支持的
2.4.2、文件事件分派器(Reactor)
Reactor 设计模式:事件驱动循环流程
Redis 服务采用 Reactor 的方式来实现文件事件处理器(每一个网络连接其实都对应一个文件描述符)
用户线程注册事件处理器之后可以继续执行做其他的工作(异步),而Reactor线程负责调用内核的select/epoll函数检查socket状态。当有socket被激活时,则通知相应的用户线程(或执行用户线程的回调函数),执行handle_event进行数据读取、处理的工作。由于select/epoll函数是阻塞的,因此多路IO复用模型也被称为异步阻塞IO模型。注意,这里的所说的阻塞是指select函数执行时线程被阻塞,而不是指socket
3、redis和Memecache的区别
3.1、存储方式
Memecache把数据全部存在内存之中,断电后会挂掉,数据不能超过内存大小。 Redis有部份存在硬盘上,redis可以持久化其数据
3.2、数据支持类型
memcached所有的值均是简单的字符串,redis作为其替代者,支持更为丰富的数据类型 ,提供list,set,zset,hash等数据结构的存储
3.3、使用底层模型不同
它们之间底层实现方式 以及与客户端之间通信的应用协议不一样。 Redis直接自己构建了VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求
3.4、value 值大小不同
Redis 最大可以达到 512M;memcache 只有 1mb
3.5、其它
redis的速度比memcached快很多
Redis支持数据的备份,即master-slave模式的数据备份。
4、热点数据和冷数据
热点数据缓存才有价值
冷数据:如果缓存还没起作用就失效了,就没有意义了
热点数据分两种,频繁修改的和不频繁修改的。一般来说频繁修改的数据不适合缓存,但如果读取这个数据对数据库压力大,那也应该用缓存
4.1、什么样的数据适合缓存
5、redis命令大全
请看我的这篇文章 redis命令大全
6、redis键的过期策略
请看我的这篇文章 redis过期键删除策略
7、缓存雪崩、缓存穿透、缓存击穿
7.1、缓存雪崩
可以简单理解为:原有缓存失效,新缓存未到期
例如:我们设置缓存时采用了相同的过期时间,在同一时刻出现大面积的缓存过期),所有原本应该访问缓存的请求都去查询数据库了,而对数据库CPU和内存造成巨大压力,严重的会造成数据库宕机。从而形成一系列连锁反应,造成整个系统崩溃
解决办法:
1、大多数系统设计者考虑用加锁( 最多的解决方案)或者队列的方式来保证不会有大量的线程对数据库一次性进行读写,从而避免缓存失效时大量的并发请求落到底层存储系统上
2、还有一个简单方案就是将缓存失效时间分散开
7.2、缓存穿透
缓存穿透是指用户查询数据,在数据库没有,自然在缓存中也不会有。这样就导致用户查询的时候,在缓存中找不到,每次都要去数据库再查询一遍,然后返回空(相当于进行了两次无用的查询)
这时的用户很可能是攻击者,攻击会导致数据库压力过大
解决方法:
1、如果一个查询返回的数据为空(不管是数据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。通过这个直接设置的默认值存放到缓存,这样第二次到缓冲中获取就有值了,而不会继续访问数据库,这种办法最简单粗暴
2、布隆过滤器
7.3、缓存击穿
缓存击穿实际上是缓存雪崩的一个特例
缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力
击穿与雪崩的区别在于击穿是对于某一特定的热点数据来说,而雪崩是全部数据
解决办法:
1、加锁
static Lock reenLock = new ReentrantLock();
public List<String> getData04() throws InterruptedException {
List<String> result = new ArrayList<String>();
// 从缓存读取数据
result = getDataFromCache();
if (result.isEmpty()) {
if (reenLock.tryLock()) {
try {
System.out.println("我拿到锁了,从DB获取数据库后写入缓存");
// 从数据库查询数据
result = getDataFromDB();
// 将查询到的数据写入缓存
setDataToCache(result);
} finally {
reenLock.unlock();// 释放锁
}
} else {
result = getDataFromCache();// 先查一下缓存
if (result.isEmpty()) {
System.out.println("我没拿到锁,缓存也没数据,先小憩一下");
Thread.sleep(100);// 小憩一会儿
return getData04();// 重试
}
}
}
return result;
}
2、定时任务主动刷新缓存
7.4、缓存预热
缓存预热就是系统上线后,将相关的缓存数据直接加载到缓存系统。这样就可以避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题!用户直接查询事先被预热的缓存数据!
解决方法:
1、直接写个缓存刷新页面,上线时手工操作下
2、数据量不大,可以在项目启动的时候自动进行加载
3、定时刷新缓存
7.5、缓存降级
当访问量剧增、服务出现问题(如响应时间慢或不响应)或非核心服务影响到核心流程的性能时,仍然需要保证服务还是可用的,即使是有损服务。系统可以根据一些关键数据进行自动降级,也可以配置开关实现人工降级。
降级的最终目的是保证核心服务可用,即使是有损的。而且有些服务是无法降级的(如加入购物车、结算)。
在进行降级之前要对系统进行梳理,看看系统是不是可以丢卒保帅;从而梳理出哪些必须誓死保护,哪些可降级
8、Redis 常见性能问题和解决方案?
(1) Master 最好不要做任何持久化工作,如 RDB 内存快照和 AOF 日志文件
(2) 如果数据比较重要,某个 Slave 开启 AOF 备份数据,策略设置为每秒同步一次
(3) 为了主从复制的速度和连接的稳定性, Master 和 Slave 最好在同一个局域网内
(4) 尽量避免在压力很大的主库上增加从库
(5) 主从复制不要用图状结构,用单向链表结构更为稳定,即: Master <- Slave1 <- Slave2 <-Slave3…
9、为什么Redis的操作是原子性的,怎么保证原子性的?
对于Redis而言,命令的原子性指的是:一个操作的不可以再分,操作要么执行,要么不执行。
Redis的操作之所以是原子性的,是因为Redis是单线程的。
Redis本身提供的所有API(单个命令)都是原子操作,Redis中的事务其实是要保证批量操作的原子性。
多个命令在并发中也是原子性的吗?
不一定, 举例:1、将get和set改成单命令操作;2、incr
如何解决:使用Redis的事务,或者使用Redis+Lua的方式实现
10、redis分布式锁
请看我的这篇文章 redis分布式锁的正确使用姿势
11、redis实现消息队列
11.1、普通消息队列
利用list数据类型的LPUSH和RPOP命令,循环生产和消费
11.2、阻塞消息队列
利用BLPOP和BRPOP命令
BRPOP key timeout #timeout为0表示永远阻塞
11.3、优先级队列
场景:小白新写了一篇博客,1000个用户订阅了小白,小白发表博客后,会将1000个用户加入队列中,发提醒邮件,此时有一个新用户订阅小白的博客,新用户会收到确认订阅的邮件,如果不排优先级的话,新用户收到订阅邮件会在发送1000个提醒邮件之后,这个时间太长了。从这个业务场景来看,提醒邮件没有确认邮件优先级高
BRPOP key1 key2 ... timeout #支持多个键,最左边的键优先级最高
11.4、发布与订阅
消息不会持久化
SUBSCRIBE channel1 channel2 ...
UNSUBSCRIBE channel1 channel2 ...
PSUBSCRIBE pattern #支持glob风格通配符
PUNSUBSCRIBE pattern
publish channel message
订阅之后会收到三条消息,第一条消息的含义是消息类型:
- subscribe
- message
- unsubscribe
12、管道技术pipeline
redis的使用瓶颈在于网络延迟,其次是cpu和内存
管道技术最显著的优势是提高了redis服务的性能
通过pipeline方式当有大批量的操作时候。我们可以节省很多原来浪费在网络延迟的时间。需要注意到是用pipeline方式打包命令发送,redis必须在处理完所有命令前先缓存起所有命令的处理结果。打包的命令越多,缓存消耗内存也越多。所以并是不是打包的命令越多越好。具体多少合适需要根据具体情况测试
12.1、redisTemplate实现管道
List<Object> objects = redisTemplate.executePipelined(new RedisCallback<Object>() {
@Override
public Object doInRedis(RedisConnection conn) throws DataAccessException {
//openPipeline可以调用,也可以不调用,但是closePineline不能调用
//RedisConnection是redis原生的连接,所以入参和出参都是byte[]类型
conn.openPipeline();
for (int i = 0; i < 10000; i++) {
conn.hSet("hash_pipeline".getBytes(), ("name" + i).getBytes(), "zhangsan".getBytes());
}
//这里必须返回null
return null;
}
});
13、redis集群
请看我的这篇文章 redis复制、哨兵、集群详细介绍
14、redis内部数据结构
请看我的这篇文章 面试:Redis五种基本类型的底层数据结构有哪些?
15、redis经典实例
15.1、使用有序集合实现排行榜
- 世界杯竞猜积分排行榜
- 游戏等级排行榜
- 标签引用次数排行榜
如果使用mysql数据库,则采用order by+limit获取前100名,如果数据量大,全表扫描肯定很慢