redis是最为广泛的存储中间件。是一个速度非常快的非关系内存数据库。
与关系型数据库相比,redis的命令请求不需要经过查询分析器或查询优化器进行处理,也避免了更新数据时引起的随机读写这些慢操作。他直接读写内存中的数据,并且数据是按照一定的数据结构存储的,所以速度会加快。
Redis命令之key操作命令
清楚的当前数据库:flushdb
添加string类型的数据:set (k) (v)
添加set类型的数据:sadd (k) (v1) (v2)
添加list类型的数据:rpush (k) (v1) (v2)
添加zset类型的数据:zadd (k) (c1) (v1) (c2) (v2)
添加hash类型的数据:hset (k) ((k)(v))
key值查询:keys * - 查询全部键,*表示可以匹配任意一个或多个字符,?表示任意一个字符,[abc]表示a、b、c中的任意一个字符
删除命令:del (k1) (k2)
判断键是否存在:exists (k) - 存在1不存在0
将当前数据库的键移除到指定数据库中:move (k) (db) - 将当前数据库的k,移到数据库id为db的库中。当前库中将不存在k。
移步至指定ID的数据库中:select (db)
重命名命令:rename (oldk) (newk) - 若重命名的键名已存在则将已存在的键所对应的值覆盖
设置过期时间:expire (k) (s) - 执行开始,s秒后k过期
设置相对于1970/1/1零点起的过期时间:expireat (k) (s) - 自1970/1/1日零点起的s秒后过期
查看剩余过期时间:ttl (k)
清除过期时间:persist (k)
随机返回一个k:randomkey
返回key存放的数据类型:type (k)
Redis中开启事务:multi
事务回滚:discard
事务进行提交:exec
监控k:watch (k) - 监控指定的keys,被监控的keys在事务之前发生修改,且事务中包含该key,如果事务执行exec,则exec将放弃所有事务中的命令
Redis 数据结构
字符串String、哈希Hash、列表List、集合Set、有序集合Zset。
插入k-v:set (k) (v)
获取v:get (k)
追加数据:append (k) ‘(v)’
数据删除:del (k)
获取原有v,并写入新的v:getset (k) (v) - 返回原有v
加法计算命令:incr (k),incrby (k) v - 若k对应的v为字符无法转为整型,会报异常。有值+v,没有k新增k并默认0+v为其v,v默认为1
减法计算命令:decr (k),decrby (k) v - 若k对应的v为字符无法转为整型,会报异常。有值-v,没有k新增k并默认0+v为其v,v默认为1
获取字符长度:strlen (k) - 返回值为字符长度,若无此key则返回0
设置v并设置过期时间:setex (k) (s) (v) - 设置key为k的value值为v,缓存的过期时间为s秒
查看剩余过期时间:ttl (k) - 剩余过期时间,-2为已过期
判断原值是否存在,存在不赋值,不存在,赋值:setnx (k) (v) - 返回0,原k有v,不赋值,返回1,赋值成功。
字符串替换赋值,指定位置替换:setrange (k) (index) (v) - 返回字符长度,index为字符下标,始为0,原有的v最大下标小于指定下标,不足的空位以“\\*00”来替代
字符串截取:getrange (k) (start) (end) - end为字符串最大下标,若大于按等于计算
批量赋值:mset (k) (v) (k) (v)
批量读取:mget (k) (k)
批量赋值,带事务操作:msetnx (k) (v) (k) (v) - 若其中k已存在,则全部新增失败,否则新增成功
Hash的散列可以存储多个键值对之间的映射,散列存储的值既可以是字符串又可以是数字值,并且用户同样可以对散列存储的数字值执行自增操作或者自减操作。
hash底层的数据结构实现有两种:
一种是ziplist,当存储的数据超过配置的阀值时就会转用hashtable的结构。这种转换比较消耗性能,所以应该尽量避免这种转换操作。同时满足以下两个条件时才会使用这种结构:
当键的个数小于hash-max-ziplist-entries(默认512)
当所有值都小于hash-max-ziplist-value(默认64)
另一种就是hashtable。这种结构的时间复杂度为O(1),但是会消耗比较多的内存空间。
键值新增:hset (k) ((k)(v))
根据键中键值的键查值:hget (k) (k)
查询hash中键所对应的键值数量:hlen (k)
判断指定键中的键值是否存在:hexists (k) (k) - 返回1存在0不存在
删除一个或者多个指定的字段:hdel (k) ((k1)(k2))
判断是否空且赋值:hsetnx (k) ((k)(v)) - 返回1赋值成功原没有此键,返回0赋值失败,原键存在
值为数值时增减:hincrby (k) ((k)(v)) - v 可正可负
批量添加键值对:hmset (k) ((k1)(v1)(k2)(v2))
批量根据键获取值:hmget (k) ((k1)(k2))
获取键值对:hgetall (k)
获取所有键:hkeys (k)
获取所有值:hvals (k)
List数据结构是链表结构,这意味着头尾操作速度快,list的容量是2的32次方减1个元素。
优势:即可以做为栈,又可以作为队列。列表中的元素是有序的,可以通过索引的下标获取某个范围内的元素列表,列表中的元素是可以重复的。还可以提供简便的分页。
Rabbitmq只关注数据的先进先出,没有数据优先级概念,但是rabbitmq也可以建立多个队列用程序路由来实现这个特权功能,而Redis实现消息队列,是可以灵活掌控的。
列表插入:rpush (k) (v) (v) - 返回列表中值的数量
查看列表数据:lrange (k) (start) (end) - end 为-1时,标识列表最后一位
向列表中左边插入数据:lpush (k) (v)
向列表中右边插入数据:rpush (k) (v)
向列表中某值前后插入:linsert (k) after(before) (v1) (v2) - 若列表中不存在v1则返回-1
列表数据删除:lpop(rpop) (k) - 从列表左(右)边移除一条数据,同时输出被删除的数据
列表区间删除:ltrim (k) (start) (end) - 保留设定的两个下标区间的值,删除不在其区间的所有值
列表长度查询:llne (k)
列表下标对应的值查询:lindex (k) (index)
列表根据下标修改数据:lset (k) (index) (v)
列表数据头尾转换:rpoplpush (k1) (k2) - 两个列表k1,k2,将k1列表的尾元素添加到k2列表的头元素中
列表阻塞:blpop (k) (s) - k列表有值的话从左边移除一个元素,若没有等k列表中有值再移除,等待时间为s秒,设置为0则无限时长
列表数据头尾转换阻塞:brpoplpush (k1) (k2) (s) - 将A列表的尾元素添加到B列表的头元素中,如果A列表中有值则插入,如果没值,则等待A中插入数据为止,等待时间为30秒,如果时间设置为0表示阻塞时间无限延长
Set是用哈希表来保持字符串的唯一性,集合汇总不可重,没有先后顺序。
集合新增:sadd (k) (v1) (v2)
查看集合数据:smembers (k)
判断集合中是否存在某值:sismember (k) (v) - 存在1不存在0
删除集合中数据:srem (k) (v1) (v2)
集合中数据个数:scard (k)
集合间移动数据:smove (k1) (k2) (v) - 将k1中的数据v移除的同时将数据v添加到k2中,v只能有一个数据
集合交集:sinter (k1) (k2)
集合并集:sunion (k1) (k2)
集合差集:sdiff (k1) (k2) - 输出为k1中存在k2中不存在的数据。
zset和set类型极为相似,他们都是字符串的集合,都是不可重复的,而zset中的每一个成员都会有一个分数(score)与之关联,Redis正式通过分数来为集合中的成员进行从小到大的排序,但是尽管zset中数据是唯一的,分数却是可以重复的。在zset中添加,删除,更新都是很快的,其时间复杂度是zset中数据数量的对数,因为有序所以访问在集合中部的数据也是非常快的。
有序集合新增:zadd (k) ((c1) (v1)(c2) (v2))
查看有序集合成员个数:zcard (k)
查看指定范围内成员:zrange (k) (start) (end) withscores - withscores 带这个输出结果为 分数+成员值
获取有序集合下标位置:zrank (k) (v)
获取分数区间之间的成员个数:zcount (k) ((c1)(c2)) - 因为有序集合中分数是可重复的
删除指定一个或多个成员:zrem (k) (v1) (v2)
获取指定值的分数:zscore (k) (v)
给指定元素的分数进行增减操作:zincrby (k) (i) (v) - i为v对应分数的增值
根据指定分数的范围获取值:zrangebyscore (k) -inf(c1) +inf(c2) limit (l1) (l2) - +inf表示最后一个成员,-inf表示第一个成员
倒序输出指定范围的数据:zrevrange (k) (start) (end) zrevrangebyscore (k) (c1) (c2)
根据坐标,分数范围删除数据:zremrangebyrank (k) (start) (end) zremrangebyscore (k) (c1) (c2)
多个有序集合的并集:zunionzstore (k3) 2 (k1) (k2) - 将k1和k2的并集添加到k3中
多个有序集合的并集:zinterstore (k3) 2 (k1) (k2) - 将k1和k2的交集添加到k3中
Redis扩展知识
基数统计HyperLogLog、布隆过滤BloomFilter、地理坐标GEO、分布式缓存发布/订阅Pub/Sub、模块化Redis Module、搜索引擎RediSearch。
HyperLogLog是在大数据的情况下关于数据基数的空间复杂度优化实现,是Redis的高级数据结构。
HyperLogLog可以提供不精确的去重计数方案,标准误差是0.81%,这个数据结构需要占据12k的存储空间,所以他不适合统计单个用户相关的数据,在计数较小时,他的存储空间采用稀疏矩阵存储,空间占用极小,当计数慢慢增加,超过了一定的阀值,才会转成稠密矩阵,占用12k的空间。
HyperLogLog实现中用到的是 16384 个桶(BitKeeper),也就是 2^14,每个桶的 maxbits 需要 6 个 bits 来存储,最大可以表示 maxbits=63,于是总共占用内存就是2^14 * 6 / 8 = 12k字节
布隆过滤器是以插件的形式实现的,是在大数据情况下关于检索一个元素是否在一个集合中的空间复杂度优化后的实现,是Redis的概率型数据结构。布隆过滤器是一个 bit 向量或者说 bit 数组,特点是高效的插入和查询。
布隆过滤器被广泛用于网页黑名单系统、垃圾邮件过滤系统、爬虫的网址判重系统以及解决缓存穿透问题。
相比于传统的 List、Set、Map 等数据结构,它更高效、占用空间更少,但是缺点是其返回的结果是概率性的,而不是确切的。
布隆过滤器实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。
原理:当一个元素被加入集合时,通过K个散列函数将这个元素映射成一个位数组中的K个点,把它们置为1。检索时,我们只要看看这些点是不是都是1就(大约)知道集合中有没有它了:如果这些点有任何一个0,则被检元素一定不在;如果都是1,则被检元素很可能在。
优点:相比于其它的数据结构,布隆过滤器在空间和时间方面都有巨大的优势。布隆过滤器存储空间和插入/查询时间都是常数(O(k))。而且它不存储元素本身,在某些对保密要求非常严格的场合有优势。
缺点:一定的误识别率和删除困难。
结合以上几点及去重需求(容忍误判,会误判在,在则丢,无妨),决定使用BlomFilter。
GEO功能在Redis3.2版本提供,支持存储地理位置信息用来实现诸如附近位置、摇一摇这类依赖于地理位置信息的功能。geo的数据类型为zset。redis基于该类型,提供了经纬度设置,查询,范围查询,距离查询,经纬度Hash等常见操作。
geo操作支持查询:help @geo
GEOADD key longitude latitude member [longitude latitude member ...]
添加成员的经纬度信息
GEODIST key member1 member2 [unit]
计算成员间距离
GEOHASH key member [member ...]
计算经纬度Hash
GEOPOS key member [member ...]
获取经纬度
GEORADIUS key longitude latitude radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] [ASC|DESC] [STORE key] [STOREDIST key]
基于经纬度坐标范围查询
GEORADIUSBYMEMBER key member radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] [ASC|DESC] [STORE key] [STOREDIST key]
基于成员范围查询
Redis的pub/sub是一种消息通信模式,主要的目的是解除消息发布者和消息订阅者之间的耦合,Redis作为一个pub/sub的server,在订阅者和发布者之间起到了消息路由的功能。
redis的发布订阅功能和23种设计模式中的观察者模式极为相似。
subscribe (channel) ** 频道订阅
unsubscribe (channel) ** 退订某频道
psubscribe (channel *) ** 频道模糊订阅
punsubscribe(channel *) ** 频道模糊订阅
publish (channel) (message) ** 在某频道发布消息
pubsub channels ** 查看发布订阅频道
pubsub numsub (channel) ** 查询指定频道的订阅数量
pubsub numpat ** 返回订阅模式的数量
发送者(发送信息的客户端)不是将信息直接发送给特定的接收者(接收信息的客户端), 而是将信息发送给频道(channel), 然后由频道将信息转发给所有对这个频道感兴趣的订阅者。也就是说发送者无须知道任何关于订阅者的信息, 而订阅者也无须知道是那个客户端给它发送信息, 它只要关注自己感兴趣的频道即可。
Redis模块化是4.0出现一大改动点,使得可以通过外部模块对Redis进行功能性扩展。 Redis的模块采用的是动态链接库的方式,可以启动的时候加载,也可以在运行时加载。跟 nginx 的模块化差不多,都是可以让别人更好的定制化自己需要的redis。
RediSearch是一个高性能的全文搜索引擎,可作为一个Redis Module 运行在Redis上。但是它与其他Redis搜索库不同的是,它不使用Redis内部数据结构,例如:集合、排序集。Redis原生的搜索还是有很大的局限性,简单的分词搜索是可以满足,但是应用到复杂的场景就不太适合。
使用RediSearch的吞吐量高、延迟低,但是相比于ElasticSearch和Solr支持的特性上还有些欠缺比如:中文的模糊搜索支持的不是很好,但是其性能很高在某些场景是可以作为搜索引擎的替代方案来试用。
互斥性:任意时刻只能有一个客户端拥有锁,不能同时多个客户端获取
安全性:锁只能被持有该锁的用户删除,而不能被其他用户删除
死锁:获取锁的客户端因为某些原因而宕机,而未能释放锁,其他客户端无法获取此锁,需要有机制来避免该类问题的发生
容错:当部分节点宕机,客户端仍能获取锁或者释放锁
锁信息必须是回过期超时的,不能让一个线程长期占有一个锁而导致死锁。
同一时刻只能有一个线程获取到锁。
redis是单线程的。
setnx(key, value):“set if not exits”,若该key-value不存在,则成功加入缓存并且返回1,否则返回0。
get(key):获得key对应的value值,若不存在则返回nil。
getset(key, value):先获取key对应的value值,若不存在则返回nil,然后将旧的value更新为新的value。
expire(key, seconds):设置key-value的有效期为seconds秒。
先拿setnx来争抢锁,抢到之后,再用expire给锁加一个过期时间防止锁忘记了释放。
如果在setnx之后执行expire之前进程意外crash或者要重启维护,则需要吧setnx和expire合成一条命令来用。
如果redis正在给线上的业务提供服务,那执行其指令会导致阻塞一段时间,需要等指令执行完毕后,服务才会恢复。这个时候可以使用scan指令,scan指令可以无阻塞的提取出指定模式的key列表,但会有一定的重复概率,在客户端做一次去重就可以了,只是整体花费的时间会长一点。
一般用list结构做队列,rpush生产消息,lpop消费消息。当lpop没有消息的时候可以适当sleep一会再重试。或者用blpop指令,在没有消息的时候他会阻塞住直到消息的到来。
若想生产一次,消费多次,可以使用pub/sub主题订阅者模式,可以实现1:N的消息队列。其缺点是在消费者下线的情况下会使得上产的消息丢失,需要使用专业的消息队列如Rabbitmq等。
使用zset(有序集合),拿时间戳作为score(分数),消息内容作为key调用zadd来生产消息,消费者用zrangebyscore指令获取N秒之前的数据轮询进行处理。
如果同一时间Key过期,Redis可能会出现短暂的卡顿现象,可以在时间上加一个随机值,使得过期时间分散开来。
RDB(Redis DataBase)
【全量】RDB 持久化,是指在指定的时间间隔内将内存中的数据集快照写入磁盘。实际操作过程是,fork 一个子进程,先将数据集写入临时文件,写入成功后,再替换之前的文件,用二进制压缩存储。
AOF(Append Only File)
【增量】AOF持久化,以日志的形式记录服务器所处理的每一个写、删除操作指令,查询操作不会记录,以文本的方式记录,可以打开文件看到详细的操作记录。在下次redis重新启动时,只要把这些写指令从前到后再重复执行一遍,就可以实现数据恢复了。
RDB耗费较长的时间,不够实时,在停机的时候会导致大量丢失数据,所以需要AOF来配合使用,在Redis实例重启的时,会使用RDB持久化文件重新构建内存,再使用AOF重放近期的操作指令来实现完整恢复重启之前的状态。如果机器掉电了,则要取决于AOF日志Sync属性的配置,入果不要求性能,在每条写指令时都sync一下磁盘,就不会丢失数据。但是在高性能的要求下每次都sync是不现实的,一般都使用定时sync,比如1s1次,这个时候最多就会丢失1s的数据。
RDB的原理是fork和cow,fork是指redis通过创建子进程来进行RDB操作,cow指的是copy or write ,子进程创建后,父进程共享数据段,父进程继续提供读写服务,写脏的页面数据会逐渐和子进程分离开来。
如果没有数据持久化的需求,也完全可以关闭RDB和AOF方式,这样的话,redis将变成一个纯内存数据库,就像memcache一样。
Pipeline指的是管道技术,指的是客户端允许将多个请求依次发给服务器,过程中而不需要等待请求的回复,在最后再一并读取结果即可。
可以将多次IO往返的时间缩减为一次,前提是pipeline执行的指令之间没有因果相关性。使用redis-benchmark进行压测的时候可以发现影响redis的QPS(每秒查询率)峰值的一个重要因素是pipeline批次指令的数目。
管道技术使用广泛,例如许多POP3协议已经实现支持这个功能,大大加快了从服务器下载新邮件的过程。
Redis可以使用主从同步,从从同步。第一次同步时,主节点做一次bgsave,并同时将后续修改操作记录到内存buffer,待完成后将rdb文件全量同步到复制节点,复制节点接受完成后将rdb镜像加载到内存。加载完成后,再通知主节点将期间修改的操作记录同步到复制节点进行重放就完成了同步过程。
集群的原理就是分库分表,去中心化。
每一个节点都存有这个集群所有主节点以及从节点的信息。它们之间通过互相的ping-pong判断是否节点可以连接上。如果有一半以上的节点去ping一个节点的时候没有回应,集群就认为这个节点宕机了,然后去连接它的备用节点。
A、某个主节点和所有从节点全部挂掉,集群就进入faill(挂掉)状态。
B、如果集群超过半数以上master(主节点)挂掉,无论是否有slave(从节点),集群进入fail状态。
C、如果集群任意master挂掉,且当前master没有slave.集群进入fail状态。
集群中原则每个Master节点都有一个或多个Slave节点。集群中所有的Master节点都可以进行读写数据,不分主次,所以redis集群是去中心化的。每个Master节点与Slave节点间通过Goossip协议内部通信,异步复制。(所有的redis节点彼此互联(PING-PONG机制),内部使用二进制协议优化传输速度和带宽.),但是异步赋值会导致某些特定情况下的数据丢失,所以redis集群不能保证数据的强一致性。
虚拟槽分区(槽:slot – RedisCluster )原因:解耦数据与节点关系,节点自身维护槽映射关系,分布式存储
RedisCluster 的缺陷:
a、键的批量操作支持有限,比如mset, mget,如果多个键映射在不同的槽,就不支持了。
b、键事务支持有限,当多个key分布在不同节点时无法使用事务,同一节点是支持事务。
c、键是数据分区的最小粒度,不能将一个很大的键值对映射到不同的节点。
d、不支持多数据库。
e、复制结构只支持单层结构,不支持树型结构。
由于Cluster架构中无Proxy(代理)层,客户端是直接与集群中的任意可用节点直接交互的,但是一个请求是怎么找到集群中的一个节点的呢,redis有多种策略,一般使用CRC16去hash(key)计算该请求要分配到具体的哪一个节点上。然后才是 客户端与节点的直接操作,对象保存到Redis之前先经过CRC16哈希到一个指定的Nod(节点)上。