Redis从入门到高级笔记【涵盖重点面试题】
- NoSQL数据库
DBEngines网站中会统计目前数据库在全世界的排名
1.1 什么是NoSQL
最常见的解释是"non-relational",很多人说它是"Not only SQL"
NoSQL仅仅是一个概念,泛指非关系型数据库
区别于关系型数据库,他们不保证关系数据的ACID特性
1.2 NoSQL的特点
应用场景
高并发的读写
海量数据的读写
高可扩展性
速度快
不适用场景
需要事务支持
基于sql的结构化查询存储,处理复杂的关系,需要即席查询(用户自定义查询条件的查询)
1.3 NoSQL有哪些
1.3.1 memchache
很早出现的NoSQL数据库
数据都在内存中,一般不持久化
支持简单的key-value模式
一般是作为缓存数据库辅助持久化的数据库
1.3.2 redis
几乎涵盖了memchache的绝大部分功能
数据都在内存中,支持持久化,主要用作备份恢复
除了支持简单的key-value模式,还支持多种数据结构的存储,比如 list,set,hash,zset等
一般是作为缓存数据库 辅助持久化的数据库
现在市面上用得非常多的一款内存数据库
1.3.3 mongoDB
高性能、开源、模式自由(schema free)的文档型数据库
数据都在内存中,如果内存不足,把不常用的数据保存到硬盘
虽然是key-value模式,但是对value(尤其是json)提供了丰富的查询功能
支持二进制数据及大型对象
可以根据数据的特点替代RDBMS,成为独立的数据库,或者配合RDBMS,存储特定的数据库
1.3.4 列存储HBase
HBase是Hadoop项目中的数据库,它用于需要对大量的数据进行随机、实时读写操作的场景中。HBase的目标就是处理数据量非常庞大的表,可以用普通的计算机处理超过10亿行数据,还可以处理有数百万列元素的数据表
- Redis介绍
2.1 Redis的基本简介
Redis是当前比较热门的NoSQL系统之一
它是一个开源的,使用ANSI C语言编写的key-value存储系统
和memcache类似,但是很大程度上补偿了memcache的不足,redis数据都是缓存在计算机内存中,不同的是,memcache只能将数据缓存到内存中,无法自动定期写入硬盘,这就表示,一断电或者重启,内存清空,数据就会丢失。
2.2 Redis的应用场景
取最新N个数据的操作
排行榜应用,取TOP N操作
需要精准设定过期时间的应用
计数器应用
uniq操作,获取某段时间所有数据排重值
实时系统,发垃圾系统
缓存
2.3 Redis的特点
高效性
读取速度是110000次/s,写的速度是81000次/s
原子性
支持多种数据结构 string list set hash zset
稳定性:持久化,主从复制(集群)
其他特性:支持过期时间,支持事务,支持订阅
- Redis单机环境安装
# 下载redis安装包
cd /opt/software
wget http://download.redis.io/releases/redis-3.2.8.tar.gz
#解压redis压缩包到指定目录
tar -zxvf redis-3.2.8.tar.gz -C /opt/module/
# 安装c程序运行环境
yum -y install gcc-c++
# 安装tcl
# 在线安装
yum -y install tcl
# 编译redis
cd redis-3.2.8/
make MALLOC=libc
make test && make install PREFIX=/opt/module/redis-3.2.8
# 修改redis配置文件
cd /opt/module/redis-3.2.8/
mkdir log
mkdir data
vim redis.conf
# 修改第61行
bind hadoop104.sl.cn
# 修改第128行
daemonize yes
# 修改第163行
logfile "/opt/module/redis-3.2.8/log/redis.log"
# 修改第247行
dir /opt/module/redis-3.2.8/data
# 启动redis
cd /opt/module/redis-3.2.8
bin/redis-server redis.conf
# 关闭redis
bin/redis-cli -h hadoop104 shutdown
# 连接redis客户端
bin/redis-cli -h hadoop104
- Redis的数据类型
string字符串
list列表
set集合
hash表
zset有序集合
4.1 对字符串string的操作
# 设置获取key
set hello world
get hello
# 批量设置获取key
mset hello1 world1 hello2 world2
mget hello1 hello2
# 设置一个key,并指定过期时间
setex hello 3 world
# INCR +1 DECR -1 将key中存储的数字加1或者减1
set pv 1
INCR pv
注意:
在执行累加器的操作时,千万不能使用set/get来操作,会造成一些数据被覆盖【多个线程同时在操作redis缓存】
要使用INCR/DESC/INCRBY,就是执行原子的累加/累减
4.2 对hash表的操作
# 设置获取key 大key和小key
hset userinfo userid 1 --> hget userinfo userid
hset userinfo username sun --> hget userinfo username
# 获取hash中所有key
hkeys userinfo
# 获取hash中所有的key和值
hgetall userinfo
4.3 对list列表的操作
# 往列表的头部插入数据
lpush list 1 2 3 4
# lrange表示取指定范围[索引]的元素
lrange list 0 -1
4.4 对set集合的操作
# 向集合添加元素
sadd set_test 1 2 3 4
# 获取所有的元素
smembers set_test
# 获取元素的个数
scard set_test
4.5 对key的操作
# 删除一个key
del list
# 检查key是否存在
exists list
# 返回所有的key
keys *
4.6 对zset的操作
它用来保存需要排序的数据,例如排行榜
有序集合中,每个元素都有score(权重),以此来对元素进行排序
它有三个元素:key member和score
# 向集合添加元素
zadd key score1 member1 score2 member2 ...
# 获取所有的元素
zrange key 0 -1 withscores
# 获取元素的个数
zcard key
# 给member1的score1加一
zincrby key increment member1
# 求member1的排名
# 默认升序 索引
zrank key member1
# 注意:这个操作效率很高,并不是重新排序,只是把zset反转即可
zrevrank key member1
4.7 对位图BitMaps的操作
计算机最小的存储单位是位bit,BitMaps是针对位的操作的,相较于String,Hash,Set等存储方式更加节省空间
BitMaps不是一种数据结构,操作是基于String结构的,一个String最大可以存储512M,那么一个BitMaps则可以设置2^32个位
BitMaps单独提供了一套命令,所以在Redis中使用BitMaps和使用字符串的方法不太相同,可以把BitMaps想象成一个以位为单位的数组,数组的每个单元只能存储0和1,数组的下标在BitMaps中叫做偏移量
BitMaps命令说明:将每个独立用户是否访问过网站放在BitMaps中,将访问的用户记作1,没有访问过的用户记作0,用偏移量作为用户的id
4.7.1 设置值
setbit key offset value
4.7.2 获取值
getbit key offset
4.7.3 获取BitMaps指定范围值为1的个数
bitcount key start end
4.7.4 BitsMaps间的运算
bitop operation destkey key
bitop是一个复合操作,它可以做多个BitMaps的and、or、not、xor(异或) 操作并将结果保存在destkey中
4.8 对HyperLogLog结构的操作
HyperLogLog常用于大数据量的统计,比如页面访问量统计或者用户访问量统计
要统计一个页面的访问量(PV),可以直接用redis计数器或者直接存数据库都可以实现,如果要统计一个页面的用户访问量(UV),一个页用户一天内如果访问多次的话,也只能计算一次,这样,我们可以使用set集合来做,因为set集合是有去重功能的,key存储页面对应的关键字,value存储对应的userid,这种方法是可行的,但如果访问量较多,假如有几千万的访问量,这就麻烦了。为了统计访问量,要频繁创建set集合对象。
# 查看hyperloglog的用法
help @hyperloglog
# pfadd key userid userid ...
# pfcount key
# pfmerge destkey sourcekey sourcekey ...
限制:
这个HyperLogLog结构存在一定的误差,误差很小,0.81%,所以它不适合对精确度要求特别高的统计,而uv这种操作,对精确度要求没那么高,是可以适用该结构的
HyperLogLog结构不会存储数据的明细,针对uv场景,它为了节省空间资源,只会存储数据经过算法计算后的基数值,基于基数值【去重后的长度】来进行统计的
- Redis Java API操作
5.1 创建maven工程并导入依赖
<dependencies>
<!--Redis的java客户端-->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
<!--单元测试-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testng</groupId>
<artifactId>testng</artifactId>
<version>6.14.3</version>
<scope>test</scope>
</dependency>
</dependencies>
5.2 创建包结构和类
在test目录下创建 com.sl.redis.api_test包结构
创建RedisTest类
5.3 连接以及关闭redis客户端
public class RedisTest {
private JedisPool jedisPool;
@BeforeTest
public void beforeTest() {
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxIdle(10); // 指定最大空闲连接数 10个
config.setMinIdle(5); // 指定最小空闲连接数 5个
config.setMaxWaitMillis(3000); // 最大等待时间为3000ms
config.setMaxTotal(50); // 最大连接数50
jedisPool = new JedisPool(config, "hadoop104",6379);
}
@Test
public void keysTest() {
// 从redis连接池获取redis连接
Jedis jedis = jedisPool.getResource();
// 调用keys方法获取所有的key
Set<String> keySet = jedis.keys("*");
for (String key : keySet) {
System.out.println(key);
}
}
@AfterTest
public void afterTest() {
jedisPool.close(); // 关闭连接池
}
}
- Redis的持久化
由于redis是一个内存数据库,所有的数据都是保存在内存中的,内存中的数据极易丢失,所以redis的数据持久化就显得尤为重要,在redis当中,提供了两种数据持久化的方式,分别为RDB以及AOF,且redis默认开启的数据持久化方式为RDB
6.1 RDB持久化方案
redis会定期保存数据快照至一个rdb文件中,并在启动时自动加载rdb文件,恢复之前保存的数据。可以在配置文件中配置redis进行快照保存的时机:
# 表示在seconds秒内如果发生了changes次数据修改,则进行一次rdb快照保存
save [seconds] [changes]
rdb是一种全量备份,是将整个redis中的所有的数据,保存到一个rdb文件中
6.2 AOF持久化方案
redis会把每一个写请求都记录在一个日志文件里,在redis重启时,会把aof文件中记录的所有写操作顺序执行一遍,确保数据恢复到最新。
aof默认是关闭的,可以进行如下配置
# 开启aof
appendonly yes
# 配置aof
appendfsync everysec 由后台线程每秒fsync一次
appendfsync always 每写入一条日志就进行一次fsync操作,数据安全性最高,但速度最慢
AOF rewrite
只保留能够把数据恢复到最新状态的最小写操作集
6.3 RDB or AOF
RDB效率要高,因为它是直接将Redis的最新数据完整的保存下来,但如果Redis中存储的数据量比较大时,也会有一定的性能消耗,因为它需要创建额外的进程来进行数据的持久化,所以要制定合理的策略
AOF效率相对RDN要低一些,因为它会将历史的写操作都执行一遍来进行恢复,同时在执行写操作的时候也会将每一个指令存储下来,当我们对数据的安全性要求较高的时候,可以考虑AOF
- Redis的高级操作
7.1 Redis事务
7.1.1 Redis事务简介
Redis事务就是一次性、顺序性、排他性的执行一个队列中的一系列命令
没有隔离级别的概念,不保证原子性!!!
一个事务从开始到执行会经历以下三个阶段:
第一阶段:开始事务
第二阶段:命令入队
第三阶段:执行事务
注意:
只有当语法有错误的时候,事务才会回滚,但是运行的错误,事务不会回滚
为什么redis不支持事务回滚? 为了保证性能!!!
7.1.2 Redis事务演示
set k1 v1
set k2 v2
multi # 开启事务
set k1 11
set k2 22
exec # 依次执行命令
get k1
get k2
7.2 Redis过期策略
Redis的过期策略是指当Redis中缓存的key过期了,Redis如何处理 【针对设置了expire过期时间的key】
过期策略有以下三种:
定时过期:每个设置过期时间的key都需要创建一个定时器,到过期时间就会被立即清除
懒惰过期:只有当访问一个key时,才会判断该key是否过期,过期则删除
定期过期:每隔一定的时间,会扫描一定数量的数据库的expires字典中一定数量的key,并清除其中已过期的key
7.3 内存淘汰策略
指在Redis的用于缓存的内存不足时,怎么处理需要新写入且需要申请额外空间的数据
实际项目中设置内存淘汰策略:maxmemory-policy allkeys-lru,移除最近最少使用的key
默认:maxmemory-policy noeviction
- Redis的主从复制架构
8.1 简介
主从复制,是指将一台Redis服务器的数据,复制到其他的Redis服务器。前者称为主节点(master),后者称为从节点(slave),数据的复制是单向的,只能由主节点到从节点。
默认情况下,每台Redis服务器都是主节点;且一个主节点可以有多个从节点或者没有,但一个从节点只能由一个主机点。
8.2 主从复制原理
当从数据库启动后,会向主数据库发送SYNC命令
主数据库收到命令后,开始在后台保存快照(RDB持久化),并将保存快照期间接收到的命令缓存下来
快照完成后,Redis(master)将快照文件和所有缓存的命令发送给从数据库
Redis(slave)接收到RDB和缓存命令时,会开始载入快照文件并执行接收到的缓存的命令
注意:后续,每当主数据库接收到写命令时,就会将命令同步给从数据库。所以3和4只会在初始化的时候执行。
8.3 主从复制的应用场景
读写分离:写操作由master节点操作,读操作由slave节点操作
比较适合用来处理读多写少的场景。而当单个主数据库不能满足需求时,就需要使用Redis 3.0后推出的集群功能
从数据库持久化
Redis中相对耗时的就是持久化,为了提高性能,可以通过主从复制创建一个或多个从数据库,并在从数据库中启动持久化,同时在主数据库中禁用持久化。
当从数据库崩溃重启后主数据库会自动将数据同步过来,无需担心数据丢失
而当主数据库崩溃时,后续我们可以通过哨兵来解决
- Redis中的Sentinel架构
9.1 Sentinel介绍
哨兵是Redis的高可用解决方案:由一个或多个Sentinel实例组成的Sentinel系统可以监视任意多个主服务器,以及这些主服务器属下的所有从服务器,并在被监视的主服务器进入下线状态时,自动将下线主服务器属下的某个从服务器升级为新的主服务器
9.2 配置哨兵
# 每台服务器都需要对哨兵进行配置
vi sentinel
# 修改第15行
bind 主机名
# 在下方添加配置,让sentinel服务后台运行
daemonize yes
# 修改第71行
sentinel monitor mymaster hadoop104 6379 2
参数说明:
sentinel monitor 代表监控
mymaster 代表服务器的名称,可以自定义
hadoop104 代表监控的主服务器,6379代表端口
2代表只有两个或两个以上的哨兵认为主服务不可用的时候才会进行failover操作
9.3 sentinel模式代码开发连接
public class RedisSentinelTest {
private JedisSentinelPool jedisSentinelPool;
@BeforeTest
public void beforeTest() {
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxIdle(10); // 指定最大空闲连接数 10个
config.setMinIdle(5); // 指定最小空闲连接数 5个
config.setMaxWaitMillis(3000); // 最大等待时间为3000ms
config.setMaxTotal(50); // 最大连接数50
HashSet<String> sentinelSet = new HashSet<String>();
sentinelSet.add("192.168.15.104:26379");
sentinelSet.add("192.168.15.105:26379");
sentinelSet.add("192.168.15.106:26379");
jedisSentinelPool = new JedisSentinelPool("mymaster", sentinelSet, config);
}
@Test
public void test() {
Jedis jedis = jedisSentinelPool.getResource();
Set<String> keySet = jedis.keys("*");
for (String key : keySet) {
System.out.println(key);
}
}
@AfterTest
public void afterTest() {
jedisSentinelPool.close();
}
}
- Redis集群
10.1 简介
Redis最开始使用主从模式做集群,若master宕机需要手动配置slave转为master;后来为了高可用提出哨兵模式,该模式下有一个哨兵监视master和slave,若master宕机可自动将slave转为master,但还是有一个问题,就是不能动态扩充,所以在redis 3.x提出cluster集群模式
为什么要实现Redis Cluster?
主从复制不能实现高可用
QPS(每秒查询率)可能无法满足业务需求
数据量的考虑
技术组件不是内存越多越好,因为内存配置的越大,例如JVM的Heap内存配置的很大后,就会导致内存碎片整理很耗时,垃圾回收会发生卡顿,导致集群的效率下降
网络流量需求,可考虑使用分布式来进行分流
10.2 分布式存储的重点–分区
mysql–根据顺序分区的方式,例如:根据主键来进行分区(分库分表),一般是在java web中会遇到
按照哈希取余的方式来进行分区(类似于MapReduce的默认分区策略)
当分区的数量发生变化的时候,会导致key产生较大影响,原先分布在第一个节点上的数据,分区数量调整后,指定到了其他的分区
按照一致性哈希的方式来进行分区
是一个环状的hash空间,它的分区算法是和哈希取余算法不一样的
算法过程 :
首先将每一个分区的标号(0,1,2)进行算法计算,然后将计算出来的值,放入到环状的hash空间
再将key同样进行算法计算,然后将计算出来的值,同样也放入到环状的hash空间
最后,找到key在hash空间中距离自己位置最近的分区,放入到该分区中
这样,当分区的数量发生变化的时候,影响不会太大
Redis集群是使用槽的方式来进行分区的
现有一个槽的空间(0-16353),需要将这些空间分布到不同的节点中
node1: 0-3xxx
node2: 3xxx-6xxx
…
有一个key,首先进行CRC16算法&16353 = 值,Redis会判断这个值应该在哪个槽中
10.3 集群搭建
# 下载redis-5.0.8安装包
cd /opt/software
wget http://download.redis.io/releases/redis-5.0.8.tar.gz
# 解压
tar -zxvf redis-5.0.8.tar.gz -C /opt/module/
# 编译
cd redis-5.0.8/
make
# 安装至指定目录
make PREFIX=/opt/module/redis-5.0.8-bin install
# 创建安装目录软连接
cd /opt/module
ln -s redis-5.0.8-bin redis
# 配置环境变量
export REDIS_HOME=/opt/module/redis
export PATH=:$PATH:$REDIS_HOME/bin
# 拷贝配置文件
cd /opt/module/redis-5.0.8
cp redis.conf /opt/module/redis
# 修改配置文件 每台机器启动2个redis服务,一个是主节点服务:7001,一个从节点服务:7002
cd redis
mkdir 7001 7002
cp redis.conf 7001/redis_7001.conf
vi 7001/redis_7001.conf
# 步骤较多,自行百度解决......
# 启动集群,需要启动6个服务就可以了
初始化集群(只需要一次):
/opt/module/redis/bin/redis-cli --cluster create 192.168.15.104:7001 192.168.15.104:7002 192.168.15.105:7001 192.168.15.105:7002 192.168.15.106:7001 192.168.15.106:7002 --cluster-replicas 1
--replicas 1 1其实代表的是一个比例,就是主节点数/从节点数的比例
测试集群:(在任意一台机器)
redis-cli -c -p 7001
注意:
配置完后,需要启动所有的端口Redis进程,一共有6个
启动好6个节点(进程)后,需要进行集群初始化
集群初始化会自动分配master和slave,我们可以指定一个master对应几个slave
redis还会自动分配槽
连接redis集群
redis-cli -c -p 端口号
但我们访问某个key的 时候,如果这个key不在本机,redis集群会自动跳转到另外的机器
补充:
问题解决办法
看log
StackoverFlow网站(上面有各种异常解决办法)
看源码
学习技术的三点
轻量级框架、技术组件(轻量级是要花少一点时间去学习各种API,因为此时没有应用场景)
重量级项目(将技术组件结合起来,切记:和业务场景结合在一起)
大师级面试题(提升自己的沟通表达能力、组织话术、熟记各种原理)
10.4 JavaAPI操作redis集群
JedisPool–操作单机版本的redis
JedisSentinelPool–操作哨兵系统的主从结构
JedisCluster–操作redis集群
public class RedisClusterTest {
private JedisCluster jedisCluster;
@BeforeTest
public void beforeTest() {
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxIdle(10); // 指定最大空闲连接数 10个
config.setMinIdle(5); // 指定最小空闲连接数 5个
config.setMaxWaitMillis(3000); // 最大等待时间为3000ms
config.setMaxTotal(50); // 最大连接数50
HashSet<HostAndPort> hostAndPort = new HashSet<HostAndPort>();
hostAndPort.add(new HostAndPort("hadoop104", 7001));
hostAndPort.add(new HostAndPort("hadoop104", 7002));
hostAndPort.add(new HostAndPort("hadoop105", 7001));
hostAndPort.add(new HostAndPort("hadoop105", 7002));
hostAndPort.add(new HostAndPort("hadoop106", 7001));
hostAndPort.add(new HostAndPort("hadoop106", 7002));
jedisCluster = new JedisCluster(hostAndPort, config);
}
@Test
public void test() {
jedisCluster.set("k2","v2");
System.out.println(jedisCluster.get("k2"));
}
@AfterTest
public void afterTest() throws IOException {
jedisCluster.close();
}
}
注意事项:
在构建JedisCluster的时候,需要将集群中的所有主从节点添加到set中
如果使用JedisCluster操作Redis的时候,不再需要获取Redis连接,直接去操作Redis即可,因为JedisCluster已经封装好了对应的操作
10.5 Redis集群面试题
问题1:Redis的多数据库机制,了解多少?
正常版:Redis支持多个数据库,并且每个数据库的数据是隔离的不能共享,单机下的redis可以支持16个数据库(默认下标为0~15)。
高调版:在redis cluster架构下,只有一个数据库空间,即0下标的数据库,因此,我们没有使用Redis的多数据功能。
问题2:懂Redis的批量操作吗?
正常版:懂一点。比如mset、mget操作等。
高调版:在生产上采用的是Redis Cluster结构,不同的key会划分到不同的slot中,因此直接使用mset或者mget等操作是行不通的。
问题3:Redis集群机制中,你觉得有什么不足的地方吗?
正常版:不知道
高调版:假设有一个key,对应的value是hash类型的,如果hash对象非常大,是不支持映射到不同节点!只能映射到集群中的一个节点上!还有就是做批量操作比较麻烦!
问题4:在Redis集群模式下,如何进行批量操作?
正常版:不知道
高调版:如果执行的key数量比较少,就不用mget了,就用串行get操作。如果真的需要执行的key很多,就需要使用Hashtag保证该这些key映射到同一台redis节点上
问题5:懂Redis事务吗?
正常版:redis事务是一系列redis命令的集合
高调版:在生产中采用的是redis cluster架构,不同的key是有可能分配在不同的redis节点上,在这种情况下redis的事务机制是不生效的,其次,redis事务不支持回滚操作
- Redis高频面试题
在应用程序和mysql数据库之间建立一个中间层:redis缓存,通过redis缓存可以有效减少查询数据库的时间消耗,但是引入redis又有可能出现缓存穿透,缓存击穿,缓存雪崩等问题
11.1 缓存穿透
key对应的数据在数据源并不存在,每次针对此key的请求从缓存获取不到,请求都会到数据源,从而可能压垮数据源。
一言以蔽之:查询key,缓存和数据源都没有,频繁查询数据源
解决的方案:
方案1:当查询不存在时,也将结果保存在缓存中
方案2:提前过滤掉不合法的请求,可以使用redis中的布隆过滤器【可以快速的过滤掉缓存中不存在的key】
11.2 缓存击穿
key对应的数据库存在,但在redis中过期
一言以蔽之:查询key,缓存过期,大量并发,频繁查询数据源
解决的方案:
使用互斥锁
setnx("互斥锁", 1) : 表示当key不存在的时候,才会设置一个key
11.3 缓存雪崩
当缓存服务器重启或者大量缓存集中在某一个时间失效
一言以蔽之:缓存不可用,频繁查询数据源
解决的方案:
加锁或者消息队列
将缓存失效时间分散开
11.4 Redis的命名规范
使用统一的命名规范
一般使用业务名(或数据库名)为前缀,用冒号分割,例如 业务名:表名:id
控制key名称的长度,不能使用过长的key
名称中不要包含特殊字符