初识Redis缓存
Redis缓存:
实际开发中经常使用Redis作为缓存数据库,从而提高数据存取效率,减轻后端数据库的压力。
可以将经常被查询的数据缓存起来,比如热点数据,这样当用户来访问的时候,就不需要到MySQL中去查询了,而是直接获取Redis中的缓存数据。
Redis是键值对类型的数据库,在设计数据时都要指定key和value。
例如某查询系统可以根据提供的网站名称返回相应的网址,因为查询的系统的并发量较高,此时可以将上述的对应关系通过Redis缓存到内存中。
设置网站名称baidu到网址http://www.baidu.com的对应关系:名称作为key,网址作为value
取出缓存:用户输入名称,数据库就可以把对应网址取出来了
设置缓存的时间:
实际项目里一般需要同时设置数据的生存时间。如果不设置,那么内存里的数据会越积越多,最终造成内存溢出。
可以在命令后通过ex或px参数设置该对象的生存时间,其中ex的时间单位是秒,px的时间单位是毫秒,如:
设置了val1对象的生存时间是2秒,当过了对应的时间用get命令去获取对象的值时,只能得到表示空的null值。
Redis数据类型
Redis为了存储不同类型的数据,提供了五种常用数据类型
string (字符串)
hash (哈希散列)
list (列表)
set (集合)
zset (sorted set:有序集合)
注意此处的数据类型指的是Value(值)的数据类型,而非key。
基本数据类型String
SDS:
字符串是Redis里最基本的数据类型,Redis使用标准C语言编写,但在存储字符时,Redis并未使用C语言的字符类型。
而是自定义了一个属于特殊结构SDS(Simple Dynamic String)即简单动态字符串)。【可以理解为用C的结构体实现了一个String类型】
SDS是一个可以修改的内部结构,非常类似于Java的ArrayList。
SDS的结构定义如下:
String类型的空间分配方式:
string采用了预先分配冗余空间的方式来减少内存的频繁分配,如下图所示:
(总体空间即上图char buf[]的空间 )
Redis每次给string分配的空间都要大于字符串实际占用的空间,这样就在一定程度上提升了Redis string存储的效率,比如当字符串长度变大时,无需再重新申请内存空间。
当字符串所占空间小于1MB时,Redis对字符串存储空间的扩容是以成倍的方式增加的;而当所占空间超过1MB时,每次扩容只增加1MB。Redis字符串允许的最大值字节数是512MB。
String类型的使用:
字符串是Redis里最基本的数据类型。
可以使用set命令设置字符串类型数据,具体语法如下:
set key value [EX seconds|PX milliseconds] [NX|XX] [KEEPTTL]
key和value分别表示待设置字符串的键和值,如果对应的key里已经有值,那么再次执行set命令时会进行覆盖。value就是String类型。[]中为可选内容
通过EX和PX参数可以指定该变量的生存时间,在大多数场景里,应该合理设置对应的生存时间,否则可能会导致内存溢出的问题。
NX参数表示当key不存在时才进行设置值的操作,如果key存在,那么该命令执行;XX参数表示当key存在时才进行操作。
KEEPTTL是Redis6.0的新特性,是指"保留生存时间”,既重复set时,保留值的过期时间。
设置以后,可以通过get key的方式读取对应key的字符串变量,具体示例:
设置获取单个字符串:
缓存员工号002、姓名Tom"的数据:
设置获取多个字符串:
mset和mget命令分别能同时设置和获取多个字符串。
其中,mset命令的语法:mset key value [key value...]
mget命令的语法:mget key [key...]
mset和mget命令不包含NX、XX、PX和EX等参数。
示例:
ttl =-1 代表永久有效
对值进行增量和减量操作:
通过incr key命令能对key所对应的数字类型值进行加1操作,如果k不存在,默认在0的基础上+1
通过decr key命令能对key所对应的值进行减1操作,如果k不存在,默认在0的基础上-1
通过incrby key increment命令能对key对应的值进行加lincrement的操作
通过decrby by decrement命令能对key对应的值进行减decrement的操作
incr和decr命令作用在字符类型上,会出错
运行上述命令时,需要确保key对应的值是数字类型,否侧会报错。在实际项目里,上述命令一般会用在统计流量和控制并发的场景中
通过getset命令设置新值:
getset key value
如果key对应的值存在,则会用value覆盖旧值,同时返回旧值(set可以覆盖但不会返回旧值)
如果key对应的值不存在,也会设值,但会返回null
基本数据类型Hash
Hash类型:
Redis hash(哈希散列)是由字符类型的field(字段)和value组成的哈希映射表结构(也称散列表)
【redis的键值对映射则指的是整体数据】
hash类型中,field与value一一对应,且不允许重复。
Redis hash特别适合于存储对象。一个filed/value可以看做是表格中一条数据记录;而一个key可以对应多条数据(多个字段和value的组合)。
当hash类型移除最后一个元素后,该存储结构就会被自动删除,其占用内存也会被系统回收。
Hash类型底层存储结构的实现方式(2种):
第一种,当存储的数据量较少时,hash采用ziplist(压缩列表,内存中一块连续的存储空间)作为底层存储结构,此时要求符合以下两个条件:
(1)哈希对象保存的所有键值对(键和值)的字符串长度总和小于64个字节。
(2)哈希对象保存的键值对数量要小于512个。
当无法满足上述条件时,hash就会采用第二种方式来存储数据:
也就是dict(字典结构)该结构类似于Java的HashMap,是一个无序的字典,并采用了数组和链表相结合的方式存储数据。
在Redis中,dict是基于哈希表算法实现的,因此其查找性能非常高效,其时间复杂度为O(1)。
哈希表:
哈希表又称散列表,其初衷是将数据映射到数组中的某个位置上, 这样就能够通过数组下标来访问该数据,从而提高数组的查询效率。
现在有1/5/8/ 三个数字,你需要把这三个数字映射到数组中,由于哈希表规定必须使用下标来访问数据,因此你需要构建一个0到8的数组
普通查找:把待查找的数字,在相应的下标数组上标记出来,它们之间一一对应。虽然这样能查,但很浪费存储空间,并且查找效率也不高。
如果采用哈希表的话,我们只需要申请一个长度为3的数组(与待查找的元素个数相同),如下图所示:
将1/5/8分别对数组长度3做取模运算,然后把它们指向运算结果对应的数组槽位,这样就把一组离散的数据映射到了连续的空间中,从而在最大限度上提高了空间的利用率,并且也提高了元素的查找效率。
但可能会出现一个问题,数字5、8映射到同一个槽位上,这样就导致其中一个数字无法查找到。上述这种情况在实际中也会遇到,我们把它称为“哈希冲突”或者“哈希碰撞”。
解决哈希冲突:
有许多方法可以解决“哈希冲突”,比如开放地址法、链表地址法,再次散列法等,而Redis采用是链表地址法。
将有冲突的数据使用链表把它们串联起来,这样即使发生了冲突,也可以将数据存储在一起,最后,通过遍历链表的方式就找到上述发生“冲突”的数据。
如下所示:
如果值是字符串的话,就需要通过哈希函数将字符串转换成具体的数值,然后再对其进行映射。
设置与获取(hset和hget)
哈希类型可以通过hset命令来设置数据,通过hget命令来读取数据。
hset的命令格式如下
hset key field value [field value ...]
其中key是待缓存对象的键,field value是以键值对的形式描述的对象数据。
针对同一个key,可以用多个field value对来存储数据,这里field可以理解成对象的属性名,value可以理解成对象的属性值。
hget的命令格式如下,其中key是待读取对象的键,如果存在key和field所对应的数据,则返回该数据,否则返回null值。仅传入key而不传入field参数,会报错。
hget key field
示例:
hsetnx命令
用hset命令设置哈希类型的变量时,如果出现重复,就会用后设的数据覆盖掉之前的数据。
与之对应的是hsetnx命令
hsetnx key field value
只有当key和field所对应的value不存在时才会设置对应的value,而且key之后只能带一对field和value
hset命令的key之后能带多个field和value对。
针对key的相关操作
通过hkeys key命令,能查看该key所对应哈希类型数据的所有field;
通过hvals key命令,能查看key所对应哈希类型数据的所有value;
通过hgetall key命令,能以field和value对的形式查看key对应的哈希类型数据。
hexists命令
通过hexists命令,能判断key和field对应的value是否存在。
hexists命令的格式:
hexists key field
对哈希类型数据的删除操作:
通过hdel命令,能删除key指定的field数据,该命令的格式如下:
hdel key field [field...]
通过该命令能同时删除一个key对应的多个field数据。如果要删除指定key所对应的整个哈希类型的数据,则需要用del key命令。
基本数据类型List
Redis list(列表)相当于Java语言中的LinkedList结构,是一个链表而非数组,其插入、删除元素的时间复杂度为O(1),但是查询速度欠佳,时间复杂度为O(n)。
当向列表中添加元素值时,首先需要给这个列表指定一个key键,然后使用相应的命令,从列表的左侧(头部)或者右侧(尾部)来添加元素,这些元素会以添加时的顺序排列。
当列表弹出(pop)最后一个元素时,该结构会被自动删除。
Redis列表的底层存储结构,其实是一个被称为快速链表(quicklist)的结构。
当列表中存储的元素较少时:
Redis 使用一块连续的内存来存储这些元素这个连续的结构被称为ziplist(压缩列表),它将所有的元素紧挨着一起存储。使用压缩列表来做列表的底层实现的情况:
(1) 当一个列表只包含少量列表项,并且每个列表项要么就是小整数值要么就是长度比较短的字符串,
(2)当一个哈希只包含少量键值对,比且每个键值对的键和值要么就是小整数值,要么就是长度比较短的字符串,
压缩列表是Redis为了节约内存而开发的,是由一系列特殊编码的连续内存块组成的顺序型(sequential))数据结构。
一个压缩列表可以包含任意多个节点(entry),每个节点可以保存一个字节数组或者一个整数值。
当列表中存储的元素较多时:
Redis列表用quicklist(快速链表)存储元素。
Redis之所以采用两种方法相结合的方式来存储元素,这是因为单独使用普通链表存储元素时,所需的空间较大,会造成存储空间的浪费。因此采用了链表和压缩列表相结合的方式,也就是quicklist+ziplist,结构如下图:
将多个ziplist使用双向指针串联起来,这样既能满足快速插入、删除的特性,又节省了一部分存储空间。
设置与获取:
可以通过lpush命令把一个或多个值依次且一次性插入到列表的头部,因此读取索引号是0的数据时,返回的是最后插入的数据。
key指定待插入的列表,element表示插入到列表的值。
lpush key element [element ...]
也可以用rpush命令在指定列表的尾部插入数据,该命令格式如下:
rpush key element [element ...]
可以通过lindex命令读取列表的值,key指定待读取的列表,index指定待读取列表值的索引号。注意,索引号是从0开始的。
lindex key index
示例:
模拟堆栈和队列:
lpush:向左边(list头)添加数据。rpush:向右边(list尾)添加数据
与之对应的有lpop和rpop命令,表示分别从Iist头和list尾读数据,而且读完会把该数据从列表里弹出(移除)。
通过如下的lpush和lpop命令能模拟"先入后出”的堆栈效果:
还可以通过lpush和rpop命令来模拟“先来先服务”的队列效果:
lrange:
通过lrange命令可以获取指定区间内的数据,start和stop分别表示开始索引和结束索引。
该命令的格式:
lrange key start stop
start <= stop时,才会取出数据。负数则表示倒数第几个。
获取列表包含的所有元素:
快捷方法:使用0作为起始索引、-1作为结束索引去调用lrange命令,这种方法非常适合于查看长度较短的列表。
列表中的最后一个元素索引为-1,依次类推。
修改列表中的元素:
通过lset命令能修改列表里的元素,能把由key指定的列表里index的数据修改为element。
该命令的格式:
lset key index element
将元素插入列表
通过使用linsert命令,用户可以将一个新元素插入列表某个指定元素的前面或者后面:
linsert list BEFORE|AFTER target_element new_element
linsert命令第二个参数的值可以是before或after,它们分别用于指示命令将新元素插入目标元素的前面或后面
获取列表的长度:
用户可以通过执行llen命令来获取列表的长度,即列表包含的元素数量:
llen list
移除指定的元素:
可以通过lrem命令删除列表里的指定元素,该命令的格式如下:
lrem key count element
当count = 0时,删除该列表里所有值是element的数据;
当count > 0时,删除从头到尾方向数量为count个、值是element的数据;(从左往右删除)
当count < 0时,删除从尾到头方向数量为count个、值是element的数据。(从右往左删除)
修剪列表:
ltrim命令接受一个列表和一个索引范围作为参数,并移出列表中位于给定索引范围之外的所有元素,只保留给定范围之内的元素:
LTRIM list start end
LTRM命令在执行完移除操作之后将返回OK作为结果。
基本数据类型Set
Set类型:
Redis的集合(set)键允许用户将任意多个各不相同的元素存储到集合中,这些元素既可以是文本数据,也可以是二进制数据。
虽然都可以存储多个元素,但集合与列表有以下两个明显的区别:
(1)列表可以存储重复元素,而集合只会存储非重复元素,尝试将一个已存在的元素添加到集合将被忽略。
(2)列表以有序方式存储元素,而集合则以无序方式存储元素。
这两个区别带来的差异主要跟命令的复杂度有关:
在执行像LINSERT和LREM这样的列表命令时,即使命令只针对单个列表元素,程序有时也不得不遍历整个列表以确定指定的元素是否存在,因此这些命令的复杂度都为O(N)。
对于集合来说,因为所有针对单个元素的集合命令都不需要遍历整个集合,所以复杂度都为O(1)。
因此当我们需要存储多个元素时,就可以考虑这些元素是否可以以无序的方式存储,并且是否不会出现重复。如果是,那么就可以使用集合来存储这些元素,从而有效地利用集合操作的效率优势。
将新元素添加到集合中:
使用sadd命令将一个或多个元素添加到集合中,命令格式如下:
sadd set element [element ...]
用sadd添加元素时,会自动忽略已存在的元素,只将不存在于集合的新元素添加到集合中。
集合中移除已有的元素
使用srem命令将一个或多个元素从集合中移除
srem set element [element ...]
这个命令会返回被移除的元素数量:
将指定的元素从一个集合移动到另一个集合
smove命令允许用户将指定的元素从源集合移动到目标集合:
smove source target element
smove命令在移动操作成功执行时返回1。如果指定的元素并不存在于源集合,那么smove命令将返回0,表示移动操作执行失败。
获取集合包含的所有元素
通过使用smembers命令,用户可以取得集合包含的所有元素:
smembers set
因为Redis集合以无序的方式存储元素,并且SMEMBERS命令在获取集合元素时也不会对元素进行任何排序动作,所以根据元素添加顺序的不同,2个包含相同元素的集合在执行SMEMBERS命令时的结果也可能会有所不同。
获取集合包含的元素数量
通过scard命令,用户可以获取给定集合的大小,即集合包含的元素数量:
scard set
检查给定元素是否存在于集合中
使用sismember命令:
sismember set element
从集合中随机地获取指定数量的元素:
通过使用SRANDMEMBER命令,用户可以从集合中随机地获取指定数量的元素。
SRANDMEMBER命令接受一个可选的count参数,用于指定用户想要获取的元素数量,如果用户没有给定这个参数,那么SRANDMEMBER命令默认只获取一个元素:
SRANDMEMBER set [count]
被SRANDMEMBER命令返回的元素仍然会存在于集合当中。
若count参数的值为正数,SRANDMEMBER命令将返回count个不重复的元素。
若count参数的值为负数,SRANDMEMBER命令将随机返回abs(count)个元素,并且在这些元素当中允许出现重复的元素。
随机从集合中移除指定数量的元素:
SPOP命令接受一个可选的cout参数,用于指定需要被移除的元素数量。如果用户没有给定这个参数,那么SPOP命令默认只移除一个元素:
spop key [count]
SPOP命令只接受正数count值,如果向SPOP命令提供负数count值将引发错误,因为负数count值对于SPOP命令是没有意义的
集合运算:
sinter命令可以计算用户给定的所有集合的交集,然后返回这个交集包含的所有元素:
sinter set [set..,]
sunion命令可以计算出用户给定的所有集合的并集,然后返回这个并集包含的所有元素:
sunion set [set..,]
sdiff命令可以计算出给定集合之间的差集,并返回差集包含的所有元素:
sdiff set [set..,]
有序集合:
Redis的有序集合(sorted set)同时具有“有序”和“集合”两种性质。
这种数据结构中的每个元素都由一个成员和一个与成员相关联的分值组成,其中成员以字符串方式存储,而分值则以64位双精度浮点数格式存储。
与集合一样,有序集合中的每个成员都是独一无二的,同一个有序集合中不会出现重复的成员。有序集合的成员将按照它们各自的分值大小进行排序:比如,分值为3.14的成员将小于分值为10.24的成员,而分值为999的成员也会小于分值为10086的成员。
有序集合的分值除了可以是数字之外,还可以是字符串" +inf "或者" -inf ",这两个特殊值分别用于表示无穷大和无穷小。
同一个有序集合不能存储相同的成员,但不同成员的分值却可以是相同的。
当两个或多个成员拥有相同的分值时,Rdis将按照这些成员在字典序中的大小对其进行排列:
举个例子,如果成员"apple"和成员"zero"都拥有相同的分值100,那么Redis将认为成员"apple"小于成员"zero",这是因为在字典序中,字母"a"开头的单词要小于字母"z"开头的单词。
有序集合是Redis提供的所有数据结构中最为灵活的一种,它可以以多种不同的方式获取数据,比如根据成员获取分值、根据分值获取成员、根据成员的排名获取成员、根据指定的分值范围获取多个成员等。
有序集合_添加或更新成员:
使用ZADD命令,用户可以向有序集合添加一个或多个新成员:
zadd sorted_set score member [score member ...]
ZADD命令除了可以向有序集合添加新成员之外,还可以对有序集合中已存在成员的分值进行更新:
在默认情况下,如果用户在执行ZADD命令时,给定成员已经存在于有序集合中,并且给定的分值和成员现有的分值并不相同,那么ZADD命令将使用给定的新分值去覆盖现有的旧分值。
有序集合_移除指定的成员:
通过使用ZREM命令,用户可以从有序集合中移除指定的一个或多个成员以及与这些成员相关联的分值:
zrem sorted_set member [member...]
通过使用ZSCORE命令,用户可以获取与给定成员相关联的分值:
zscore sorted_set member
有序集合_对成员分值执行自增或自减操作:
使用ZINCRBY命令,对有序集合中指定成员的分值执行自增操作,为其加上指定的增量。
zincrby sorted_set increment member
redis没有相应的自减操作,所以可以将一个负数增量传给ZINCRBY命令,达到对分值的自减操作。
有序集合_获取成员在有序集合中的排名:
排名标记是从0开始的。
ZRANK命令返回的是成员的升序排列排名,即成员在按照分值从小到大进行排列时的排名
ZRANK sorted_set member
ZREVRANK命令返回的则是成员的降序排列排名,即成员在按照分值从大到小进行排列时的排名。
ZREVRANK sorted_set member
有序集合_获取指定索引范围内的成员:
排名标记是从0开始的。
ZRANGE命令返回的是在索引范围内成员的升序排列排名,即成员在按照分值从小到大进行排列时的排名
zrange sorted_set start end
ZREVRANGE命令返回的则是在索引范围内成员的降序排列排名,即成员在按照分值从大到小进行排列时的排名。
zrevrange sorted_set start end
有序集合_获取指定分值范围内的成员:
ZRANGEBYSCORE以升序排列的方式获取有序集合中分值介于指定范围内的成员:
ZRANGEBYSCORE sorted_set min max
ZREVRANGEBYSCORE以降序排列的方式获取有序集合中分值介于指定范围内的成员:
ZREVRANGEBYSCORE sorted_set max min
命令的min参数和max参数分别用于指定用户想要获取的成员的最小分值和最大分值
ZRANGEBYSCORE命令和ZREVRANGEBYSCORE命令接受min参数和max参数的顺序正好相反。
有序集合_统计指定分值范围内的成员数量
使用ZCOUNT命令,可以统计出有序集合中分值介于指定范围之内的成员数量:
zcount sorted_set min max
范围控制:
ZRANGEBYSCORE、ZCOUNT等命令可以控制区间的开合。上述命令接受的分值范围都是闭区间分值范围。
如果用户想要定义的是开区间而不是闭区间,那么可以在给定分值范围时,在分值参数的前面加上一个单括号“(”,这样,具有给定分值的成员就不会出现在命令返回的结果当中。
min参数和max参数除了可以是普通的分值或者带有(符号的分值之外,还可以是特殊值+inf或者-inf。前者用于表示无穷大,而后者则用于表示无穷小。