Redis学习(一) redis基本用法-缓存的应用

1、Redis是什么?

官方的说法是:Redis是一个开源的,基于BSD许可的,内存中的数据结构存储系统,它可以用作数据库、缓存和消息中间件,它支持多种类型的数据结构,比如字符串(String),散列(hashes),列表(list),集合(set),有序集合(sorted set)与范围查询,bitmaps,hyperloglogs,地理空间(geospatial),索引半径查询。
Redis内置了复制(replication),LUA脚本(Lua scripting),LRU驱动事件(LRU eviction)事务(transactions)和不同级别的磁盘持久化(persistence),并可以通过哨兵(Sentinel)和自动分区(Cluster)提供高可用性(high availability)。

什么意思呢?
Redis可以当做数据库使用,也可以当做缓存和消息队列来用,并且,它支持多种类型的数据结构,并且内置了一大堆不明觉厉的功能,而且还可以布置集群来提高可用性。

下面,我们就来逐步分析Redis的作用以及业务场景。

2、使用场景

因为Redis的数据都储存在内存中,当然,它也可以持久化,但它的持久化并不能保证数据绝对落地,而且还会带来Redis性能的下降,因为,持久化太过频繁的话,会增大Redis的压力。
那么,Redis的使用场景就呼之欲出了:用于存储少量的,访问频率较高的数据。
当然了,少量的这一点,是因为内存很珍贵,如果你的服务器内存极高,自然就不需要太过注重这一点了。

3、缓存的使用方式

Redis有五种最常用的基本类型,string,hash,list,set以及zset(sorted set)。
Redis有很多命令,光String相关的就是22个命令,比如setgetappendgetset等等,官方有提供详细的命令文档,有兴趣的可以去了解一下。

Redis命令

如果觉得这些命令不够用的话,redis还提供了lua脚本扩展自定义命令的功能,可以编写原子性的自定义命令。

  • 3.1 String
    string是Redis最基本的数据类型,使用key-value的方式存储。
    此类型是二进制安全的,也就是说Redis的string可以存储任何数据,比如jpg对象,或者序列化流。
    一个键最多可以储存512M。
    如果key已经存在,那么,新的value会覆盖旧的value
127.0.0.1:6379> set testKey Redis
OK
127.0.0.1:6379> get testKey
"Redis"
127.0.0.1:6379>
  • 3.2 Hash
    hash也是键值对集合,只不过存储的是string类型的field和value的映射表,非常适合存储对象。
    执行hmset命令时,如果哈希表不存在的话,就会创建一个新的哈希表,并插入数据
    如果field已经存在,则新的value会覆盖旧的value
127.0.0.1:6379> hmset testMap key1 "value1" key2 "value2"
OK
127.0.0.1:6379> hmget testMap key1
1) "value1"
127.0.0.1:6379>

若是想取出key中的所有field的话,应该用hgetall命令

127.0.0.1:6379> hgetall testMap
1) "key1"
2) "value1"
3) "key2"
4) "value2"
127.0.0.1:6379>

注意:hmset命令在4.0.0版本之后被废弃,要使用hset命令
· 3.3 List
Redis中的list是有序列表,按照插入顺序排序,可以添加任意一个元素到列表的头部(左边)或者尾部(右边)

127.0.0.1:6379> lpush testList value1
(integer) 1
127.0.0.1:6379> lpush testList value2
(integer) 2
127.0.0.1:6379> lrange testList 0 10
1) "value2"
2) "value1"
127.0.0.1:6379>

可以看到,value2虽然是后插入的,但因为使用了lpush命令,所以被插入到了头部,变成了第一个
要是想插入到尾部的话,就要使用rpush命令
· 3.4 Set
Redis的Set是无序集合,如果某个value已经存在,执行命令会返回0

127.0.0.1:6379> sadd testSet value1
(integer) 1
127.0.0.1:6379> sadd testSet value2
(integer) 1
127.0.0.1:6379> sadd testSet value3
(integer) 1
127.0.0.1:6379> sadd testSet value4
(integer) 1
127.0.0.1:6379> smembers testSet
1) "value3"
2) "value4"
3) "value1"
4) "value2"
127.0.0.1:6379> sadd testSet value4
(integer) 0
  • 3.5 zset
    zset和set一样,都是string类型的集合,且都不允许重复的value,但zset是有序的。
    zset中,每个元素都会关联一个double类型的score,redis就是通过score来进行排序的,zset的value是唯一的,但score是可以重复的。
127.0.0.1:6379> zadd testZSet 0 value1 0 value2 0 value3 2 value4 1 value5
(integer) 5
127.0.0.1:6379> zrangebyscore testZSet 0 10
1) "value1"
2) "value2"
3) "value3"
4) "value5"
5) "value4"
127.0.0.1:6379>

可以看到,zset的顺序和插入顺序无关,而是和score的值有关。

4、事务

关系型数据库中事务的特点就不再赘述了,在Redis中,事务可以一次执行多个命令,并且带有以下两个重要保证:

  • 事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行,事务在执行过程中,不会被其他客户端发来的命令请求打断。
  • 事务是一个原子操作:要么全不执行,要么全部执行。

当使用AOF做持久化的时候,Redis会使用write(2)命令将事务写入到磁盘中。
但是,如果当Redis服务期意外中断,或者遇见硬件故障,就可能只有部分事务被成功写入磁盘里,而Redis在重启时如果发现AOF文件出现了这样的问题,就会退出,并且报错。
使用redis-check-aof程序可以修复这一问题:它会移除 AOF 文件中不完整事务的信息,确保服务器可以顺利启动。
multi命令用于开启事务,它总是返回OK。
multi被执行后,客户端可以继续向服务端发送任意数量的命令,这些命令不会被立即执行,而是被放到一个队列中,直到调用exec命令为止。

127.0.0.1:6379> multi
OK
127.0.0.1:6379> rpush testList v1
QUEUED
127.0.0.1:6379> rpush testList v2
QUEUED
127.0.0.1:6379> rpush testList v3
QUEUED
127.0.0.1:6379> exec
1) (integer) 1
2) (integer) 2
3) (integer) 3
127.0.0.1:6379>

exec命令返回的是一个数组,数组中的每个元素都是执行事务中的命令产生的回复,并且,回复命令的顺序和命令发送的顺序一致。
当客户端处于事务状态时,每一个命令都会返回 QUEUED的状态回复,代表这些命令入队成功。

注意:Redis的事务没有回滚功能,如果某条命令执行失败了,那么下面的命令依然会继续执行——Redis不会停止执行事务的命令。

按照Redis官方的说法,之所以不回滚,是因为命令错误只有一种可能:语法错误,或者命令用在了错误的key上。
也就是说,这些错误是因为使用者的原因造成的,这些问题应该在开发时就被发现,不应该出现在生产环境中。
而放弃了回滚,Redis才可以保持内部的简洁和迅速。

简单来说就是:我不能因为你的低级错误而妥协,以至于牺牲我自己的最大优点。

discard命令可以放弃事务,并清空队列,客户端也会从事务状态中退出。

127.0.0.1:6379> set testKey 1
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> incr testKey
QUEUED
127.0.0.1:6379> discard
OK
127.0.0.1:6379> get testKey
"1"
127.0.0.1:6379>

除了上面三个命令,Redis还提供了watch命令,为事务提供check-and-set(CAS)行为。

watch命令可以监视某个key,并会发觉这些key是否被改动了,如果至少一个被监视的key在执行exec命令之前被改动,那么整个事务都会被取消,exec命令返回nil来表示事务已经失败。
举个例子,我在客户端1里设置了key1=1,然后监视此key并开启事务,将key1设置成2,但是不提交。

127.0.0.1:6379> set key1 1
OK
127.0.0.1:6379> watch key1
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set key1 2
QUEUED

然后在客户端2里,把key1设置成3,表示在事务执行期间,有另一人修改了watch监视的key。

127.0.0.1:6379> set key1 3
OK
127.0.0.1:6379>

这时候,返过来再去客户端1里执行exec命令。

127.0.0.1:6379> exec
(nil)
127.0.0.1:6379> get key1
"3"
127.0.0.1:6379>

可以看到,exec命令执行失败了,并返回了nil,而key1的值,依然是客户端2里设置的3.
这也就是Redis中的乐观锁。

对key的监视周期,从执行watch命令开始生效,直到调用exec为止。
而且还可以监视多个key。

127.0.0.1:6379> watch key1 key2 key3
OK

exec被调用后,不管事务是否成功,对key的监视都会取消。

使用unwatch命令,可以取消对所有key的监视。

5、bitmap

什么是bitmap?
所谓bitmap,其实就是byte数组,用二进制表示,值只有1和0
bitmap最典型的应用,我个人觉得应该就是布隆过滤器了,实际应用中,大概是我接触到的项目比较少,基本用不到bitmap。
从网上说的案例来讲,最常用的就是存储用户在线状态,记录用户签到记录等操作。
比如建立一个极长的bitmap,以用户的ID当下标,如果用户签到了,就在将此下标中的值更新为1,这么做的优势就是,可以极大地节省空间,并且时间复杂度为O(1),速度很快。

6、hyperloglogs

HyperLogLog 是用来做基数统计的算法,HyperLogLog 的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定 的、并且是很小的。

在 Redis 里面,每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 2^64 个不同元素的基数。这和计算基数时,元素越多耗费内存就越多的集合形成鲜明对比。

但是,因为 HyperLogLog 只会根据输入元素来计算基数,而不会储存输入元素本身,所以 HyperLogLog 不能像集合那样,返回输入的各个元素。

所谓用户基数就是指不重复的数据,比如数据集 {1, 3, 5, 7, 5, 7, 8}, 那么这个数据集的基数集为 {1, 3, 5 ,7, 8}, 基数(不重复元素)为5。 基数估计就是在误差可接受的范围内,快速计算基数。

7、过期策略

Redis可以使用expire命令来为key设置过期时间,设置的时间过期后,key会被自动删除。

过期时间只能使用删除key或者覆盖key的命令来清楚,包括delsetgetset和所有*store命令。

对于修改key中的值而不是覆盖的操作,不会修改过期时间,比如想list的新增命令lpush,或者修改hash的值hset,这些都不会修改过期时间。

redis也提供了另一个命令persist,可以把key改回持久的,以清除过期时间。

注意:使用rename修改key的名字,新的key会继承旧key的属性,包括过期时间。

知道了redis有过期时间的概念,并且可以自动删除过期的key,那么,很容易就能想到一个新的问题。

redis是怎么删除过期的key的?

redis有两种过期策略:定期删除惰性删除

先说惰性删除。
很简单,当客户端获取某个key的时候,redis会检查它是否设置了过期时间以及是否过期,如果key已经过期,则将其删除。

定期删除就是,redis会每秒执行10次下面的操作
1、从带有过期时间的key里随机挑选出20个进行检查
2、删除所有过期的key
3、如果被删除的key超过了25%,则立即继续执行1,直到被删除的key低于25%为止

这是一个狭义的概率算法,假设我们选出的key代表了整个存储空间,这个算法,可以保证过期的key一直低于25%。

但是,这样一来,还有个问题。

假如经过了两种删除策略之后,内存依然不足的话,会发生什么情况?

答案是:内存淘汰机制

所谓内存淘汰机制,就是当redis内存不足的时候,会从key中删除一些数据,腾出内存空间写入新的数据。

而redis的内存淘汰机制,有以下8种

  1. volatile-lru:从已设置过期时间的数据集中,移除最久没有使用的key
  2. volatile-lfu:从已设置过期时间的数据集中,移除使用频率最少的key
  3. volatile-ttl:从已设置过期时间的数据集中,移除将要过期的key
  4. volatile-random:从已设置过期时间的数据中,随机移除某个key
  5. allkeys-lru:当内存不足写入新数据时,从所有数据中,移除最久没有使用的Key
  6. allkeys-lfu:当内存不足写入新数据时,从所有数据中,移除使用频率最少的Key
  7. allkeys-random:当内存不足写入新数据时,从所有数据中,随机移除某个key
  8. no-eviction:当内存不足写入新数据时,写入操作会报错,同时不删除数据【默认】

注意:volatile-lfu和allkeys-lfu,是redis4.0才引入的策略,3.x版本是没有的。

8、管道(Pipelining)

Redis是一种基于客户端-服务端模型及请求/相应协议的TCP服务。
也就是说,通常情况下,一个请求,会遵循以下步骤:

  • 客户端向服务端发送一个查询请求,并监听Socket返回,通常是以阻塞模式等待服务器相应。
  • 服务端处理命令,并将结果返回客户端。

也就是说,每个命令的执行时间=客户端发送时间+服务端处理时间+服务端返回时间+一个网络的来回时间。其中,一个网络的来回时间是不确定的,它的决定因素很多,比如客户端到服务端需要多少跳,网络是否拥堵等。这个时间的量级是最大的,也就是说,一个命令的执行时间,很大因素上要取决于网络开销。

举例来说,假如服务端每秒可以处理1万条数据,网络开销是100毫秒,那么实际上每秒钟只能处理10个请求,最简单的办法,是把服务端和客户端都放在同一服务器上,这样可以把网络开销降低到1ms以下,但生产环境中,我们几乎不会这样来使用。

为了解决这一问题,Redis提供了管道技术。
其实不光是Redis,管道技术已经非常成熟并且得到广泛应用了,例如POP3协议由于支持管道技术,从而显著提高了从服务器下载邮件的速度。

在Redis中,如果客户端使用管道发送了多条命令,那么服务器会将多条命令放入一个队列中,这一操作会占用一些内存,所以管道中的命令不是越多越好,而是要有一个合理的值,根据自己的服务器内存来分配的合理的值。

说到这里,其实可以看出来了,所谓管道技术,就是将数据打包处理,通过降低网络开销的方式来减少处理时间,以提升效率。

其实Redis还提供了另一种方式来提高效率,即脚本(Scripting),脚本的性能还要优于管道,只不过要使用lua来编写脚本,有些类似于存储过程,有需要的可以根据自己的需要来使用。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

友情链接更多精彩内容