如何学好Redis
建立知识框架,打造系统观。
Redis启动
Windows下:
在安装目录下执行:
.\redis-server.exe .\redis.windows.conf
客户端连接Redis
Windows下:
.\redis-cli.exe -h 127.0.0.1 -p 6379
Linux下安装Redis
参考官方说明即可
一.命令
String
set name xiaoming
get name
expire name 5 #5秒过期
setex name 5 xiaohong
setnx name #不存在时设置
set age 10
incr age #对于数字可以增加1
incrby age 10
#批量操作
mset name xiaoming age 10
mget name age
List
左右添加、左右删除
lpush books java python c++
lpop books
rpush c
rpop books
lindex books 1 #获取下标为1的元素(下标从0开始)
lindex books -1 #获取倒数第一个元素
注意下标从右往左计数。另外没有rindex命令。
lrange books 0 -1 #获取0到倒数第一个元素,即全部元素
ltrim books 1 -1 #保留下标1到最后一个元素,删除其它元素。即删除第一个元素
ltrim books -1 -1 #保留最后一个元素
ltrim books 2 3 #保留下标2到下标3的元素
底层结构:quicklist,用双向指针连接的zipList
Hash
hset nums one 1 #添加值
hget nums one #获取值
hgetall nums #获取所有键值对
hmset nums two 2 three 3 #批量添加
hmget nums one two #批量获取
hincrby nums one 1 #对value为int的键值对进行加法操作
底层结构:数组+链表
SET
sadd books python #添加元素
smembers books #列出所有元素
sismember books java #判断元素是否存在
scard books #列出set中元素个数
spop books #弹出一个元素。弹出规则?
底层结构:基于Hash,value设置为null
ZSET
是Redis独有的数据结构。一方面它是一个set,保证了内部value的唯一性。另一方面它可以给每一个value赋一个score,代表这个value的排序权重。按score从小到大排序。
#添加元素时要带着一个权重值
zadd books 9.0 "think in java"
zadd books 8.9 "java concurrency"
zadd books 8.6 "java cookbook"
zrange books 0 -1 #按score从小到大排序列出
zrevrange books 0 -1 #按score逆序排列
zcard books #获取元素个数
zscore books "think in java" #获取元素的score
zrank books "think in java" #获取位次,从0开始,低到高排序
zrangebyscore books 0 8.91 #获取分值区间内的元素
zrangebyscore books -inf 8.91 withscore #-inf表示负无穷,withscore表示列出分数
zrem books "java concurrency" #删除value
底层结构:跳表
通用规则
对于list、hash、set、zset四种容器类型,有两个通用规则:
(1)create if not exists
(2)drop if no elements
过期时间
所有类型的元素都可以使用expire设置过期时间,只能对数据整体设置,例如不能对hash中的某个key单独设置过期时间。
设置过期时间
expire key
查看某个key的过期时间
ttl key
删除命令
所有类型都可以用del进行删除
二.使用Jedis操作Redis
引入依赖:
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.5.1</version>
</dependency>
连接Redis
/**
* 连接Redis
* */
public static Jedis connectJedis() {
Jedis jedis = new Jedis("localhost");
return jedis;
}
执行命令
通过Jedis对象执行命令
//Jedis中的方法和Redis命令基本是一一对应的
Jedis jedis = ConnectRedis.connectJedis();
jedis.set("name","xiaoming");
jedis.get("name");
jedis.hmset(key,hashmap);
思考:存储结构体信息改用hash还是string?
根据使用场景决定,原则是尽可能地减少查询次数。
每次使用是只使用其中的某个字段呢(hash)?还是需要全部字段呢(String)?
Hash不需要进行序列化、反序列化,是否可以承受这部分的性能损失?
三.分布式锁
版本1:
setnx lock true
//执行业务逻辑
del lock
使用setnx抢占锁,只有第一个执行成功的能抢到锁。
最后使用del删除锁。
问题:如果执行业务逻辑时出现异常,删除操作没有正常执行,则其它进程无法抢占到锁。
版本2:
setnx lock true
expire lock 5
//执行业务逻辑
del lock
给锁加上一个过期时间,即使任务没有执行完也会在过期后释放锁。
问题:抢占锁和设置过期时间是两条语句,如果执行完第一条语句后,程序崩溃无法加入过期时间,就会有版本1一样的问题。
版本3:
使用一条指令,同时执行setnx和expire
set lock true ex 5 nx
//执行业务逻辑
del lock
将抢占锁和设置过期时间在一条语句中执行,避免了上述问题。锁一定会被释放。
问题:如果过期时间到了,但是业务代码没有执行完成,锁会释放掉,这时其它进程有可能抢占到锁导致并发执行问题。
Redission怎么实现分布式锁的?是否解决了上述问题?redLock是什么?
Redission在获取到锁后,会启动一个看门狗线程,不断判断任务是否还持有锁,持有的话进行续期。默认是每次续期30秒钟。避免出现任务还每执行完,但锁已经到期的情况。
那会不会出现机器宕机,没有释放锁的情况呢?不会因为机器宕机了,续期的线程也不会执行了,到时间自然就释放锁了。正常执行完时,unlock时也会停止看门狗线程。
问题:锁被其它进程释放掉。
情景:A任务执行超时,锁过期释放掉。B又申请了锁。A这时执行完成,把B的锁释放掉。
给锁加一个每个客户端唯一的值,只释放自己加的锁。需要进行判断后进行判断,为了保证原子性需要使用lua脚本。
仍然存在的问题:设置锁到master,这时进行主从复制,但master宕机了,从机变为主机但没有设置的锁信息。之后来加锁的进程可能成功,造成锁被获得了两遍。
解决:使用多台master,不使用master-salve结构。RedLock算法,向所有机器发送set lock true ex 5 nx命令只有半数以上机器设置成功时,才算加锁成功。执行完成后使用del删除。
四.消息队列
可以使用list做简单的消息队列。
使用rpush进行生产。
使用lbpop进行消费
#生产者
rpush msg-queue msg1
#消费者 0为等待时间,0表示一直等待。也可设置为大于0的值,表示等待若干秒
blpop msg-queue 0
五.Redis命令的原子性
在分布式锁应用中,申请锁、设置过期时间,需要保证原子性;判断再删除锁也需要原子性。
保证原子性有两种方法:一种是使用多合一的命令,如set的nx、ex参数。setnx命令等。或者使用lua脚本同时执行多个命令。
六.位图操作
#设置第一位位1
bitset xiaoming-login 1 1
bitset xiaoming-login 2 0
bitset xiaoming-login 3 1
bitset xiaoming-login 4 1
bitset xiaoming-login 5 0
bitset xiaoming-login 6 1
bitset xiaoming-login 7 1
#获取第1位的value(0 or 1)
bitget xiaoming-login 1
bitget xiaoming-login 5
#统计有多少个1
bitcount xiaoming-login
#可按字节指定范围统计1的个数
bitcount xiaoming-login 0 0 #第1个字节中1的个数
bitcount xiaoming-login 0 1 #前两个字节中1的个数
#查找第一个1、0的位置
bitpos bit 0 #查找第一个0的位置
bitpos bit 1 #查找第一个1的位置
bitpos bit 1 0 1 #查找前两个字节中第一个1的位置
bitpos bit 1 1 #查找第一到最后一个字节中第一个1的位置
注意bitcount、bitpos中start、end的含义是以字节为单位的。字节从0开始计数。
位操作的典型应用:统计用户打卡天数。
七.HyperLogLog
以误差0.81%统计不重复的数据个数。
pfadd log user1 #添加元素
pfcount log #返回元素个数
pfmerge log log2 #将log2合并到log
如果不需要要精确地统计个数,可以使用HyperLogLog,好处是相比于set占用空间少。只需要占用12k的空间,就可以存储大量的数据。(数据较少时不需要12K这么多)。
缺点:只能用来计数,不能判断某个元素是否在集合中。
典型应用:统计UV
八.布隆过滤器
上文说到HyperLogLog不能判断某个元素是否在集合中存在。而布隆过滤器就是为了解决这个问题的。
布隆过滤器的特点:如果判断某个元素不存在,则该元素一定不存在;如果判断某个元素存在,则有一定的误判概率,即该元素有可能不存在。
命令1:创建布隆过滤器
bf.reserve key error_rate initial_size
通过error_rate可设置错误率。错误率越低,需要的存储空间就越大。initial_size表示预估添加的元素个数,当添加的元素个数超过它时误判率会提高。
如果不显示地创建布隆过滤器,直接使用bf.add进行元素添加,则创建一个默认的过滤器。批量添加bf.madd
判断元素是否存在,bf.exists。批量查询bf.mexists
应用:爬虫,判断url是否已经处理过。
九.简单限流器
需求:指定时间段Period内,用户的某个操作不超过max_count次。
使用zset。为每个用户的每个操作建立一个zset。value为时间戳,score为时间戳。
新的用户操作来到,先加到zset中,zadd key score value
。
再截取zset,删除now-period外的数据,使用命令:remrangebyscore key 0 now-period
。
然后使用scard key
判断当前set中的个数,如果不超过max_count则可以进行操作,否则拒绝操作。
十.持久化
RDB
分为RDB和AOF。
RDB全量持久化,将内存中的数据存储到经过压缩的二进制文件RDB文件中。有两个命令save和bgsave。save在当前进程执行,执行期间Redis不再相应其它请求。bgsave通过fork一个子进程,进行持久化到RDB日志的工作。持久化的同时主进程继续提供服务,如果这时有写操作,则通过COW(Copy On Write 写时复制)功能对要修改的数据复制出一份进行修改,RDB持久化进程继续在原数据上进行读取操作。这也就达到了“快照”的能力。
这里也要注意如果写操作特别多,RDB时内存增长就会较多,需要注意内存使用风险。
而从rdb恢复数据的工作不需要手动执行,在redis启动时会自动执行。但如果开启了aof日志,会优先使用aof。aof更新频率高,更准确。
除了手动发送save、bgsave命令,可以通过配置,让redis自动执行。
默认情况下配置如下,标识900秒内至少有1个修改时或300秒内时至少有10次修改时或60秒内至少有1万次修改时执行bgsave。
save 900 1
save 300 10
save 60 10000
这个配置在配置文件中,可根据需要进行修改。
AOF
AOF通过记录执行的命令来持久化Redis。每执行一个命令就向aof文件中做一次记录。
刷新问题
问题是文件的写入操作会被OS先进行缓存,不会立马写入磁盘。这样就可能导致宕机时缓存中的数据无法被记录。
可以主动刷新缓冲区。Redis提供了一个配置来决定刷新缓冲区的频率。有三种频率:永不主动刷新、每个命令后主动刷新、每秒主动刷新一次。
再这三种方式中,刷新太快性能不好,永不刷新有数据损失风险,每秒刷新比较合适。即不影响性能,宕机时也只会损失1秒的数据。
数据还原
Redis重新执行一遍aof文件中的命令就恢复了数据。
AOF重写
对同一个key的多次操作都记录下来没有必要,只需要记录最终状态就好了。
使用bgrewriteaof可以重写AOF。重写过程:fork新进程,遍历所有key,将一个key记录为一条写入命令。重写过程中,主进程照常提供服务。只不过需要在进行AOF时,多写一份AOF到AOF重写缓存区。重写结束后,主进程将AOF重写缓冲区和重写后的AOF合并,得到最终的AOF文件。并替换原来的AOF文件。这样就保证了主进程尽可能的减少阻塞时间,并且完成了AOF文件的重写。AOF重写缓冲区是个关键。
AOF重写缓冲区是什么形式?一个文件吗?
AOF重写的触发
- auto-aof-rewrite-min-size: 表示运行AOF重写时文件的最小大小,默认为64MB
- auto-aof-rewrite-percentage: 这个值的计算方法是:当前AOF文件大小和上一次重写后AOF文件大小的差值,再除以上一次重写后AOF文件大小。也就是当前AOF文件比上一次重写后AOF文件的增量大小,和上一次重写后AOF文件大小的比值。
RDB+AOF
AOF是增量日志,磁盘刷新频率一般设置为每秒一次(可以平衡效率和数据丢失概率)。每次只需要记录一条命令,执行较快。但是使用AOF恢复数据时较慢(将所有命令重新执行一遍)。
RDB是快照,由主进程fork出子进程(fork过程阻塞主进程)将内存中的数据以二进制的形式存储到文件中。是全量日志。快照不能太频繁,因为会阻塞主进程。使用RDB恢复数据较快。但是RDB执行时间较长会丢失较多的数据。
如何同时利用RDB数据恢复快和AOF数据丢失少的优点呢?Redis4.0提出了一种RDB和AOF结合使用的方式。设定RDB以一定频率执行,AOF只需要记录两次RDB之间的日志。这样通过最后一次RDB日志,和这次RDB之后的AOF日志就可以恢复完整的数据。AOF也不会变的很大。
设置参数为:aof-use-rdb-preamble yes
十一.Redis为什么这么快?
(1)数据存储在内存中,内存中的操作本身就比较快。Redis在收到一个命令后可以以微妙级的速度查找到对应的value,并快速完成操作。
(2)Redis中的数据结构设计的非常高效。所有的Redis操作最终都落脚到对数据的查找、修改、删除等操作。高效合理的数据结构让这个过程非常快。
(3)Redis采用了单线程对外提供服务。通过使用IO多路复用可以同时处理大量请求。同时,由于是单线程不必进行线程切换进一步加快了Redis的速度。另外单线程下不需要考虑并发访问问题,不需要加锁等操作,这也提高了Redis的速度。
十二.Redis的键和值是如何组织的?
哈希表。
哈希表底层使用数组+链表。
也就是有一个全局的哈希表用于存储key和value的指针。
哈希冲突:使用链表。(这时可能会导致查询变慢)。
所以在元素个数较多时需要rehash。
rehash不是一次性完成的,否则会造成较长时间的服务不可用(服务线程用于rehash了)。
而是同时维护两个哈希表,在每次处理一个请求时,rehash数组的一个结点(包括整条链表的内容)。
rehash完成后再用新的哈希表替换原来的哈希表。原哈希表的空间可以回收。
rehash时新申请的数组长度是原数组的两倍。
十三.Redis面试问题
你的项目架构中,为什么会用redis?
用redis做了什么事?解决了什么问题?
用Redis的过程中,有没有发现一些问题?如何解决的?
如何用Redis解决海量数据的日活、月活问题?
如何用Redis解决高并发下黑名单、白名单问题?
在大数据环境中,Redis能解决什么问题?
在高并发下,基于并发安全,如何实现高质量的分布式锁?
Redis的String、Zset等数据结构的底层实现原理是什么?
在使用Redis的过程中,有没有发现它的短板问题?如何进行优化?
如何进行Redis的性能优化?
Redis的数据一致性如何处理?
如何进行Redis集群方案的选择?
阐述一下Rdis6.*的新特性。
十四.Redis客户端——Redission
Redission将Redis中的String类型映射为通用对象桶bucket
十五.缓存雪崩、缓存击穿、缓存穿透
十六.三大主线、两大纬度
三大主线:
高性能(线程模型、数据结构、持久化、网络框架)
高可靠(主从、哨兵)
高可扩展(分片集群、负载均衡)
两大纬度:
应用纬度:缓存、集群、数据结构应用
系统纬度:处理层、内存层、存储层、网络层
十七.Redis值类型的底层实现结构
数据类型 | 底层实现 |
---|---|
String | 简单动态字符串 |
List | 压缩列表、双向链表 |
Hash | 压缩列表、哈希表 |
Set | 哈希表、整形数组 |
Sorted Set | 跳表、压缩列表 |
可以看到在List、Hash和Sorted Set中都使用到了压缩列表。可见这是Redis中一个很重要的数据结构,在这篇文章中对这种结构做了介绍。
十八.Java键值对存储结构
Redis是一个key-value键值对存储系统,对于key和value的存储采用哈希表。这里的哈希表的实现和JDK中HashMap很像,使用了数组+链表的形式。其中链表是为了解决哈希冲突的。
在哈希冲突比较严重时Redis会进行rehash操作,比较独特的是Redis会采取渐进式rehash的方式,在多次的请求处理过程中,逐渐将元素拷贝到新的哈希表中。在这个过程中同时维护两个哈希表。rehash结束后,用新的哈希表替代老的哈希表。