文章目录
- 为什么要用Nosql
- redis入门
- redis基本知识
- 五大基础数据类型
- 三种特殊数据类型
- 事务
- 监控
- springboot整合redis
- 配置文件
- Redis持久化
- Redis发布订阅
- Redis主从复制
- 哨兵模式
- 布隆过滤器
- 缓存穿透(查不到)
- 缓存击穿(量太大,缓存过期)
- 缓存雪崩
- 分布式锁?有哪些实现方案?redis分布式锁的理解,删key的时候有什么问题?
- redis默认内存及修改
- redis内存满了之后怎么办(淘汰策略)
- redis内存淘汰策略
- LRU
- Redis6.0之前的版本真的是单线程吗?
- redis为什么是单线程的
- Redis 是如何判断数据是否过期的呢?
- Redis 给缓存数据设置过期时间有啥用?
- 过期的数据的删除策略了解么?
- 如何保证缓存和数据库数据的一致性?
- 缓存常用的3种读写策略
- 分布式ID
为什么要用Nosql
1、单机
2、缓存+mysql+垂直拆分(读写分离)
3、水平拆分+mysql集群
NoSQL特点
- 方便扩展(数据之间没有关系)
- 大数据量高性能
- 数据类型是多样型的(不需要事先设计数据库)
redis入门
redis是什么
Redis(Remote Dictionary Server ),即远程字典服务,是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API
功能
- 内存存储、持久化,内存种是断电即失,所以说持久化很重要(rdb,aof)
- 效率高,可用于高速缓存
- 发布订阅系统
启动
连接
redis-cli -p 6379
查看进程 ps -ef|grep redis
shutdown
redis基本知识
默认16个数据库,默认使用的是第0个
可以使用select进行切换数据库
redis为什么单线程还这么快
多线程不一定比单线程效率高
核心:redis是将所有的数据全部放在内存中,所以使用单线程去操作效率就是最高的,多线程会有cpu上下文切换,比较耗时,对于内存来说,没有上下文切换效率就是最高的,多次读写都是在一个cpu上的,在内存情况下,这个就是最佳方案。
五大基础数据类型
对于string 数据类型:由于Redis的高性能读写功能,而string类型的value也可以是数字,可以用作计数器(INCR,DECR)、点赞量浏览量、订单号采用incr 命令生成
对于 list 数据类型:微信订阅公众号,作者发布文章后,
lpush likeAuthor articalID1、 articalID2
对于 hash 数据类型:value 存放的是键值对,比如可以做小型购物车功能,对于同一个用户可以添加多种商品。
hset userId goodID number
对于 set 数据类型: set 数据类型不允许重复,利用这两个特性我们可以进行全局去重,比如在用户注册模块,判断用户名是否注册, SISMEMBER myset user1;微博共同关注 sinter key1 key2 (交集);
对于 zset 数据类型:有序的集合,可以做范围查找,排行榜应用、热搜。
Redis-key
String(字符串)
应用场景
- 商品编号、订单号采用incr 命令生成
- 点赞数量、浏览量
List
list 列表,它是简单的字符串列表,按照插入顺序排序,你可以添加一个元素到列表的头部(左边)或者尾部(右边),它的底层实际上是个链表结构。
使用场景
- 微信订阅公众号
set(无序,值不重复)
集合对象 set 是 string 类型(整数也会转换成string类型进行存储)的无序集合。
使用场景
-
微博共同关注 sinter key1 key2 (交集)
-
微信抽奖小程序 srandmember key1 id1 id2 (将用户放入set) scard key1(统计参与的总人数)
-
微信点赞 sadd key1 id1 id2 (新增点赞) 、srem key1 id1 (取消用户id1的点赞)、smembers key1(展示所有点赞过的用户)、scard key1 (统计总数)、sismember key1 id1(判断用户id1是否对楼主点赞过)
Hash(哈希)
Map集合,key-map,值是一个键值对集合。
Map<String,Map<Object,Object>>
使用场景
- 小型购物车
Zset(有序集合)
在set的基础上增加一个值
使用场景
- 销量排行榜 zadd good 9 1001 15 1002 (编号为1001的商品销量为9,编号为1002的销量为15)、zincrby good 2 1001 (编号为1001的商品销量增加2)、zrange good 0 10 whithscores
- 热搜
三种特殊数据类型
geospatial地理位置
geoadd
geopos
geodist
georadius
Hyperloglog
基数统计的算法
基数:不重复的元素
优点:占用内存比较小
Bitmap
位存储
1字节=8位
事务
Redis事务本质:一组命令的集合,一个事务中的所有命令都会被序列化,在事务执行的过程中,会按照顺序执行,
一次性,顺序性,排他性 执行一系列的命令
Redis事务没有隔离级别的概念
Redis单条命令保证原子性,但是事务不保证原子性
所有的命令在事务中,并没有直接被执行,而是将它们放到队列,只有发起执行命令的时候才会执行! Exec
redis的事务:
- 开启事务 multi
- 命令入队
- 执行事务 exec
监控
WATCH 命令用于监听指定的键,当调用 EXEC 命令执行事务时,如果一个被 WATCH 命令监视的键被修改的话,整个事务都不会执行,直接返回失败。
springboot整合redis
RedisAutoConfigration
配置文件
Redis持久化
Redis是内存数据库,如果不将内存中的数据库状态保存到磁盘,那么一旦服务器进程退出,服务器中的数据状态就会消失,所以Redis提供了持久化功能!
1、RDB(Redis Data base)
2、AOF
开启 AOF 持久化后每执行一条会更改 Redis 中的数据的命令,Redis 就会将该命令写入硬盘中的 AOF 文件。
让 Redis 每秒同步一次 AOF 文件,Redis 性能几乎没受到任何影响。而且这样即使出现系统崩溃,用户最多只会丢失一秒之内产生的数据
Redis发布订阅
Redis主从复制
info replication
主机可以写,从机只能读不能写,主机中的所有信息合数据,都会自动被从机保存
哨兵模式
自动选举老大
主机宕机之后再回来时,只能归并到新的主机下,当做从机,这就是哨兵模式的规则!
哨兵模式优点
- 哨兵集群,基于主从复制模式,所有的主从配置优点,它全有
- 主从可以切换,故障可以转移,系统的可用性就会更好
- 哨兵模式就是主从模式的升级,手动到自动,更加健壮
缺点:
- Redis不好在线扩容的,集群容量一旦到达上限,在线扩容就十分麻烦
- 实现哨兵模式的配置时很麻烦的,里面有很多选择
布隆过滤器
位数组
有一定的错误识别率
不同的字符串可能哈希出来的位置相同,这种情况我们可以适当增加位数组大小或者调整我们的哈希函数。
布隆过滤器说某个元素存在,小概率会误判。布隆过滤器说某个元素不在,那么这个元素一定不在。
布隆过滤器使用场景
- 判断给定数据是否存在:防止缓存穿透(判断请求的数据是否有效避免直接绕过缓存请求数据库)、判断一个数字是否存在于包含大量数字的数字集中、邮箱的垃圾邮件过滤、黑名单功能等等。
- 去重:比如爬给定网址的时候对已经爬取过的 URL 去重。
缓存穿透(查不到)
概念:用户想要查询一个数据,发现redis内存数据库没有,也就是缓存没有命中,于是向持久层数据库查询。发现也没有,于是本次查询失败,当用户很多的时候,缓存都没有命中,于是都去请求了持久层数据库。这会给持久层数据库造成很大的压力,这时候就相当于出现了缓存穿透。
解决方案:
把所有可能存在的请求的值都存放在布隆过滤器中,当用户请求过来,先判断用户发来的请求的值是否存在于布隆过滤器中。不存在的话,直接返回请求参数错误信息给客户端,存在的话才会走下面的流程。
但是,需要注意的是布隆过滤器可能会存在误判的情况。总结来说就是: 布隆过滤器说某个元素存在,小概率会误判。布隆过滤器说某个元素不在,那么这个元素一定不在。
缓存击穿(量太大,缓存过期)
热点key,大并发集中对这一个点进行访问,这个key失效的时候,大量的请求到数据库
缓存雪崩
缓存在同一时间大面积的失效,后面的请求都直接落到了数据库上,造成数据库短时间内承受大量请求。
数据预热、设置不同的失效时间比如随机设置缓存的失效时间。
分布式锁?有哪些实现方案?redis分布式锁的理解,删key的时候有什么问题?
JVM层面的锁是单机版的锁
分布式为服务架构,拆分后各个微服务之间为了避免冲突和数据故障而加入的一种锁,分布式锁
用redis做分布式锁
有可能删除别人的锁
finally块的判断+del删除操作不是原子性的
redisson
分布式锁总结
redis默认内存及修改
info memory
redis内存满了之后怎么办(淘汰策略)
redis内存淘汰策略
LRU
package com.example.test;
import java.util.HashMap;
import java.util.Map;
/**
* LRUCache(int capacity) 以正整数作为容量 capacity 初始化 LRU 缓存
* int get(int key) 如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1 。
* void put(int key, int value) 如果关键字已经存在,则变更其数据值;如果关键字不存在,则插入该组「关键字-值」。当缓存容量达到上限时,它应该在写入新数据之前删除最久未使用的数据值,从而为新的数据值留出空间。
*
* @param
* @param
* @return
*/
public class LRUCache {
private Map<Integer, DLinkedNode> cache = new HashMap<>();
private int size;
private int capacity;
private DLinkedNode head, tail;
//初始化 LRU 缓存
public LRUCache(int capacity) {
this.size = 0;
this.capacity = capacity;
//使用伪头部和伪尾部节点
head = new DLinkedNode();
tail = new DLinkedNode();
head.next = tail;
tail.pre = head;
}
public int get(int key) {
DLinkedNode node = cache.get(key);
if (node == null) {
return -1;
}
moveToHead(node);
return node.value;
}
public void put(int key, int value) {
DLinkedNode node = cache.get(key);
if (node == null) {
DLinkedNode newNode = new DLinkedNode(key, value);
addHead(newNode);
cache.put(key, newNode);
size++;
if (size > capacity) {
DLinkedNode tail = removeTail();
cache.remove(tail.key);
size--;
}
} else {
node.value = value;
moveToHead(node);
}
}
//链表头部添加节点
public void addHead(DLinkedNode node) {
node.pre = head;
node.next = head.next;//head.next=tail;
head.next.pre = node;//尾节点的pre指针指向待添加节点
head.next = node;//
}
private void removeNode(DLinkedNode node) {
node.pre.next = node.next;
node.next.pre = node.pre;
}
public void moveToHead(DLinkedNode node) {
removeNode(node);
addHead(node);
}
public DLinkedNode removeTail() {
DLinkedNode res = tail.pre;
removeNode(res);
return res;
}
//创建一个双向链表
class DLinkedNode {
int key;
int value;
DLinkedNode pre;
DLinkedNode next;
public DLinkedNode() {
}
public DLinkedNode(int key, int value) {
this.key = key;
this.value = value;
}
}
}
Redis6.0之前的版本真的是单线程吗?
Redis在处理客户端的请求时,包括获取 (socket 读)、解析、执行、内容返回 (socket 写) 等都由一个顺序串行的主线程处理,这就是所谓的“单线程”。但如果严格来讲从Redis4.0之后并不是单线程,除了主线程外,它也有后台线程在处理一些较为缓慢的操作,例如清理脏数据、无用连接的释放、大 key 的删除等等。
redis为什么是单线程的
- 用单线程模型能带来更好的可维护性,方便开发和调试;
- 使用单线程模型也能并发的处理客户端的请求;
- Redis 服务中运行的绝大多数操作的性能瓶颈都不是 CPU;
并发处理
使用单线程模型也并不意味着程序不能并发的处理任务,Redis 虽然使用单线程模型处理用户的请求,但是它却使用 I/O 多路复用机制并发处理来自客户端的多个连接,同时等待多个连接发送的请求。
在 I/O 多路复用模型中,最重要的函数调用就是 select 以及类似函数,该方法的能够同时监控多个文件描述符(也就是客户端的连接)的可读可写情况,当其中的某些文件描述符可读或者可写时,select 方法就会返回可读以及可写的文件描述符个数。
使用 I/O 多路复用技术能够极大地减少系统的开销,系统不再需要额外创建和维护进程和线程来监听来自客户端的大量连接,减少了服务器的开发成本和维护成本。
性能瓶颈
- 多线程虽然会帮助我们更充分地利用 CPU 资源,但是线程切换其实会带来额外的开销
- Redis 的操作都会在内存中完成不会涉及任何的 I/O 操作,这些数据的读写由于只发生在内存中,所以处理速度是非常快的
多线程技术能够帮助我们充分利用 CPU 的计算资源来并发的执行不同的任务,但是 CPU 资源往往都不是 Redis 服务器的性能瓶颈。
Redis 并不是 CPU 密集型的服务,如果不开启 AOF 备份,所有 Redis 的操作都会在内存中完成不会涉及任何的 I/O 操作,这些数据的读写由于只发生在内存中,所以处理速度是非常快的;整个服务的瓶颈在于网络传输带来的延迟和等待客户端的数据传输,也就是网络 I/O,所以使用多线程模型处理全部的外部请求可能不是一个好的方案。
AOF 是 Redis 的一种持久化机制,它会在每次收到来自客户端的写请求时,将其记录到日志中,每次 Redis 服务器启动时都会重放 AOF 日志构建原始的数据集,保证数据的持久性。
多线程虽然会帮助我们更充分地利用 CPU 资源,但是操作系统上线程的切换也不是免费的,线程切换其实会带来额外的开销,其中包括:
- 保存线程 1 的执行上下文;
- 加载线程 2 的执行上下文;
频繁的对线程的上下文进行切换可能还会导致性能地急剧下降,这可能会导致我们不仅没有提升请求处理的平均速度,反而进行了负优化,所以这也是为什么 Redis 对于使用多线程技术非常谨慎。
Redis 选择使用单线程模型处理客户端的请求主要还是因为 CPU 不是 Redis 服务器的瓶颈,所以使用多线程模型带来的性能提升并不能抵消它带来的开发成本和维护成本,系统的性能瓶颈也主要在网络 I/O 操作上;而 Redis 引入多线程操作也是出于性能上的考虑,对于一些大键值对的删除操作,通过多线程非阻塞地释放内存空间也能减少对 Redis 主线程阻塞的时间,提高执行的效率。
Redis 是如何判断数据是否过期的呢?
Redis 通过一个叫做过期字典(可以看作是 hash 表)来保存数据过期的时间。过期字典的键指向 Redis 数据库中的某个 key(键),过期字典的值是一个 long long 类型的整数,这个整数保存了 key 所指向的数据库键的过期时间(毫秒精度的 UNIX 时间戳)。
Redis 给缓存数据设置过期时间有啥用?
因为内存是有限的,如果缓存中的所有数据都是一直保存的话,分分钟直接 Out of memory。
127.0.0.1:6379> exp key 60 # 数据在 60s 后过期
(integer) 1
127.0.0.1:6379> setex key 60 value # 数据在 60s 后过期 (setex:[set] + [ex]pire)
OK
127.0.0.1:6379> ttl key # 查看数据还有多久过期
(integer) 56
使用场景: 很多时候,我们的业务场景就是需要某个数据只在某一时间段内存在,比如我们的短信验证码可能只在 1 分钟内有效,用户登录的token可能只在 1 天内有效。
过期的数据的删除策略了解么?
如果假设你设置了一批 key 只能存活 1 分钟,那么 1 分钟后,Redis 是怎么对这批 key 进行删除的呢?
- 惰性删除:只会在取出 key 的时候才对数据进行过期检查。这样对 CPU 最友好,但是可能会造成太多过期 key 没有被删除。
- 定期删除 : 每隔一段时间抽取一批 key 执行删除过期 key 操作。并且,Redis 底层会通过限制删除操作执行的时长和频率来减少删除操作对 CPU 时间的影响。
定期删除对内存更加友好,惰性删除对 CPU 更加友好。两者各有千秋,所以 Redis 采用的是 定期删除+惰性/懒汉式删除 。
但是,仅仅通过给 key 设置过期时间还是有问题的。因为还是可能存在定期删除和惰性删除漏掉了很多过期 key 的情况。这样就导致大量过期 key 堆积在内存里,然后就 Out of memory 了。
怎么解决这个问题呢?答案就是:Redis 内存淘汰机制。
Redis 提供 6 种数据淘汰策略:
- volatile-lru(least recently used):从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰
- volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰
- volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰
- allkeys-lru(least recently used):当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 key(这个是最常用的)
- allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
- no-eviction:禁止驱逐数据,也就是说当内存不足以容纳新写入数据时,新写入操作会报错。这个应该没人使用吧!
4.0 版本后增加以下两种:
- volatile-lfu(least frequently used):从已设置过期时间的数据集(server.db[i].expires)中挑选最不经常使用的数据淘汰
- allkeys-lfu(least frequently used):当内存不足以容纳新写入数据时,在键空间中,移除最不经常使用的 key
如何保证缓存和数据库数据的一致性?
遇到写请求是这样的:更新 DB,然后直接删除cache
如果更新数据库成功,而删除缓存这一步失败的情况的话,简单说两个解决方案:
- 缓存失效时间变短(不推荐,治标不治本) :我们让缓存数据的过期时间变短,这样的话缓存就会从数据库中加载数据。另外,这种解决办法对于先操作缓存后操作数据库的场景不适用。
- 增加 cache 更新重试机制(常用): 如果 cache 服务当前不可用导致缓存删除失败的话,我们就隔一段时间进行重试,重试次数可以自己定。如果多次重试还是失败的话,我们可以把当前更新=失败的 key 存入队列中,等缓存服务可用之后,再将 缓存中对应的 key 删除即可。
缓存常用的3种读写策略
1、Cache Aside Pattern(旁路缓存模式)
适合读请求比较多的场景。需要同时维系 DB 和 cache,并且是以 DB 的结果为准。
在写数据的过程中,可以先删除 cache ,后更新 DB 么?
不行、可能会造成数据库(DB)和缓存(Cache)数据不一致的问题
请求1先把cache中的A数据删除 -> 请求2从DB中读取数据->请求1再把DB中的A数据更新。
在写数据的过程中,先更新DB,后删除cache就没有问题了么?
可能会出现数据不一致性的问题
请求1从DB读数据A->请求2写更新数据 A 到数据库并把删除cache中的A数据->请求1将数据A写入cache。
缺陷:首次请求数据一定不在 cache 的问题
解决办法:可以将热点数据可以提前放入cache 中。
2、Read/Write Through Pattern(读写穿透)
Read/Write Through Pattern 中服务端把 cache 视为主要数据存储,从中读取数据并将数据写入其中。cache 服务负责将此数据读取和写入 DB,从而减轻了应用程序的职责。
和 Cache Aside Pattern 一样, Read-Through Pattern 也有首次请求数据一定不再 cache 的问题,对于热点数据可以提前放入缓存中。
3、Write Behind Pattern(异步缓存写入)
Write Behind Pattern 和 Read/Write Through Pattern 很相似,两者都是由 cache 服务来负责 cache 和 DB 的读写。
Read/Write Through 是同步更新 cache 和 DB,而 Write Behind Caching 则是只更新缓存,不直接更新 DB,而是改为异步批量的方式来更新 DB。
Write Behind Pattern 下 DB 的写性能非常高,非常适合一些数据经常变化又对数据一致性要求没那么高的场景,比如浏览量、点赞量。比如消息队列中消息的异步写入磁盘
分布式ID
Redis来生成分布式ID,其实和利用Mysql自增ID类似,可以利用Redis中的incr命令来实现原子性的自增与返回,比如:
127.0.0.1:6379> set seq_id 1 // 初始化自增ID为1
OK
127.0.0.1:6379> incr seq_id // 增加1,并返回
(integer) 2
127.0.0.1:6379> incr seq_id // 增加1,并返回
(integer) 3
使用redis的效率是非常高的,但是要考虑持久化的问题。Redis支持RDB和AOF两种持久化的方式。
RDB持久化相当于定时打一个快照进行持久化,如果打完快照后,连续自增了几次,还没来得及做下一次快照持久化,这个时候Redis挂掉了,重启Redis后会出现ID重复。
AOF持久化相当于对每条写命令进行持久化,如果Redis挂掉了,不会出现ID重复的现象,但是会由于incr命令过得,导致重启恢复数据时间过长。