redis-笔记
相关知识
数据库
默认16个数据库,类似数组下标从0开始,初始默认使用0号库
使用命令select <dbid>来切换数据库
统一密码管理,所有库同样密码
dbsize查看当前数据库的key的数量
flushdb清空当前库
flushall清空全部库
多路复用io
多路复用是指使用一个线程来检查多个文件描述符(socket)的就绪状态,比如调用select和poll函数,传入多个文件描述符,如果有一个文件描述符就绪,则返回,否则阻塞直到超时。得到就绪状态后进行真正的操作可以在同一个线程里执行,也可以启动线程执行(比如使用线程池)
和memcache的区别
- 支持多种数据类型
- 支持持久化
- 单线程+多路复用io(memcache是多线程+锁)
基本数据类型
键key
命令 | 作用 |
---|---|
keys * | 查看当前库所有key |
exists | 判断某个key是否存在 |
type | 查看key是什么类型 |
del | 删除指定key |
unlink | 非阻塞删除,仅将key从keyspace元数据中删除,真正的删除会在后续异步操作 |
expire | 为key 设定过期时间,单位秒 |
ttl | 查看还有多数秒过期,-1表示永不过期,-2表示已过期 |
字符串string
string是redis最基本的类型,你可以理解成与memcached一模一样的类型,一个key对应一个value
string类型是二进制安全的。意味着redis的string可以包含任何数据。比如图片或者序列化的对象
string类型是redis最基本的数据类型,一个redis字符串value最大可以说512m
常用命令
命令 | 作用 |
---|---|
set | 添加键值对 |
setnx | 当key不存在时添加 |
get | 查询对应键值 |
append | 追加到原值末尾 |
strlen | 值的长度 |
incr | 自增1 |
decr | 自减1 |
incrby | 自增指定值 |
decrby | 自减指定值 |
mset | 同时添加多个值 |
mget | 同时获取多个值 |
msetnx | 当所有key都不存在时,添加多个值 |
getrange | 获得范围的值 |
setrange | 覆写范围的值 |
setex | 设置值的同时,设置过期时间,单位秒 |
getset | 设置新值的同时获得旧值 |
原子操作
所谓原子操作是指不会被线程调度机制打断的操作
这种操作一旦开始,就一直运行到结束,中间不会有任何contextswitch(切换到另一个线程)
- 在单线程中,能够在单条指令中完成的操作都可以认为是“原子操作”,因为中断只能发生于指令之间
- 在多线程中,不能被其它进行(线程)打断的操作就叫原子操作
redis单命令的原子性得益于redis的单线程
数据结构
string的数据结构为简单动态字符串(simple dynarnic string,缩写sds)。是可以修改的字符串,内部结构实现上类似于java的arraylist,采用预分配冗余空间的方式来减少内存的频繁分配
redis内部为当前字符串实际分配的空间一般要高于实际长度。当字符串长度小于1m时,扩容是加倍现有空间,如果超过2m,扩容一次最多扩1m的空间。并且字符串的最大长度为512m
列表list
单键多值
redis列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边)
它的底层实际是个双向链表,对两端的操作性能很高,通过索引下标操作中间的节点性能较差
常用命令
命令 | 作用 |
---|---|
lpush/rpush | 从左边/右边插入一个或多个值 |
lpop/rpop | 从左边/右边弹出一个值 |
rpoplpush | 从一个列表左边弹出一个值,插入另一个列表的右边 |
lrange | 按照索引下标获取元素(从左到右) |
lindex | 按照下标获得元素 |
len | 获取列表的长度 |
linsert | 在指定值的前面或后面 添加值 |
lrem | 从左边开始删除n个指定的value |
lset | 将指定下标的值替换 |
数据结构
list的数据结构为快速链表quicklist
首先在列表元素较少的情况下会使用一块连续的内存存储,这个结构是ziplist(压缩列表)
它将所有的元素紧挨着一起存储,分配的是一块连续的内存
当数据量较多时会改成quicklist
因为普通的链表需要的附加指针空间太大,会比较浪费空间。比如这个列表里存储的只是index类型的数据,结构上还需要两个额外的指针prev和next
redis将链表和ziplist结合起来组成了quicklist。也就是将多个ziplist使用双向指针串起来使用。这样既满足了快速插入删除性能,又不会出现太大的空间冗余
集合set
set对外提供的功能与list类似是一个列表的功能,特殊之处在于set是可以自动去重的,当年需要存储一个列表数据,又不希望出现重复数据时,set是一个很好的选择,并且set提供了判断某个成员是否存在的接口,这个也是list所不能提供的
redis的set是string类型的无序集合。它底层其实是一个value为null的hash表,所以添加、删除、查找的时间复杂度都是O(1)
常用命令
命令 | 作用 |
---|---|
sadd | 将一个或多个元素加入集合,以存在的元素会忽略 |
smembers | 取出该集合的所有值 |
sismember | 判断集合是否有指定值 |
scard | 获取集合元素个数 |
srem | 删除集合中的某个元素 |
spop | 随机弹出一个值 |
srandmember | 随机获取n个值 |
smove | 把集合中的指定的值迁移到另一个集合中 |
sinter | 返回两个集合的交集 |
sunion | 返回两个集合的并集 |
sdiff | 返回两个集合的差集 |
数据结构
set数据结构是dict字典,字典是用哈希表实现的
java中的hashset的内部使用的是hashmap,只不过所有的value都指向同一个对象。redis的set结构也是一样,它的内部也使用hash结构,所有的value都指向同一个内部值
哈希hash
hash是一个键值对集合
hash是一个string类型的field和value的映射表,hash特别适合存储对象。类似java里面的Map<String,Object>
常用命令
命令 | 作用 |
---|---|
hset | 给指定值hash的field赋值 |
hget | 取出field的值 |
hmset | 批量设置hash的值 |
hexists | 指定的field是否存在 |
hkeys | 获取所有的field |
hvals | 获取所有的value |
hincrby | 为指定的field自增指定值 |
hsetnx | 当field不存在时,为field赋值 |
数据结构
hash类型对应的数据结构是两种:ziplist(压缩列表),hashtable(哈希表)。当field-value长度较短且个数较少时,使用ziplist,否则使用hashtable
有序集合zset
zset和set非常相似,是一个没有重复元素的字符串集合
不同之处是zset的每个成员都关联了一个评分(score),这个评分被用来按照从最低到最高的方式排序集合中的成员。集合的成员是唯一的,但是评分可以重复
因为元素是有序的,所以可以根据评分或者次序来获取一个范围的元素
访问有序集合的中间元素也是非常快的,因此你能够使用有序集合做为一个没有重复成员的智能列表
常用命令
命令 | 作用 |
---|---|
zadd | 将一个或多个元素及评分加入到zset |
zrange | 返回指定范围内所有的元素,可以使用withscores参数获取评分 |
zrangebyscore | 获取指定评分范围内的元素,并按照评分从小到大排列 |
zrevrangebyscore | 和zrangebyscore一样,只不过从大到小排列 |
zincrby | 为元素的评分加上指定值 |
zrem | 删除指定的值 |
zcount | 统计指定评分范围内的元素个数 |
zrank | 返回指定值在集合中的排名,从0开始 |
数据结构
zset是一个非常特殊的数据结构,一方面它等价于java的数据结构Map<String,Double>,可以给每一个元素value赋予一个权重score,另一方面它又类似于TreeSet,内部的元素会按照权重score进行排序,可以得到每个元素的名次,还可以通过score的范围来获取元素的列表
zset底层使用两个数据结构
- hash:hash的作用就是关联元素value和权重score,保障元素value的唯一性,可以通过元素value找到相应的score值
- 跳跃表:跳跃表的目的在于给元素value排序,根据score的范围获取元素列表
发布订阅
发布订阅是一种消息的通信模式:发送者发送消息,订阅者接收消息
reids客户端可以订阅任意数量的频道
命令
命令 | 作用 |
---|---|
subscribe | 订阅指定频道 |
publish | 向指定频道发布消息 |
新数据类型
bitmaps
bitmaps本身不是一种数据类型,实际上它是字符串(key-value),但是它可以对字符串的位进行操作
bitmaps单独提供了一套命令,所以redis中使用bitmaps和使用字符串的方法不太相同。可以把bitmaps想象成一个以位为单位的数组,数组的每个单元只能存储0和1,数组 的下标在bitmaps中叫做偏移量
常用命令
命令 | 作用 |
---|---|
setbit | 设置bitmaps中某个偏移量的值(0或1) |
getbit | 获取bitmaps中某个偏移量的值 |
bitcount | 统计设置为1的数量,可以指定范围 |
bitop | 复合操作,可以做or并集、and交集、not非、xor异或 |
hyperloglog
hyperloglog是用来做基数统计的算法,优点是,在输入元素的数量或者体积非常大时,计算基数所需的空间总是固定的、并且是很小的
redis中每个hyperloglog只需要花费12kb内存,就可以计算近2^64个不同元素的基数。这和计算基数时,元素越多耗费内存就越多的集合形成鲜明对比
但是,hyperloglog只会根据输入元素来计算基数,而不会存储输入元素本身,所以hyperloglog不能像set那样返回输入的各个元素
基数
比如数据集{1,3,5,7,5,7,8},这个数据集的基数集为{1,3,5,7,8},基数为5
基数估计就是在误差可接受范围内,快速计算基数
常用命令
命令 | 作用 |
---|---|
pfadd | 添加指定元素到hyperloglog中 |
pfcount | 计算近似的基数 |
pfmerge | 合并操作 |
geospatial
redis3.2中增加了对geo类型的支持。geo地理信息的缩写
该类型,就是元素的二维坐标,在地图上就是经纬度。redis基于该类型,提供了经纬度设置、查询、范围查询、距离查询和经纬度hash等常见操作
常用命令
命令 | 作用 |
---|---|
geoadd | 添加地理位置 |
geopos | 获得指定地区的坐标 |
geodist | 获取两个位置的直线距离 |
georadius | 以给定的经纬度为中心,找出某一半径的内元素 |
事务
redis事务是一个单独的隔离操作:事务中所有命令都会序列化,按顺序执行。事务在执行的过程中,不会被其他客户端发送来的命令请求打断
redis事务的主要作用就是串联多个命令防止别的命令插队
命令
命令 | 作用 |
---|---|
multi | 开始 |
exec | 执行 |
discard | 回滚 |
错误处理
- 组队时出错:执行时整个队列都会被取消
- 执行时出错:只有报错的命令不会执行
特性
- 单独的隔离操作:事务中的所有命令都会序列化、按顺序执行。事务在执行过程中,不会被其他客户端的命令打断
- 没有隔离基本的概念:队列中的命令没有提交之前都不会被实际执行,因为事务提交前任何指令都不会被实际执行
- 不保证原子性:事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚
锁
悲观锁
每次去拿数据的时候都认为别人会修改,所以每次在拿到数据的时候都会上锁,这样别人想拿到这个数据就会block直到它拿到锁
乐观锁
每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制
乐观锁适用于多读的应用类型,这样可以提高吞吐量。redis就是利用这种check-and-set机制实现事务的
命令
命令 | 作用 |
---|---|
watch | 在执行multi之前,先执行watch,可以监视一个或多个key,如果在事务执行之前这个key被其他命令改动,那么事务将被打断 |
unwatch | 取消watch命令的监控,如果执行watch命令后,exec或discard被执行的话,就不需要执行unwatch了 |
持久化
rdb
rdb( Redis DataBase ),每隔一段时间将内存中的数据集快照写入磁盘,它恢复时是将快照文件直接读到内存里
执行流程
redis会单独创建(fork)一个子进程来进行持久化,会先将数据写入到一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次被持久化的文件(dump.rdb)
优点
- 适合大规模的数据恢复
- 节省磁盘空间
- 恢复速度快
缺点
- fork的时候,内存中的数据被克隆了一份,大致2倍的膨胀需要考虑
- 虽然redis在fork时使用了写时拷贝技术,但是如果数据庞大时还是比较消耗性能
- 会丢失最后一次备份后的修改
fork
fork的作用是复制一个与当前进程一样的进程。新进程的所有数据都和原进程一致,但是是一个全新的进程,并作为原进程的子进程
配置文件
- dir:文件生成的目录
- dbfilename:rdb文件名,默认dump.rdb
- save:时间间隔规则
- stop-writes-on-bgsave-error:当磁盘满了时,是否关闭写操作
- rdbcompression:是否文件压缩
- rbdchecksum:检查完整性
手动持久化
命令 | 作用 |
---|---|
save | 只是保存,其他不管,全部阻塞 |
bgsave | redis会在后台异步进行快照操作,操作时,可以响应客户端请求 |
lastsave | 最后一次执行快照的时间 |
aof
aof( Append Only File ),以日志的形式来纪录每个写操作(增量保存),将redis执行过的所有写指令纪录下来(读操作不纪录),只许追加文件但不可用改写文件,redis启动之初会读取该文件重新构建数据,换言之,redis重启的话就根据日志文件的内容将写操作从前到后执行一遍,以完成恢复工作
执行流程
- 客户端的请求写命令会被append追加到aof缓冲区内
- aof缓冲区根据aof持久化策略将操作sync同步到磁盘的aof文件中
- aof文件大小超过重写策略或手动重写时,会对aof文件rewrite重写,压缩aof文件容量
- redis重启时,会重新load加载aof文件中的写操作达到数据恢复的目的
优点
- 备份机制更加稳健,丢失数据概率更低
- 可读的日志文本,通过操作aof文件,可以处理误操作
劣势
- 比rdb占用跟多磁盘空间
- 恢复备份速度更慢
- 每次写都同步,有一定性能压力
配置
- appendonly:是否开启aof,默认不开启
- appendfilename:aof文件名称,默认appendonly.aof
- dir:aof文件的保存路径,同rdb的路径一致
- appendifsync:同步频率设置
- always:始终同步,每次redis的写入都会被立刻记入日志;性能较差但数据完整性好
- everysec:每秒同步,每秒记入日志一次,如果宕机,本秒数据可能丢失
- no:不主动进行同步,把同步时机交给操作系统
同时开启rdb和aof,以aof为准
rewrite压缩
aof草原文件追加的方式,文件会越来越大,为避免此种情况,新增了重写机制,当aof文件的大小超过所设的的阈值时,redis就会启动aof文件的内容压缩,只保留可以恢复数据的最小指令集,可以使用命令bgrewriteaof
原理:aof文件持续增长而过大时,会fork出一条新进程来将文件重写(先写临时文件最后再rename),redis4后的重写,是指把rdb的快照,以二进制的形式附在新的aof头部,作为已有的历史数据,替换掉原来的流水账操作
主从复制
主服务数据更新后根据配置和策略,自动同步到从服务的机制
主服务以写为主,从服务以读为主
命令
命令 | 作用 |
---|---|
slaveof 主服务ip 端口号 | 在从服务器上执行,设置主服务器 |
info replication | 查看状态 |
slaveof on one | 将从服务变成主服务 |
原理
- 当从服务器连接上主服务器之后,从服务器向主服务器发送进行数据同步消息
- 主服务器接收到从服务器发送过来同步消息,把主服务器数据进行持久化rdb文件,把rdb文件发给从服务器,从服务器拿到rdb进行读取
- 每次主服务器进行写操作之后,和从服务器进行数据同步
哨兵模式
能监控主机是否故障,如果故障了根据投票数自动将从服务转为主服务
- 建立一个主从环境
- 新建配置文件sentinel.conf,名字不能错
- 修改配置文件内容:sentinel monitor 监控对象名称 主服务IP地址 主服务端口号 需要几个哨兵同意
- 启动:redis-sentinel 配置文件
选举新的主服务策略:
- 优先级靠前的,redis.conf中replica-priority,值越小优先级越高
- 偏移量最大的,指数据最全的
- runid最小的,每个redis启动后随机生成的一个10位的runid
集群
redis集群实现了对redis的水平扩容,即启动n个redis节点,将整个数据库分布存储在这n个节点中,每个节点存储总数的1/n
redis集群通过分区(partition)来提供一定程度的可用性(vavilability):即使集群中有一部分节点失效或者无法进行通信,集群也可以继续处理命令请求
配置
配置文件
- cluster-enabled:开启集群模式
- cluster-config-file:当前节点配置文件名称
- cluster-node-timeout:失联时间(毫秒),集群自动进行主从切换
- cluster-require-full-coverage:如果一段插槽的主从都挂掉
- yes:整个集群挂掉
- no:该插槽数据全都不能使用,其他没有问题
切换到redis的src目录下执行
redis-cli --cluster create --cluster -replicas 1 redis服务IP地址端口号列表
- 1:以最简单的方式配置集群
集群方式连接 redis-cli -c
命令
命令 | 作用 |
---|---|
cluster nodes | 查看节点信息 |
cluster keyslot | 计算key的插槽值 |
cluster countkeysinslot | 查看插槽中有几个值(只能看当前服务器的) |
cluster getkeysinslot | 返回插槽中指定数量的值 |
slots
一个redis集群包含16384个插槽,数据库中的每个键都属于这个16384个插槽中的一个
集群使用公式crc16(key)%16384来计算key属于哪个插槽,其中crc16(key)语句用于计算key的crc16校验和
集群中每个节点负责处理一部分插槽
集群中无法执行如mset这样的批量操作
如果要批量添加需要设置组,如:mset name zhangsan age 20
好处
- 实现扩容
- 分摊压力
- 无中心配置相对简单
缺点
- 多键操作是不支持的
- 多键的redis事务是不支持的
- lua脚本是不支持的
常见问题
缓存穿透
key对应的数据在数据源并不存在,每次针对此key的请求从缓存获取不到,请求会压倒数据源,从而可能压垮数源。比如用一个不存在的用户id获取用户信息,不论缓存还是数据库都没有,若黑客利用此漏洞进行攻击可能压垮数据库
解决方案
- 对空值做缓存:如果一个查询返回的数据为空,我们仍然把这个结果缓存,设置空结果的过期时间很短
- 设置可访问的名单(白名单)使用bitmaps类型定义一个可访问的名单,id作为偏移量,每次访问和bitmap进行比较,如果id不在进行拦截
- 布隆过滤器:用于检索一个值是否在一个集合中
缓存击穿
key对应的数据存在,但在redis中过期,此时若有大量并发请求过来,这些请求发现缓存过期一般会从数据库加载并回设到缓存,这个时候大并发的请求可能瞬间把数据库压垮
解决方案
- 预先设置热门数据:在redis高峰访问之前,把一些数据提前存入到redis里面,加大这些热门数据key的时长
- 实时调整:现场监控哪些数据热门,实时调整key的过期时长
- 使用锁
- 热门数据永不过期
缓存雪崩
key对应的数据存在,但在redis中过期,此时若有大量并且请求过来,这些请求发现缓存过期一般都会从数据库加载数据并回设到缓存,这个时候大并发的请求可能瞬间把数据库压垮
缓存雪崩和缓存击穿的区别在于缓存雪崩针对很多key,缓存击穿则是某一个key
- 构建多级缓存架构
- 使用锁或队列
- 设置过期标志更新缓存:纪录缓存数据是否过期(设置提前量),如果过期会触发通知另外的线程在后台去更新实际key的缓存
- 将缓存失效时间分散开:在原有过期时间上加一个随机值
分布式锁
- 使用setnx上锁,通过del释放锁
- 锁一直没有释放,设置key过期时间,自动释放
可以通过set key value nx ex time命令上锁的时候添加过期时间
这样可以保证操作的原子性
确保分布式锁可用的条件
- 互斥性:再任意时刻,只有一个客户端能持有锁
- 不会发生死锁:即使有一个客户端再持有锁的时期崩溃而没有释放锁,也能保证后续其他客户端能加锁
- 只能释放自己的锁:加锁和解锁必须是同一个客户端,客户端自己不能把别人是锁释放
- 加锁和解锁必须具有原子性