一、基础
1、redis简介
Redis是一个使用 C 语言编写的、开源的高性能非关系型(NoSQL)数据库。
Redis 为键值对数据库,键的类型只能为字符串,值支持五种数据类型:字符串、列表(link lists)、集合(sets)、散列表(hash tables)、有序集合(sorted sets)。
与传统数据库不同的是 Redis 的数据是存在内存中的,所以读写速度非常快,因此 redis 被广泛应用于缓存方向,每秒可以处理超过 10万次读写操作,是已知性能最快的Key-Value DB(键值对数据库)。
Redis 也经常用来做分布式锁。除此之外,Redis 支持事务 、持久化、LUA脚本、LRU驱动事件、多种集群方案。
1.1、Hash tables类型的项目使用实例
场景:以用户id为键,将用户加购物车商品(信息包含pid、商品数量)放入redis缓存;查看购物车时全部取出展示。
1)存:redisTemplate.opsForHash().put(userId, productId, productNumber);
2)只取全部的商品idList<String> pIdList = redisTemplate.opsForHash().values("userId");
3)取全部商品信息Map<String, int> map = redisTemplate.opsForHash().entries("userId");
存的时候只能一条一条数据存、相互之间没有联系,但是取的时候可以像从一张表中取出的一样,这就是非关系型数据库“非关系”的核心奥义。
2、Redis有哪些优缺点?
1)优点:
读写性能优异, Redis能读的速度是110000次/s,写的速度是81000次/s。
支持数据持久化,支持AOF和RDB两种持久化方式。
支持事务,Redis的所有操作都是原子性的,同时Redis还支持对几个操作合并后的原子性执行。
数据结构丰富,除了支持string类型的value外还支持hash、set、zset、list等数据结构。
支持主从复制,主机会自动将数据同步到从机,可以进行读写分离。
2)缺点
我能想到的缺点只有:受物理内存的限制,redis不能用作海量数据的高性能读写,只适应于,在数据量较小场景下的操作和运算,有些局限。
3、redis的数据类型
redis的value常用的数据类型有以下几种:
1)string:能表达字符串,整数,浮点数
2)list:可以存储有序,可重复的元素
List在实际开发中常常不会用Redis List结构存储,而是存为Redis String;
原因:Redis List 适合频繁增删元素的场景(如消息队列),而 List 作为整体存取时,序列化为字符串更高效。
比如,存设备上报数据List,常常需要再每次存取的时候,把所有数据都取出来处理一下再放进去、而不能每次存取一个,这种情况就更适合用Redis String,而不是Redis List。
实现:
存的时候
# 先用fastjson键List<MyObj>序列化未json字符串
String json = JSON.toJSONString(userList);
# 再用set(key, value)存入redis
jedisCluster.set(key, json);
取的时候
# 先用get方法取出json串
String json = jedisCluster.get(key);
if (json == null) return new ArrayList<>(); // 避免 NullPointerException
# 再用fastjson反序列化为 List<MyObj>
List<MyObj> list = JSON.parseArray(json, MyObj.class);
2.2)注意事项:保证序列化与反序列化的一致性
禁止进行二次序列化
开发中常常会对redis的操作方法进行封装,有一次我用封装的put()方法存list,先用fastjson序列化为json串再用put存入,然后再取的时候用fastjson反序列化报错了,说取出来的json串格式不对、转换失败;
后来才发现,封装的put()方法,内部就已经对放入的value进行了一次序列化了,而我在调用put前进行了一次序列化,相当于进行了两次序列化,第二次序列化相当于对字符串进行序列化,导致最终的json串无法再反序列化为最初的list。
3)set:无序去重的集合。set提供了交集,并集等方法,对于实现共同好友,共同关注等功能特别方便
4)zset:有序set。内部维护了一个score的参数来实现。适用于排行榜和带权重的消息队列等场景
5)hash:键值对集合
以jedis为例,hash的设置一般用hset()方法,hset()入参有两种
hset(key, map)
或者
hset(key, feild, value)
其中key为键,feild为字段(其实也是键),value为值,
这里我一般把key理解为分组,feild为键,value为值;比如我们有一个产品组,每个产品有多个设备,每个设备有一个对应的数据值,那么就可以hset(产品Code,设备Code,设备Data)这样把数据分门别类的放入redis。
hash取的时候可以用hget()和hgetAll()方法;
hget()方法要传两个值,key和feild,还是以产品-设备-数据为例,hget可以直接得到对应产品的指定设备的数据;
hgetAll()方法传一个key值,得到一个Map,map的key即为feild,也就是说hgetAll()会一下获取产品下所有设备的数据,通过map返回。
4、redis在实际开发中的应用场景
1)缓存
将热点数据放到内存中,设置内存的最大使用量以及淘汰策略来保证缓存的命中率。
2)分布式锁实现
在分布式场景下,无法使用单机环境下的锁来对多个节点上的进程进行同步。可以使用 Redis 自带的 SETNX 命令实现分布式锁,除此之外,还可以使用官方提供的 RedLock 分布式锁实现。
redis分布式锁实现方案:
a、用setNX命令,key为对应的资源标记,value存获取到锁的线程设置的唯一标记;
b、setNX返回1,表示获取到锁,就可以执行任务;
c、setNX返回0,表示获取锁失败;
此分布式锁方案的一些问题及解决办法:
a、死锁问题:采用setNX + expire过期时间解决死锁(同时,为了保证两个命令的原子性,可用lua脚本同时执行2个命令);
b、误删问题:value存获取到锁的线程设置的唯一标记就是为了解决误删问题,要求线程释放锁前先判断value,是当前线程加的锁,才可用释放;
c、锁过期释放、但任务未完成的问题(续期问题):开启一个守护线程,定期检查锁是否存在,存在则适当延长过期时间。
d、采用Redisson分布式锁框架,其特有的看门狗watch dog,可以解决续期问题。
3)计数器
可以对 String 进行自增自减运算,从而实现计数器功能。Redis 这种内存型数据库的读写性能非常高,很适合存储频繁读写的数据。
4)防止消息重复消费(具体看Rabbit MQ)
5、Redis 的持久化机制是什么?分别做一些说明
1)Redis持久化的原因:如果把数据放在内存中,电脑关闭或重启数据就会丢失,所以放在内存中的数据不是持久化的,而放在磁盘就算是一种持久化。
Redis 的数据存储在内存中,如果 linux 宕机或重启,又或者 Redis 崩溃或重启,所有的内存数据都会丢失,为解决这个问题,Redis 提供两种机制对数据进行持久化存储,便于发生故障后能迅速恢复数据。
2)Redis 提供两种持久化机制 RDB(默认) 和 AOF 机制,
RDB就是在指定的时间间隔内将内存中的数据集快照写入磁盘,数据恢复时将快照文件直接再读到内存。RDB 保存了在某个时间点的数据集(全部数据),存储在一个二进制文件中,默认叫 dump.rdb。RDB 技术非常适合做备份,可以保存最近一个小时,一天,一个月的全部数据;保存数据是在单独的进程中写文件,不影响 Redis 的正常使用;RDB 恢复数据时比其他 AOF 速度快。
AOF就是Redis每次接收到一条改变数据的命令时,它将把该命令写到一个 AOF 文件中(只记录写操作,读操作不记录),当 Redis 重启时,它通过执行 AOF 文件中所有的命令来恢复数据。
两种持久化机制的实现方式,都是在redis.conf 文件中做相应配置即可。
3)两种持久化方式的优缺点:
RDB恢复方便,恢复速度也快;但是会丢失一部分数据看,而且会分出一个进程,如果数据库数据量大、服务器cpu也比较弱的时候,可能每次备份数据就会比较慢,可能达到1s。
AOF是一个可以提供完整数据的保障方案;缺点是,AOF 文件会在操作过程中变得越来越大,而且其中很多记录对最终的结果是无用的。
可以同时使用两种持久化方案,redis默认会用aof以保证数据的完整性。
6、Redis中key长度对性能的影响
今天我们一起用代码来验证一下key的长度对redis读取key的性能影响。
网络环境:本地
内存:8G
redis版本:redis-5.0.7
实验代码如下,读写1000次长度为16、128、512、1024、2048、4096、9012、20000、100000长度的key获取执行的总时长:
随机生成指定长度的字符串方法如下:
运行main方法三次,数据表现基本一致,下图是运行输出的三组数据:
结论:
当key的长度不超过1024(即512个字符)的长度的时候,基本上对性能不造成影响,但是一旦超过1024长度,随着key长度的增加,耗时也会随之增加。
所以,key长度对redis读写性能的影响是当key长度超过1024字节之后,因此我们在实际开发过程中可以根据自己的key长度预估对redis是否存在性能影响。
在实际业务开发中,基本上大家的key不会超过1024字节,因此可以在命名的时候,尽量取一些能见名知义的key,不必刻意为了缩短key长度而降低key的可读性。
当有这种key就必须特别长的时候,或者不确定是否超过1024字节,我们可以对key做一次hash后取哈希值作为redis的key,这样就可以大幅提高redis的性能了。
7、Redis官网关于key的一些规则
1.非常长的key是不推荐的。一个1024 bytes是一个非常坏的注意,不仅仅是因为内存浪费,更是因为在数据集中搜索对比的时候需要耗费更多的成本。当要处理的是匹配一个非常大的值,从内存和带宽的角度来看,使用这个值的hash值是更好的办法(比如使用SHA1)。
2.特别短的key通常也是不推荐的。在写像u100flw这样的键的时候,有一个小小的要点,我们可以用user:1000:followers代替。可读性更好,对于key对象和value对象增加的空间占用与此相比来说倒是次要的。当短的key可以很明显减少空间占用的时候,你的工作就是找到正确的平衡。
3.尝试去固定一个schema。比如object-type:id是一个好主意,-和.通常用于多个字符的域,就像comment🔢reply.to,或者comment🔢reply-to。
4.最大的key允许512MB。
8、Redis的一些开发规范
key命名设计
(1)【建议】: 可读性和可管理性 以业务名(或数据库名)为前缀(防止key冲突),用冒号分隔,比如业务名:表名:id。
(2)【建议】: 简洁性 保证语义的前提下,控制key的长度,当key较多时,内存占用也不容忽视,例如:U_O_P_HS:#userId代表用户已经购买的商品Id的hash存储。
(3)【强制】:不要包含特殊字符反例:包含空格、换行、单双引号以及其他转义字符。
value设计
(1)【强制】:拒绝bigkey(防止网卡流量、慢查询)string类型控制在10KB以内,hash、list、set、zset元素个数不要超过5000。反例:一个包含200万个元素的list。非字符串的bigkey,不要使用del删除,使用hscan、sscan、zscan方式渐进式删除,同时要注意防止bigkey过期时间自动删除问题(例如一个200万的zset设置1小时过期,会触发del操作,造成阻塞,而且该操作不会不出现在慢查询中(latency可查)),查找方法和删除方法
(2)【推荐】:选择适合的数据类型。例如:实体类型(要合理控制和使用数据结构内存编码优化配置,例如ziplist,但也要注意节省内存和性能之间的平衡)正例:hmset user:1 name tom age 19 favor football
(3).【推荐】:控制key的生命周期,redis不是垃圾桶。建议使用expire设置过期时间(条件允许可以打散过期时间,防止集中过期),不过期的数据重点关注idletime。
二、集群
1、集群的概念
集群,就是通过添加服务器的数量,提供相同的服务,从而让服务器达到一个稳定、高效的状态。
redis集群就是多个redis节点(组)一起工作的模式。它没有代理节点和中心节点,各个节点平等。
2、集群的必要性
1)单个redis存在不稳定性。当redis服务宕机了,就没有可用的服务了。
2)单个redis的读写能力是有限的。
总结:redis集群是为了强化redis的读写能力,提高容错(当集群中的某一个节点故障时,redis还可以继续处理客户端的请求)
3、Redis集群(redis cluster)介绍
1)集群组成 及 故障转移方式
一、每个主Redis只有一个库db0(这点不同于单节点);
二、每个主Redis只能存0~16384个槽的一部分;
三、如果一半以上的主Redis判断某个主Redis断了(所以为了投票,主Redis只能是奇数个,至少3个),启用这个Redis的副Redis为主,若后面这个"主"Redis又重启了,它就是副了;
2)分配槽slot的概念
一个Redis集群包含16384个插槽,数据库中每个键都属于这16384槽中的一个,集群使用键的哈希值 % 16384来计算键属于哪一个槽。
每个节点负责处理一部分插槽,如一个集群有3个节点,A节点负责处理0-5500号插槽,节点B负责处理5501-11000号插槽,节点C负责处理11001-16383号插槽。
redis-cli查询键值时,如客户端对应的服务器中不存在该键,则Redis会报错。
而使用redis-cli -c -p port中的-c参数就可以实现自动重定向。
不在一个slot中的数据,不能使用mget和mset等多键操作。
3)集群模式的缺点
1. 客户端实现复杂,驱动要求实现Smart Client,缓存slots mapping信息并及时更新,提高了开发难度。
目前仅JedisCluster相对成熟,但异常处理也还不完善,比如常见的“max redirect exception”
2. 节点会因为某些原因发生阻塞(阻塞时间大于 cluster-node-timeout)被判断下线,这种failover是没有必要的
3. 数据通过异步复制,不保证数据的强一致性
4. slave充当“冷备”,不能缓解读压力
5. 批量操作限制,目前只支持具有相同slot值的key执行批量操作,对mset、mget、sunion等操作支持不友好
6. key事务操作支持有线,只支持多key在同一节点的事务操作,多key分布不同节点时无法使用事务功能
7. 不支持多数据库空间,单机redis可以支持16个db,集群模式下只能使用一个,即db 0
4、Redis主从复制介绍
1)基础概念
一、主从复制模式中包含一个主数据库实例(master)与一个或多个从数据库实例(slave)
客户端可对主数据库进行读写操作,对从数据库进行读操作,主数据库写入的数据会同步给从数据库。
二、主从库具体的同步机制
1、slave启动后,向master发送SYNC命令,master接收到SYNC命令后通过bgsave保存快照(即上文所介绍的RDB持久化),并使用缓冲区记录保存快照这段时间内执行的写命令
2、master将保存的快照文件发送给slave,并继续记录执行的写命令
3、slave接收到快照文件后,加载快照文件,载入数据
4、master快照发送完后开始向slave发送缓冲区的写命令,slave接收命令并执行,完成复制初始化
5、此后master每次执行一个写命令都会同步发送给slave,保持master与slave之间数据的一致性
5、Redis主集群模式 与 主从复制的关系
1)集群与主从复制是互相补充的技术,主从复制不是集群模式的一种;
2)集群是确立1个redis组与另一个redis组之间的运行关系;
3)主从复制是确立1个redis组中,主、副两个库之间的运行关系;