1.学习目标
2.介绍与安装
2.1.Redis是什么?
Redis 是一个开源(BSD许可)的,内存中的数据结构存储系统,它可以用作数据库、缓存和消息中间件。 它支持多种类型的数据结构,如 字符串(strings), 散列(hashes), 列表(lists), 集合(sets), 有序集合(sorted sets) 与范围查询, bitmaps, hyperloglogs 和 地理空间(geospatial) 索引半径查询。 Redis 内置了 复制(replication),LUA脚本(Lua scripting), LRU驱动事件(LRU eviction),事务(transactions) 和不同级别的 磁盘持久化(persistence), 并通过 Redis哨兵(Sentinel)和自动 分区(Cluster)提供高可用性(high availability)。
注意:redis存储的数据一般为频繁访问但长时间不改变。
2.2.Redis的优势
- 性能极高 – Redis能读的速度是110000次/s,写的速度是81000次/s 。
- 丰富的数据类型 – Redis支持二进制案例的 Strings, Lists, Hashes, Sets 及 Ordered Sets 数据类型操作。
- 原子 – Redis的所有操作都是原子性的,意思就是要么成功执行要么失败完全不执行。单个操作是原子性的。多个操作也支持事务,即原子性,通过MULTI和EXEC指令包起来。
- 丰富的特性 – Redis还支持 publish/subscribe, 通知, key 过期等等特性。
2.3.Redis与其他key-value存储有什么不同?
- Redis有着更为复杂的数据结构并且提供对他们的原子性操作,这是一个不同于其他数据库的进化路径。Redis的数据类型都是基于基本数据结构的同时对程序员透明,无需进行额外的抽象。
- Redis运行在内存中但是可以持久化到磁盘,所以在对不同数据集进行高速读写时需要权衡内存,因为数据量不能大于硬件内存。在内存数据库方面的另一个优点是,相比在磁盘上相同的复杂的数据结构,在内存中操作起来非常简单,这样Redis可以做很多内部复杂性很强的事情。同时,在磁盘格式方面他们是紧凑的以追加的方式产生的,因为他们并不需要进行随机访问。
2.4.支持的数据类型
string、hash、list、set、sorted set
2.5.安装Redis
详情参考:https://www.runoob.com/redis/redis-conf.html
2.6.Redis配置
详情参考:https://www.runoob.com/redis/redis-conf.html
2.7.安装win可视化管理工具
gayhub:https://github.com/qishibo/AnotherRedisDesktopManager/releases
3.关系型数据库与非关系型数据库
3.1.关系型数据库
采用关系模型来组织数据的数据库,关系模型就是二维表格模型。一张二维表的表名就是关系,二维表中的一行就是—条记录,二维表中的一列就是一个字段。
优点
- 容易理解
- 使用方便,通角的sql语言
- 易于维护,丰富的完整性(实体完整性、参照完整性和用户定义的完整性)大大降低了数据冗余和数据不一致的概率
缺点
- 磁盘I/O是并发的瓶颈
- 海量数据查询效率低
- 横向扩展困难,无法简单的通过添加硬件和服务节点来扩展性能和负载能力,当需要对数据库进行升级和扩展时,需要停机维护和数据迁移
- 多表的关联查询以及复杂的数据分析类型的复杂sql查询,性能欠佳。因为要保证acid,必须按照三范式设计。
数据库
Orcale,Sql Server,MySql,DB2
3.2.非关系型数据库
非关系型,分布式,一般不保证遵循ACID原则的数据存储系统。键值对存储,结构不固定。
优点
- 根据需要添加字段,不需要多表联查。仅需id取出对应的value
- 适用于SNS(社会化网络服务软件。比如facebook,微博)。
- 严格上讲不是一种数据库,而是一种数据结构化存储方法的集合
缺点
- 只适合存储一些较为简单的数据·不合适复杂查询的数据
- 不合适持久存储海量数据
数据库
- K-V: Redis,Memcache
- 文档:MongoDB
- 搜索:Elasticsearch,Solr
- 可扩展性分布式:HBase
3.3.比较
内容 | 关系型数据库 | 非关系型数据库 |
---|---|---|
成本 | 有些需要收费(Orcale) | 甚本都是开源 |
查询数据 | 存储存于硬盘中,速度慢 | 数据存于缓存中,速度快 |
存储格式 | 只支持基础类型 | K-V文档,图片等 |
扩展性 | 有多表查询机制,扩展困难 | 数据之间没有耦合,容易扩展 |
持久性 | 适用持久存储。海量存储 | 不适用持久存储,海量存储 |
数据一致性 | 事务能力强,强调数据的强一致性 | 事务能力弱,强调数据的最终一致性 |
4.Redis-cli操作Redis
4.1.字符串(String)
Redis 字符串数据类型的相关命令用于管理 redis 字符串值
序号 | 命令 | 描述 |
---|---|---|
1 | set key value | 设置指定 key 的值 |
2 | get key | 获取指定 key 的值。 |
3 | getrange key start end | 返回 key 中字符串值的子字符 |
4 | getset key value | 将给定 key 的值设为 value ,并返回 key 的旧值(old value)。 |
5 | mset key value [key value ...] | 同时设置一个或多个 key-value 对。 |
6 | mget key1 [key2...] | 获取所有(一个或多个)给定 key 的值。 |
7 | setbit key offset value | 对 key 所储存的字符串值,设置或清除指定偏移量上的位(bit)。 |
8 | getbit key offset | 对 key 所储存的字符串值,获取指定偏移量上的位(bit)。 |
9 | setnx key value | 只有在 key 不存在时设置 key 的值。 |
10 | msetnx key value [key value ...] | 同时设置一个或多个 key-value 对,当且仅当所有给定 key 都不存在。 |
11 | setrange key offset value | 用 value 参数覆写给定 key 所储存的字符串值,从偏移量 offset 开始。 |
12 | strlen key | 返回 key 所储存的字符串值的长度。 |
13 | setex key seconds value | 将值 value 关联到 key ,并将 key 的过期时间设为 seconds (以秒为单位)。 |
14 | psetex key milliseconds value | 这个命令和 setex 命令相似,但它以毫秒为单位设置 key 的生存时间,而不是像 setex 命令那样,以秒为单位。 |
15 | incr key | 将 key 中储存的数字值增一。 |
16 | incrby key increment | 将 key 所储存的值加上给定的增量值(increment) 。 |
17 | incrbyfloat key increment | 将 key 所储存的值加上给定的浮点增量值(increment) 。 |
18 | decr key | 将 key 中储存的数字值减一。 |
19 | decrby key decrement | key 所储存的值减去给定的减量值(decrement) 。 |
20 | append key value | 如果 key 已经存在并且是一个字符串, append 命令将指定的 value 追加到该 key 原来值(value)的末尾。 |
栗子:
127.0.0.1:6379> set name aisen
OK
127.0.0.1:6379> get name
"aisen"
127.0.0.1:6379> getrange name 0 2
"ais"
127.0.0.1:6379> getrange name 0 -1
"aisen"
127.0.0.1:6379> getset name Aisen
"aisen"
127.0.0.1:6379> mset sex man age 21
OK
127.0.0.1:6379> mget name sex age
1) "Aisen"
2) "man"
3) "21"
127.0.0.1:6379> setbit bit 10086 1
(integer) 0
127.0.0.1:6379> getbit bit 10086
(integer) 1
127.0.0.1:6379> getbit bit 100
(integer) 0
127.0.0.1:6379> setnx name tom
(integer) 0
127.0.0.1:6379> setnx dog tom
(integer) 1
127.0.0.1:6379> get dog
"tom"
127.0.0.1:6379> msetnx cat json book loveletter
(integer) 1
127.0.0.1:6379> mget cat book
1) "json"
2) "loveletter"
127.0.0.1:6379> setrange cat 2 bo
(integer) 4
127.0.0.1:6379> get cat
"jsbo"
127.0.0.1:6379> strlen cat
(integer) 4
127.0.0.1:6379> setex boy 5 bobo
OK
127.0.0.1:6379> get boy
"bobo"
127.0.0.1:6379> get boy
(nil)
127.0.0.1:6379> get boy
(nil)
127.0.0.1:6379> psetex boy 5000 bobo
OK
127.0.0.1:6379> get boy
"bobo"
127.0.0.1:6379> get boy
(nil)
127.0.0.1:6379> incr age
(integer) 22
127.0.0.1:6379> incr age
(integer) 23
127.0.0.1:6379> incrby age 2
(integer) 25
127.0.0.1:6379> set age 21.0
OK
127.0.0.1:6379> incrbyfloat age 2
"23"
127.0.0.1:6379> incrbyfloat age 2.1
"25.10000000000000142"
127.0.0.1:6379> set age 21
OK
127.0.0.1:6379> decr age
(integer) 20
127.0.0.1:6379> decrby age 2
(integer) 18
127.0.0.1:6379> append dog op
(integer) 5
127.0.0.1:6379> get dog
"tomop"
4.2.哈希(Hash)
Redis hash 是一个 string 类型的 field(字段) 和 value(值) 的映射表,hash 特别适合用于存储对象。
Redis 中每个 hash 可以存储 2的32次方 - 1 键值对(40多亿)。
序号 | 命令 | 描述 |
---|---|---|
1 | hset key field value | 将哈希表 key 中的字段 field 的值设为 value 。 |
2 | hget key field | 获取存储在哈希表中指定字段的值。 |
3 | hmset key field1 value1 [field2 value2 ] | 同时将多个 field-value (域-值)对设置到哈希表 key 中。 |
4 | hmget key field1 [field2] | 获取所有给定字段的值 |
5 | hkeys key | 获取所有哈希表中的字段 |
6 | hvals key | 获取哈希表中所有值。 |
7 | hgetall key | 获取在哈希表中指定 key 的所有字段和值 |
8 | hdel key field1 [field2] | 删除一个或多个哈希表字段 |
9 | hexists key field | 查看哈希表 key 中,指定的字段是否存在。0不存在,1存在。 |
10 | hincrby key field increment | 为哈希表 key 中的指定字段的整数值加上增量 increment 。 |
11 | hincrbyfloat key field increment | 为哈希表 key 中的指定字段的浮点数值加上增量 increment 。 |
12 | hlen key | 获取哈希表中字段的数量 |
13 | hsetnx key field value | 只有在字段 field 不存在时,设置哈希表字段的值。 |
14 | hscan key cursor [MATCH pattern] [COUNT count] | 迭代哈希表中的键值对。cursor - 游标。pattern - 匹配的模式。count - 指定从数据集里返回多少元素,默认值为 10 。 |
栗子:
127.0.0.1:6379> hset human name aisen
(integer) 1
127.0.0.1:6379> hget human name
"aisen"
127.0.0.1:6379> hmset human sex man age 21
OK
127.0.0.1:6379> hmget human name sex age
1) "aisen"
2) "man"
3) "21"
127.0.0.1:6379> hkeys human
1) "name"
2) "sex"
3) "age"
127.0.0.1:6379> hvals human
1) "aisen"
2) "man"
3) "21"
127.0.0.1:6379> hgetall human
1) "name"
2) "aisen"
3) "sex"
4) "man"
5) "age"
6) "21"
127.0.0.1:6379> hdel human sex
(integer) 1
127.0.0.1:6379> hgetall human
1) "name"
2) "aisen"
3) "age"
4) "21"
127.0.0.1:6379> hexists human sex
(integer) 0
127.0.0.1:6379> hexists human name
(integer) 1
127.0.0.1:6379> hincrby human age 1
(integer) 22
127.0.0.1:6379> hincrbyfloat human age 1
"23"
127.0.0.1:6379> hincrbyfloat human age 1.2
"24.19999999999999929"
127.0.0.1:6379> hlen human
(integer) 2
127.0.0.1:6379> hsetnx human sex man
(integer) 1
127.0.0.1:6379> hsetnx human sex man
(integer) 0
127.0.0.1:6379> hgetall human
1) "name"
2) "aisen"
3) "age"
4) "24.19999999999999929"
5) "sex"
6) "man"
127.0.0.1:6379> hscan human 0 match "na*"
1) "0"
2) 1) "name"
2) "aisen"
4.3.列表(List)
Redis列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边)。一个列表最多可以包含 232 - 1 个元素 (4294967295, 每个列表超过40亿个元素)。
序号 | 命令 | 描述 |
---|---|---|
1 | lpush key value1 [value2] | 将一个或多个值插入到列表头部 |
2 | lrange key start stop | 获取列表指定范围内的元素 |
3 | lpop key | 移出并获取列表的第一个元素 |
4 | rpush key value1 [value2] | 在列表中添加一个或多个值 |
5 | rpop key | 移除列表的最后一个元素,返回值为移除的元素。 |
6 | llen key | 获取列表长度 |
7 | lindex key index | 通过索引获取列表中的元素 |
8 | lset key index value | 通过索引设置列表元素的值 |
9 | linsert key before|after pivot value | 在列表的元素前或者后插入元素 |
10 | lrem key count value | 移除列表元素 |
11 | lpushx key value | 将一个值插入到已存在的列表头部 |
12 | rpushx key value | 将一个值插入到已存在的列表尾部 |
13 | ltrim key start stop | 对一个列表进行修剪(trim),就是说,让列表只保留指定区间内的元素,不在指定区间之内的元素都将被删除。 |
14 | brpop key1 [key2 ] timeout | 移出并获取列表的最后一个元素, 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。如果列表为空,返回一个 nil 。 否则,返回一个含有两个元素的列表,第一个元素是被弹出元素所属的 key ,第二个元素是被弹出元素的值。 |
15 | blpop key1 [key2 ] timeout | 移出并获取列表的第一个元素, 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。 |
16 | brpoplpush source destination timeout | 从source列表中弹出一个值,将弹出的元素插入到destination列表中并返回它; 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。 |
17 | rpoplpush source destination | 移除列表的最后一个元素,并将该元素添加到另一个列表并返回 |
栗子:
127.0.0.1:6379> lpush ls 1
(integer) 1
127.0.0.1:6379> lpush ls 2 3
(integer) 3
127.0.0.1:6379> lrange ls 0 -1
1) "3"
2) "2"
3) "1"
127.0.0.1:6379> lpop ls
"3"
127.0.0.1:6379> lrange ls 0 -1
1) "2"
2) "1"
127.0.0.1:6379> rpush ls 3 4
(integer) 4
127.0.0.1:6379> lrange ls 0 -1
1) "2"
2) "1"
3) "3"
4) "4"
127.0.0.1:6379> rpop ls
"4"
127.0.0.1:6379> lrange ls 0 -1
1) "2"
2) "1"
3) "3"
127.0.0.1:6379> llen ls
(integer) 3
127.0.0.1:6379> lindex ls 0
"2"
127.0.0.1:6379> lset ls 0 0
OK
127.0.0.1:6379> lrange ls 0 -1
1) "0"
2) "1"
3) "3"
127.0.0.1:6379> linsert ls after 1 2
(integer) 4
127.0.0.1:6379> lrange ls 0 -1
1) "0"
2) "1"
3) "2"
4) "3"
127.0.0.1:6379> linsert ls after 3 4
(integer) 5
127.0.0.1:6379> lrange ls 0 -1
1) "0"
2) "1"
3) "2"
4) "3"
5) "4"
127.0.0.1:6379> rpush ls a
(integer) 6
127.0.0.1:6379> linsert ls after a b
(integer) 7
127.0.0.1:6379> lrange ls 0 -1
1) "0"
2) "1"
3) "2"
4) "3"
5) "4"
6) "a"
7) "b"
127.0.0.1:6379> lrem ls 1 0
(integer) 1
127.0.0.1:6379> lrange ls 0 -1
1) "1"
2) "2"
3) "3"
4) "4"
5) "a"
6) "b"
127.0.0.1:6379> lpushx ls c
(integer) 7
127.0.0.1:6379> lrange ls 0 -1
1) "c"
2) "1"
3) "2"
4) "3"
5) "4"
6) "a"
7) "b"
127.0.0.1:6379> rpushx ls e
(integer) 8
127.0.0.1:6379> lrange ls 0 -1
1) "c"
2) "1"
3) "2"
4) "3"
5) "4"
6) "a"
7) "b"
8) "e"
127.0.0.1:6379> ltrim ls 0 4
OK
127.0.0.1:6379> lrange ls 0 -1
1) "c"
2) "1"
3) "2"
4) "3"
5) "4"
127.0.0.1:6379> brpop ls 100
1) "ls"
2) "4"
127.0.0.1:6379> rpush ls2 a
(integer) 1
127.0.0.1:6379> brpoplpush ls ls2 100
"3"
127.0.0.1:6379> lrange ls2 0 -1
1) "3"
2) "a"
4.4.集合(Set)
Redis 的 Set 是 String 类型的无序集合。集合成员是唯一的,这就意味着集合中不能出现重复的数据。
Redis 中集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是 O(1)。
集合中最大的成员数为 232 - 1 (4294967295, 每个集合可存储40多亿个成员)。
下表列出了 Redis 集合基本命令:
序号 | 命令 | 描述 |
---|---|---|
1 | sadd key member1 [member2] | 向集合添加一个或多个成员 |
2 | smembers key | 返回集合中的所有成员 |
3 | scard key | 获取集合的成员数 |
4 | srem key member1 [member2] | 移除集合中一个或多个成员 |
5 | sismember key member | 判断 member 元素是否是集合 key 的成员 |
6 | spop key | 移除并返回集合中的一个随机元素 |
7 | srandmember key [count] | 返回集合中一个或多个随机数 |
8 | sdiff key1 [key2] | 返回第一个集合与其他集合之间的差异。 |
9 | sdiffstore destination key1 [key2] | 返回给定所有集合的差集并存储在 destination 中,会覆盖destination |
10 | sinter key1 [key2] | 返回给定所有集合的交集 |
11 | sinterstore destination key1 [key2] | 返回给定所有集合的交集并存储在 destination 中,会覆盖destination |
12 | smove source destination member | 将 member 元素从 source 集合移动到 destination 集合 |
13 | sunion key1 [key2] | 返回所有给定集合的并集 |
14 | sunionstore destination key1 [key2] | 所有给定集合的并集存储在 destination 集合中,会覆盖destination |
15 | sscan key cursor [MATCH pattern] [COUNT count] | 迭代集合中的元素 |
栗子:
127.0.0.1:6379> sadd set aaa bbb ccc
(integer) 3
127.0.0.1:6379> smembers set
1) "ccc"
2) "aaa"
3) "bbb"
127.0.0.1:6379> scard set
(integer) 3
127.0.0.1:6379> srem set aaa bbb
(integer) 2
127.0.0.1:6379> smembers set
1) "ccc"
127.0.0.1:6379> sismember set ccc
(integer) 1
127.0.0.1:6379> sismember set aaa
(integer) 0
127.0.0.1:6379> spop set
"ccc"
127.0.0.1:6379> sadd set aaa bbb ccc
(integer) 3
127.0.0.1:6379> srandmember set
"bbb"
127.0.0.1:6379> srandmember set
"bbb"
127.0.0.1:6379> srandmember set
"aaa"
127.0.0.1:6379> sadd set2 aaa eee fff
(integer) 3
127.0.0.1:6379> sdiff set set2
1) "ccc"
2) "bbb"
127.0.0.1:6379> sdiffstore set3 set set2
(integer) 2
127.0.0.1:6379> smembers set3
1) "ccc"
2) "bbb"
127.0.0.1:6379> sinter set set2
1) "aaa"
127.0.0.1:6379> sinterstore set3 set set2
(integer) 1
127.0.0.1:6379> smembers set3
1) "aaa"
127.0.0.1:6379> smove set3 set2 aaa
(integer) 1
127.0.0.1:6379> smembers set2
1) "fff"
2) "eee"
3) "aaa"
127.0.0.1:6379> sunion set set2
1) "aaa"
2) "bbb"
3) "ccc"
4) "fff"
5) "eee"
127.0.0.1:6379> sunionstore set3 set set2
(integer) 5
127.0.0.1:6379> smembers set3
1) "aaa"
2) "bbb"
3) "ccc"
4) "fff"
5) "eee"
127.0.0.1:6379> sscan set3 0 match "a*"
1) "0"
2) 1) "aaa"
4.5.有序集合(Sorted set)
Redis 有序集合和集合一样也是 string 类型元素的集合,且不允许重复的成员。
不同的是每个元素都会关联一个 double 类型的分数。redis 正是通过分数来为集合中的成员进行从小到大的排序。
有序集合的成员是唯一的,但分数(score)却可以重复。
集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是 O(1)。 集合中最大的成员数为 232 - 1 (4294967295, 每个集合可存储40多亿个成员)。
下表列出了 redis 有序集合的基本命令:
序号 | 命令 | 描述 |
---|---|---|
1 | zdd key score1 member1 [score2 member2] | 向有序集合添加一个或多个成员,或者更新已存在成员的分数 |
2 | zcard key | 获取有序集合的成员数 |
3 | zrange key start stop [withscores] | 通过索引区间返回有序集合指定区间内的成员 |
4 | zrank key member | 返回有序集合中指定成员的索引 |
5 | zcount key min max | 计算在有序集合中指定区间分数的成员数 |
6 | zincrby key increment member | 有序集合中对指定成员的分数加上增量 increment |
7 | zinterstore destination numkeys key [key ...] | 计算给定的一个或多个有序集的交集并将结果集存储在新的有序集合 destination 中 |
8 | zlexcount key min max | 在有序集合中计算指定字典区间内成员数量 |
9 | zrangebylex key min max [LIMIT offset count] | 通过字典区间返回有序集合的成员 |
10 | zrangebyscore key min max [withscores] [LIMIT] | 通过分数返回有序集合指定区间内的成员 |
11 | zrem key member [member ...] | 移除有序集合中的一个或多个成员 |
12 | zremrangebylex key min max | 移除有序集合中给定的字典区间的所有成员 |
13 | zremrangebyrank key start stop | 移除有序集合中给定的排名区间的所有成员 |
14 | zremrangebyscore key min max | 移除有序集合中给定的分数区间的所有成员 |
15 | zrevrange key start stop [] | 返回有序集中指定区间内的成员,通过索引,分数从高到低 |
16 | zrevrangebyscore key max min [withscores] | 返回有序集中指定分数区间内的成员,分数从高到低排序 |
17 | zrevrank key member | 返回有序集合中指定成员的排名,有序集成员按分数值递减(从大到小)排序 |
18 | zscore key member | 返回有序集中,成员的分数值 |
19 | zunionstore destination numkeys key [key ...] | 计算给定的一个或多个有序集的并集,并存储在新的 key 中 |
20 | zscan key cursor [MATCH pattern] [COUNT count] | 迭代有序集合中的元素(包括元素成员和元素分值) |
栗子:
127.0.0.1:6379> zadd sset 10 a 9 b 8 c 7 d
(integer) 4
127.0.0.1:6379> zrange sset 0 -1
1) "d"
2) "c"
3) "b"
4) "a"
127.0.0.1:6379> zcard sset
(integer) 4
127.0.0.1:6379> zrem sset a
(integer) 1
127.0.0.1:6379> zrange sset 0 -1
1) "d"
2) "c"
3) "b"
127.0.0.1:6379> zrank sset d
(integer) 0
4.6.Redis中以层级关系、目录形式存储数据
数据更加清晰明了
127.0.0.1:6379> set cart:user01:item01 apple
OK
127.0.0.1:6379> get cart:user01:item01
"apple"
127.0.0.1:6379> keys *
1) "set3"
2) "bit"
3) "age"
4) "ls2"
5) "cart:user01:item01"
6) "name"
7) "book"
8) "dog"
9) "human"
10) "ls"
11) "set2"
12) "set"
13) "sex"
14) "sset"
15) "cat"
4.7.设置key失效时间
- expire , set key value :设置key ttl秒后失效
- pexpire ,set key value :设置key ttl毫秒后失效
- expire ,set key value :设置key timestamp秒级(十位)失效
- pexpire ,set key value :设置key timestamp毫秒级(十三位)失效
ttl:获取的值为-1说明key没有设置有效期,当值为-2说明过了有效期
127.0.0.1:6379> set name aisen ex 5
OK
127.0.0.1:6379> ttl name
(integer) 3
127.0.0.1:6379> ttl name
(integer) 2
127.0.0.1:6379> ttl name
(integer) -2
127.0.0.1:6379> get name
(nil)
127.0.0.1:6379> set name aisen
OK
127.0.0.1:6379> ttl name
(integer) -1
127.0.0.1:6379> expire name 5
(integer) 1
127.0.0.1:6379> ttl name
(integer) -2
nx: 必须不存在的key
xx: 必须已经存在的key
127.0.0.1:6379> set name aisen ex 5 nx
OK
127.0.0.1:6379> ttl name
(integer) -2
127.0.0.1:6379> set name aisen ex 5 nx
OK
127.0.0.1:6379> ttl name
(integer) 3
127.0.0.1:6379> set name aisen ex 5 xx
(nil)
127.0.0.1:6379> set name aisen
OK
127.0.0.1:6379> set name aisen ex 5 xx
OK
127.0.0.1:6379> ttl name
(integer) -2
4.8.通用删除
del key
127.0.0.1:6379> keys *
1) "set3"
2) "bit"
3) "age"
4) "ls2"
5) "cart:user01:item01"
6) "book"
7) "dog"
8) "human"
9) "ls"
10) "set2"
11) "set"
12) "sex"
13) "sset"
14) "cat"
127.0.0.1:6379> del human ls set sset
(integer) 4
127.0.0.1:6379> keys *
1) "set3"
2) "bit"
3) "age"
4) "ls2"
5) "cart:user01:item01"
6) "book"
7) "dog"
8) "set2"
9) "sex"
10) "cat"
127.0.0.1:6379>
5.Java操作Redis
5.1.创建项目
5.2.依赖引入,相关配置
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.5</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>cn.aisencode</groupId>
<artifactId>redisdemo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>redisdemo</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!--spring redis 组件-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<!--
1.x 的版本默认采用的连接池技术是Jedis
2.0 以上默认连接池是Lettuce
如果采用Jedis,需要排除Lettuce的依赖
使用jedis:当多线程使用同一个连接时,是线程不安全的。所以要使用连接池,为每个jedis实例分配一个连接。
使用Lettuce:当多线程使用同一连接实例时,是线程安全的。
-->
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- jedis 依赖-->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
<!--spring web 组件-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--spring test 组件-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
application.yml
spring:
redis:
host: 127.0.0.1
port: 6379
database: 0 # 选择哪个库,默认0库
timeout: 10000ms
# 服务器密码
password: root
jedis:
pool:
# 最大连接数,默认 8
max-active: 1024
# 最大连接阻塞等待时间,单位毫秒,默认 -1
max-wait: 10000ms
# 最大空闲连接,默认 8
max-idle: 200
# 最小空闲连接
min-idle: 5
5.3.初识Jedis操作Redis
在test测试类中
package cn.aisencode.redisdemo;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import redis.clients.jedis.Jedis;
@SpringBootTest
class RedisdemoApplicationTests {
@Test
public void getConnt() {
//创建jedis对象
Jedis jedis = new Jedis("127.0.0.1",6379);
//设置密码
//jedis.auth("root");
//指定库,默认0
jedis.select(1);
//测试连接
String result = jedis.ping();
System.out.println(result);
//添加数据
jedis.set("name","Aisen");
//获取数据
String name = jedis.get("name");
System.out.println(name);
//关闭连接
if (jedis != null) {
jedis.close();
}
}
}
5.4.初识Jedis连接池
/**
* 初识jedis 连接池
*/
@Test
public void getConnt02() {
//初始化jedis连接池
JedisPool jedisPool = new JedisPool(new JedisPoolConfig(), "127.0.0.1", 6379, 1000);
//获取连接对象
Jedis jedis = jedisPool.getResource();
//指定连接库
jedis.select(2);
//测试连接
String result = jedis.ping();
System.out.println(result);
//添加数据
jedis.set("name","Aisen");
//获取数据
String name = jedis.get("name");
System.out.println(name);
//关闭连接
if (jedis != null) {
jedis.close();
}
}
5.5.封装Jedis连接池
5.5.1.将JedisPool注入Spring
package cn.aisencode.redisdemo.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
/**
* @author Aisen
* @time 2021.5.25 16:21
*/
@Configuration
public class RedisConfig {
//服务器地址
@Value("${spring.redis.host}")
private String host;
//服务器端口
@Value("${spring.redis.port}")
private int port;
//服务器密码
@Value("${spring.redis.password}")
private String password;
//连接超时时间
@Value("${spring.redis.timeout}")
private String timeout;
//连接库
@Value("${spring.redis.database}")
private int database;
//最大连接数
@Value("${spring.redis.jedis.pool.max-active}")
private int maxActive;
//最大连接阻塞时间
@Value("${spring.redis.jedis.pool.max-wait}")
private String maxWait;
//最大空闲连接
@Value("${spring.redis.jedis.pool.max-idle}")
private int maxIdle;
//最小空闲连接
@Value("${spring.redis.jedis.pool.min-idle}")
private int minIdle;
@Bean
public JedisPool getJedisPool() {
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
//最大连接数
jedisPoolConfig.setMaxTotal(maxActive);
//最大空闲连接
jedisPoolConfig.setMaxIdle(maxIdle);
//最小空闲连接
jedisPoolConfig.setMinIdle(maxIdle);
//最大连接阻塞时间
Long maxWaitMillis = Long.valueOf(maxWait.substring(0, maxWait.length() - 2));
jedisPoolConfig.setMaxWaitMillis(maxWaitMillis);
//连接池对象
JedisPool jedisPool = new JedisPool(jedisPoolConfig,host,port,Integer.valueOf(timeout.substring(0,timeout.length() - 2)));
return jedisPool;
}
}
5.5.2.创建工具类
package cn.aisencode.redisdemo.utils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import javax.annotation.PostConstruct;
/**
* @author Aisen
* @time 2021.5.25 16:53
*/
//1.加上@Component
@Component
public class JedisPoolUtils {
@Autowired
private JedisPool testJedisPool;
private static JedisPool jedisPool;
//2.使用@PostConstruct注解方法,将注入的bean赋给静态bean中
@PostConstruct
public void init(){
jedisPool = this.testJedisPool;
}
/**
* 获取jedis实例对象
* @return
*/
public static Jedis getJedis() {
//3.通过instance. 的形式调用
return jedisPool.getResource();
}
}
5.5.3.测试类
/**
* 测试JedisPoolUtils
*/
@Test
public void useJedisPoolUtils() {
//获取jedis实例
Jedis jedis = JedisPoolUtils.getJedis();
//测试连接
String result = jedis.ping();
System.out.println(result);
//添加数据
jedis.set("name","Aisen");
//获取数据
String name = jedis.get("name");
System.out.println(name);
//关闭连接
if (jedis != null) {
jedis.close();
}
}
5.6.Jedis操作String
/**
* Jedis操作String
*/
@Test
public void testString() {
//获取jedis实例
Jedis jedis = JedisPoolUtils.getJedis();
//测试连接
String result = jedis.ping();
System.out.println(result);
//添加一条数据
jedis.set("name","Aisen");
//获取一条数据
String name = jedis.get("name");
System.out.println(name);
//添加多条数据
jedis.mset("sex","man","age","21");
//获取多条数据
List<String> ls = jedis.mget("name", "sex", "age");
System.out.println(ls);
//删除key
jedis.del("sex");
//获取所有key
Set<String> keys = jedis.keys("*");
System.out.println(keys);
//关闭连接
if (jedis != null) {
jedis.close();
}
}
5.7.Jedis操作Hash
/**
* Jedis操作Hash
*/
@Test
public void testHash() {
//获取jedis实例
Jedis jedis = JedisPoolUtils.getJedis();
//测试连接
String result = jedis.ping();
System.out.println(result);
//添加一条数据
jedis.hset("user","name","Aisen");
//获取一条数据
String name = jedis.hget("user","name");
System.out.println(name);
//添加多条数据
Map<String,String> map = new HashMap<>();
map.put("sex","man");
map.put("age","21");
jedis.hmset("user",map);
//获取多条数据
List<String> ls = jedis.hmget("user", "sex", "age");
System.out.println(ls);
//获取整个
Map<String, String> user = jedis.hgetAll("user");
user.entrySet().forEach(e-> System.out.println(e.getKey()+":"+e.getValue()));
//删除
jedis.hdel("user","age");
//获取所有key
Set<String> keys = jedis.keys("*");
System.out.println(keys);
//关闭连接
if (jedis != null) {
jedis.close();
}
}
5.8.Jedis操作List
/**
* Jedis操作list
*/
@Test
public void testList() {
//获取jedis实例
Jedis jedis = JedisPoolUtils.getJedis();
//测试连接
String result = jedis.ping();
System.out.println(result);
//左添加
jedis.lpush("students","zhangsan","lisi");
//右添加
jedis.rpush("students","wangwu","zhouliu");
//获取数据
List<String> students = jedis.lrange("students", 0, -1);
System.out.println(students);
//获取条数
Long llen = jedis.llen("students");
System.out.println(llen);
//删除数据
jedis.lrem("students",1,"wangwu");
//左弹出
String lpop = jedis.lpop("students");
System.out.println(lpop);
//右弹出
String rpop = jedis.rpop("students");
System.out.println(rpop);
//获取数据
List<String> student = jedis.lrange("students", 0, -1);
System.out.println(student);
//关闭连接
if (jedis != null) {
jedis.close();
}
}
5.9.Jedis操作Set
/**
* jedis操作set
*/
@Test
public void testSet() {
//获取jedis实例
Jedis jedis = JedisPoolUtils.getJedis();
//测试连接
String result = jedis.ping();
System.out.println(result);
//添加
jedis.sadd("words","aaa","bbb","ccc","ddd","eee");
//获取key下所有数据
Set<String> words = jedis.smembers("words");
words.forEach(System.out::println);
//获取总条数
Long scard = jedis.scard("words");
System.out.println(scard);
//删除
jedis.srem("words","aaa","bbb");
//关闭连接
if (jedis != null) {
jedis.close();
}
}
5.10.Jedis操作Sorted Set
/**
* Jedis操作Sorted Set
*/
@Test
public void testSortedSet() {
//获取jedis实例
Jedis jedis = JedisPoolUtils.getJedis();
//测试连接
String result = jedis.ping();
System.out.println(result);
//添加,注意score作为value
Map<String,Double> map = new HashMap<>();
map.put("zhangsan",7D);
map.put("lisi",4D);
map.put("wangwu",9D);
jedis.zadd("human",map);
//获取数据
Set<String> human = jedis.zrange("human", 0, -1);
human.forEach(System.out::println);
//获取总条数
Long zcard = jedis.zcard("human");
System.out.println(zcard);
//删除
jedis.zrem("human","zhangsan","lisi");
//关闭连接
if (jedis != null) {
jedis.close();
}
5.11.Jedis层级目录
/**
* jedis层级目录
*/
@Test
public void testDir() {
//获取jedis实例
Jedis jedis = JedisPoolUtils.getJedis();
//测试连接
String result = jedis.ping();
System.out.println(result);
jedis.set("card:user01:item01","apple");
System.out.println(jedis.get("card:user01:item01"));
//关闭连接
if (jedis != null) {
jedis.close();
}
}
5.12.Jedis设置失效时间
/**
* jedis设置失效时间
*/
@Test
public void testExpire() {
//获取jedis实例
Jedis jedis = JedisPoolUtils.getJedis();
//测试连接
String result = jedis.ping();
System.out.println(result);
//给存在的key设置失效时间
jedis.set("code","test");
//设置失效时间,单位秒
jedis.expire("code",5);
//设置失效时间,单位毫秒
jedis.pexpire("code",5000);
//查看失效时间,单位秒,-1为不失效,-2位已经失效
Long ttl = jedis.ttl("code");
System.out.println(ttl);
//设置不存在的key
//设置失效时间,单位秒
jedis.setex("test",5,"test");
//设置失效时间,单位毫秒
jedis.psetex("test2",5,"test2");
//查看失效时间,单位毫秒
Long pttl = jedis.pttl("test2");
System.out.println(pttl);
//nx,xx的用法
SetParams setParams = new SetParams();
//不存才key,时才能设置成功
setParams.nx();
//存在key,才能设置成功
//setParams.xx();
//设置失效时间,秒
setParams.ex(5);
//设置失效时间,毫秒
//setParams.px(5000);
jedis.set("test3","test3",setParams);
}
5.13.Jedis查询所有Key
/**
* 查询所有key
*/
@Test
public void testAllKey() {
//获取jedis实例
Jedis jedis = JedisPoolUtils.getJedis();
//测试连接
String result = jedis.ping();
System.out.println(result);
//获取key的数量
Long size = jedis.dbSize();
System.out.println(size);
//查询所有key
Set<String> keys = jedis.keys("*");
keys.forEach(System.out::println);
//关闭连接
if (jedis != null) {
jedis.close();
}
}
5.14.Jedis事务
redis的事务弱,一般不用。若操作三张表,前面两操作成功,后面一张操作失败,事务回滚只会回滚第三张表
/**
* 事务
*/
@Test
public void testMulti() {
//获取jedis实例
Jedis jedis = JedisPoolUtils.getJedis();
//测试连接
String result = jedis.ping();
System.out.println(result);
//开启事务
Transaction ts = jedis.multi();
ts.set("tel","10086");
//提交事务
ts.exec();
//回滚事务
//ts.discard();
//关闭连接
if (jedis != null) {
jedis.close();
}
}
5.15.Jedis操作数组
一般用于对象的序列化存储。
5.15.1.序列化工具类
package cn.aisencode.redisdemo.util;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
/**
*
* 序列化工具类
*/
public class SerializingUtil {
/**
* 功能简述: 对实体Bean进行序列化操作.
* @param source 待转换的实体
* @return 转换之后的字节数组
* @throws Exception
*/
public static byte[] serialize(Object source) {
if (source == null) {
return null;
}
ByteArrayOutputStream byteOut = null;
ObjectOutputStream ObjOut = null;
try {
byteOut = new ByteArrayOutputStream();
ObjOut = new ObjectOutputStream(byteOut);
ObjOut.writeObject(source);
ObjOut.flush();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (null != ObjOut) {
ObjOut.close();
}
} catch (IOException e) {
ObjOut = null;
}
}
return byteOut.toByteArray();
}
/**
* 功能简述: 将字节数组反序列化为实体Bean.
* @param source 需要进行反序列化的字节数组
* @return 反序列化后的实体Bean
* @throws Exception
*/
public static Object deserialize(byte[] source) {
if (source == null) {
return null;
}
ObjectInputStream ObjIn = null;
Object retVal = null;
try {
ByteArrayInputStream byteIn = new ByteArrayInputStream(source);
ObjIn = new ObjectInputStream(byteIn);
retVal = ObjIn.readObject();
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (null != ObjIn) {
ObjIn.close();
}
} catch (IOException e) {
ObjIn = null;
}
}
return retVal;
}
}
5.15.2.实体类
package cn.aisencode.redisdemo.pojo;
import java.io.Serializable;
/**
* @author Aisen
* @time 2021.5.26 09:29
*/
//实现序列化接口
public class User implements Serializable {
//自定义serialVersionUID 实现版本兼容
private static final long serialVersionUID = 7570604755630016196L;
private Integer id;
private String username;
private String password;
public User() {
}
public User(Integer id, String username, String password) {
this.id = id;
this.username = username;
this.password = password;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", username='" + username + '\'' +
", password='" + password + '\'' +
'}';
}
}
5.15.3.操作数组
/**
* 操作数组
*/
@Test
public void testByte() {
//获取jedis实例
Jedis jedis = JedisPoolUtil.getJedis();
//测试连接
String result = jedis.ping();
System.out.println(result);
//初始化对象
User user = new User(1,"zhangsan","123456");
//序列化对象为byte数组
byte[] userKey = SerializingUtil.serialize("user:" + user.getId());
byte[] userValue = SerializingUtil.serialize(user);
//存入redis
jedis.set(userKey,userValue);
//反序列化
byte[] bytes = jedis.get(userKey);
User user1= (User) SerializingUtil.deserialize(bytes);
System.out.println(user1);
//关闭连接
if (jedis != null) {
jedis.close();
}
}
6.Redis持久化方案
6.1.bgsave手动存储
优点:简单,一个命令bgsave
缺点:频繁输入
6.2.rdb自动存储
redis自带方案,相关配置在配置文件中(redis.windows.conf、redis.conf)。
表示会存储在当前文件同级目录下的dump.rdb中。
自动保存配置为900秒至少有一个key改变,就会存储。300秒至少有10个key发生改变就会存储。60秒至少有一万个key改变就会存储。
优点:省心
缺点:没有达到存储条件,还是可能丢失数据
6.3.aof方案
每次redis的操作命令都会记录到aof文件中,下次redis启用时会运行里面的命令,从而保证数据完整。
优点:实时,数据完整。
缺点:文件会越来越大,初始化会越来越慢。
开启:配置文件中,将no改为yes并保存,然后重启redis,前面的rdb方案就会自动失效,启用aof。
建议rdb和aof配合使用。
7.Redis linxu搭建主从复用
7.1.主从的概念
概述
主(master)和 从(slave)部署在不同的服务器上,当主节点服务器写入数据时会同步到从节点的服务器上,一般主节点负责写入数据,从节点负责读取数据
优点
- 读写分离,提高效率
- 数据热备份,提供多个副本
缺点
- 主节点故障,集群则无法进行工作,可用性比较低,从节点升主节点需要人工手动干预
- 单点容易造成性能低下
- 主节点的存储能力受到限制
- 主节点的写受到限制(只有一个主节点)
- 全量同步可能会造成毫秒或者秒级的卡顿现象
注意
集群中宕机大于50%,集群为不可用。所以集群服务器的数量为奇数台,节约成本。
7.2.读写分离
通过过本地端口,模拟集群搭建。
- 新建三个文件夹,分别放子服务器的数据,日志,配置文件。
- 复制主配置文件到/opt/redis/conf配置目录下
- 提取公共配置文件,修改redis.conf 为redis-common.conf
- 修改共用配置,设置所有ip都可以访问
- 保护模式关闭,端口号注释,在每个从服务器的单独配置文件中设置
- 后台启动开启
- 进程文件注释,单独配置
- 日志注释,单独配置
- 使用默认的rdb方案,存储文件名注释,单独配置,文件地址目录修改
- 主服务器设置了密码,从服务器访问输入密码
- 默认开启只读,不需要修改
- 访问密码配置
保存并退出:wq
- 新建三个从服务器单独配置文件
- 修改redis.-6379.conf,6379作为主服务器,最后一句注释掉。
- 其他两个同理
- 启动集群
- 模拟三台服务器,启动redis-cli
- 查看主从状态
主:
从:
- 测试主节点设置值,从节点获取
注意:从节点不能写,没有开启
7.3.哨兵配置
概述
哨兵节点(奇数)监控集群,当主节点宕机,会选举最合适的从节点担任主节点,当前主节点好了,会成为从节点。提高集群高可用性。
搭建
哨兵配置文件,sentinel.conf
- 复制sentinel.conf 到配置文件目录,并改名为sentinel-common.conf
- 端口注释,后台启动开启,进程文件注释,日志文件注释
- 哨兵 监控 主节点ip 端口 ,2代表我们有3台服务器大于50%为2
- 设置主服务器密码
- 哨兵连接主节点超时时间,超时就会启用选举,默认30秒
- 选举超时时间,超时就会终止本次选举
:wq保存并退出
- 新建私有哨兵配置文件
- 配置三个
测试
启用哨兵
查看哨兵日志 tail -f
重启6379
6381依然是主节点,6379是从节点
8.SpringDataRedis
8.1.新建项目
8.2.添加依赖修改配置文件
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.0</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>cn.aisencode</groupId>
<artifactId>springdataredisdemo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>springdataredisdemo</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!--spring-data-redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<!--
1.x 的版本默认采用的连接池技术是Jedis
2.0 以上默认连接池是Lettuce
如果采用Jedis,需要排除Lettuce的依赖
使用jedis:当多线程使用同一个连接时,是线程不安全的。所以要使用连接池,为每个jedis实例分配一个连接。
使用Lettuce:当多线程使用同一连接实例时,是线程安全的。
这里使用Lettuce
-->
</dependency>
<!--commons-pool2 对象池依赖-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
application.yml
spring:
redis:
host: 127.0.0.1
port: 6379
# 选择哪个库,默认0库
database: 0
timeout: 10000ms
# 服务器密码
password: root
lettuce:
pool:
# 最大连接数,默认 8
max-active: 1024
# 最大连接阻塞等待时间,单位毫秒,默认 -1
max-wait: 10000ms
# 最大空闲连接,默认 8
max-idle: 200
# 最小空闲连接
min-idle: 5
8.3.测试类
package cn.aisencode.springdataredisdemo;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
@SpringBootTest
class SpringdataredisdemoApplicationTests {
//不加泛型默认Object,会以二进制存储
@Autowired
private RedisTemplate<String,String> redisTemplate;
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Test
public void initConn() {
//1.redisTemplate
ValueOperations ops = redisTemplate.opsForValue();
ops.set("name","zhangsan");
Object name = ops.get("name");
System.out.println(name);
//2.stringRedisTemplate
ValueOperations<String, String> ops1 = stringRedisTemplate.opsForValue();
ops1.set("age","21");
String age = ops1.get("age");
System.out.println(age);
}
}
8.4.自定义模板解决序列化问题
默认情况下的模板RedisTemplate<Object, Object>,默认序列化使用的是
JdkSerializationRedisSerializer,存储二进制字节码。这时需要自定义模板,当自定义模板后又想存储String字符串时,可以使StringRedisTemplate的方式,他们俩并不冲突。
序列化问题
要把 domain object做为key-value对保存在redis 中,就必须要解决对象的序列化问题。Spring Data Redis给我们提供了—些现成的方案:
- JdkSerializationRedisSerializer使用JDK提供的序列化功能。优点是反序列化时不需要提供类型信息(class),但缺点是序列化后的结果非常庞大,是JSON格式的5倍左右,这样就会消耗Redis 服务器的大量内存。
- Jackson2JsonRedisSerializer使用Jackson库将对象序列化为JSON字符串。优点是速度快,序列化后的字符串短小精悍。但缺点也非常致命,那就是此类的构造函数中有一个类型参数,必须提供要序列化对象的类型信息(.class对象)。通过查看源代码,发现其只在反序列化过程中用到了类型信息。
- Generic]ackson23sonRedisSerializer通用型序列化,这种序列化方式不用自己手动指定对象的Class,
测试
新建配置类:RedisConfig.java
package cn.aisencode.springdataredisdemo.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* @author Aisen
* @time 2021.5.26 15:07
*/
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String,Object> redisTemplate(LettuceConnectionFactory lettuceConnectionFactory) {
RedisTemplate<String,Object> redisTemplate = new RedisTemplate<>();
//为string类型的key设置序列化
redisTemplate.setKeySerializer(new StringRedisSerializer());
//为object类型的value设置序列化
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
//为hash类型的key设置序列化
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
//为hash类型的value设置序列化
redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
//设置连接工厂
redisTemplate.setConnectionFactory(lettuceConnectionFactory);
return redisTemplate;
}
}
新建实体类:User.java
package cn.aisencode.springdataredisdemo.pojo;
import java.io.Serializable;
/**
* @author Aisen
* @time 2021.5.26 09:29
*/
//实现序列化接口
public class User implements Serializable {
//自定义serialVersionUID 实现版本兼容
private static final long serialVersionUID = -8684061704982377387L;
private Integer id;
private String username;
private String password;
public User() {
}
public User(Integer id, String username, String password) {
this.id = id;
this.username = username;
this.password = password;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", username='" + username + '\'' +
", password='" + password + '\'' +
'}';
}
}
测试
package cn.aisencode.springdataredisdemo;
import cn.aisencode.springdataredisdemo.pojo.User;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
@SpringBootTest
class SpringdataredisdemoApplicationTests {
//不加泛型默认Object,会以二进制存储
@Autowired
private RedisTemplate redisTemplate;
//private RedisTemplate<String,String> redisTemplate;
@Autowired
private StringRedisTemplate stringRedisTemplate;
/**
* 测试连接
*/
@Test
public void initConn() {
//1.redisTemplate
/*ValueOperations ops = redisTemplate.opsForValue();
ops.set("name","zhangsan");
Object name = ops.get("name");
System.out.println(name);*/
//2.stringRedisTemplate
ValueOperations<String, String> ops1 = stringRedisTemplate.opsForValue();
ops1.set("age","21");
String age = ops1.get("age");
System.out.println(age);
}
/**
* 测试序列化
*/
@Test
public void testSerial() {
User user = new User(1,"zhangsan","123456");
ValueOperations ops = redisTemplate.opsForValue();
ops.set("user",user);
User user1 = (User) ops.get("user");
System.out.println(user1);
}
}
8.5.Lettuce操作String
/**
* 操作Sting
*/
@Test
public void testString() {
ValueOperations ops = redisTemplate.opsForValue();
//添加一条数据
ops.set("name","aisen");
//获取一条数据
String name = (String) ops.get("name");
//层级关系
ops.set("user:aisen:item1","item");
//添加多条数据
Map<String,String> map = new HashMap<>();
map.put("age","21");
map.put("sex","man");
ops.multiSet(map);
//获取多条数据
List<String> keys = new ArrayList<>();
keys.add("name");
keys.add("age");
keys.add("sex");
List list = ops.multiGet(keys);
list.forEach(System.out::println);
//删除
redisTemplate.delete("name");
}
8.6.Lettuce操作Hash
/**
* 操作Hash
*/
@Test
public void testHash() {
HashOperations ops = redisTemplate.opsForHash();
//添加一条数据
ops.put("user","name","asien");
//获取一条数据
String name = (String) ops.get("user", "name");
System.out.println(name);
//添加多条数据
Map<String,String> map = new HashMap<>();
map.put("age","21");
map.put("sex","man");
ops.putAll("user",map);
//获取多条数据
List<String> keys = new ArrayList<>();
keys.add("name");
keys.add("age");
keys.add("sex");
List list = ops.multiGet("user", keys);
list.forEach(System.out::println);
//获取所有key下的值
Map<String,String> user = ops.entries("user");
user.entrySet().forEach(e -> System.out.println(e.getKey()+":"+ e.getValue()));
//删除
ops.delete("user","sex","age");
}
8.7.Lettuce操作List
/**
* 操作List
*/
@Test
public void testList() {
ListOperations ops = redisTemplate.opsForList();
//左添加
ops.leftPush("human","zhansan");
//右添加
ops.rightPush("human","lisi");
ops.rightPush("human","zhaoliu");
//获取数据
List human = ops.range("human", 0, -1);
human.forEach(System.out::println);
//获取总条数
Long size = ops.size("human");
System.out.println(size);
//左抛出
String leftPop = (String) ops.leftPop("human");
System.out.println(leftPop);
//删除
ops.remove("human",1,"lisi");
}
8.8.Lettuce操作Set
/**
* 操作Set
*/
@Test
public void testSet() {
SetOperations ops = redisTemplate.opsForSet();
//添加数据
ops.add("worlds","aaa","bbb","ccc");
String[] myWorlds = new String[]{"ddd","eee"};
ops.add("worlds",myWorlds);
//获取数据
Set worlds = ops.members("worlds");
worlds.forEach(System.out::println);
//删除
ops.remove("world","aaa","eee");
}
8.9.Lettuce操作Sorted Set
/**
* 操作Sorted Set
*/
@Test
public void testSortedSet() {
ZSetOperations ops = redisTemplate.opsForZSet();
//添加数据
ops.add("object","math",7d);
//set添加
ZSetOperations.TypedTuple<Object> english = new DefaultTypedTuple<>("english", 5d);
ZSetOperations.TypedTuple<Object> chinese = new DefaultTypedTuple<>("chinese", 8d);
Set<ZSetOperations.TypedTuple<Object>> set = new HashSet<>();
set.add(english);
set.add(chinese);
ops.add("object",set);
//获取数据
Set object = ops.range("object", 0, -1);
object.forEach(System.out::println);
//获取条数
Long size = ops.size("object");
System.out.println(size);
//删除
ops.remove("object","math");
}
8.10.Lettuce获取所有key和设置失效时间
/**
* 获取所有key
*/
@Test
public void testAllKey() {
Set keys = redisTemplate.keys("*");
keys.forEach(System.out::println);
}
/**
* 设置失效时间
*/
@Test
public void testExpire() {
ValueOperations ops = redisTemplate.opsForValue();
//设置key时同时添加失效时间
ops.set("test","test",30, TimeUnit.SECONDS);
//获取失效时间
Long expire = redisTemplate.getExpire("test");
System.out.println(expire);
//给已经存在的key设置失效时间
redisTemplate.expire("test",30,TimeUnit.SECONDS);
}
8.11.Lettuce整合哨兵
方式一
直接在yml文件配置
#哨兵模式
sentinel:
#主节点名称
master: mymaster
#主节点密码
password: root
#从节点ip:端口
nodes: 127.0.0.1:26379,127.0.0.1:26380,127.0.0.1:26381
方式二
在redisConfig.class 配置类中配置
/**
* 配置类整合哨兵
* @return
*/
@Bean
public RedisSentinelConfiguration redisSentinelConfiguration() {
RedisSentinelConfiguration redisSentinelConfiguration = new RedisSentinelConfiguration()
//主节点名称
.master("mymaster")
//哨兵
.sentinel("127.0.0.1",26379)
.sentinel("127.0.0.1",26380)
.sentinel("127.0.0.1",26381);
//主节点密码
redisSentinelConfiguration.setPassword("root");
return redisSentinelConfiguration;
}
9.如何应对缓存穿透、缓存击穿、缓存雪崩问题
9.1.Key的过期淘汰机制
Redis可以对存储在Redis中的缓存数据设置过期时间,比如我们获取的短信验证码一般十分钟过期,我们这时候就需要在验证码存进Redis时添加一个key的过期时间,但是这里有一个需要格外注意的问题就是:并非key过期时间到了就一定会被Redis给删除。
9.1.1.定期删除
Redis默认是每隔100ms就随机抽取一些设置了过期时间的Key,检查其是否过期,如果过期就删除。为什么是随机抽取而不是检查所有key?因为你如果设置的key成千上万,每100毫秒都将所有存在的key检查—遍,会给CPU带来比较大的压力。
9.1.2.惰性删除
定期删除由于是随机抽取可能会导致很多过期Key到了过期时间并没有被删除。所以用户在从缓存获取数据的时候,redis会检查这个key是否过期了,如果过期就删除这个key。这时候就会在查询的时候将过期key从缓存中清除。
9.1.3.内存淘汰机制
仅仅使用定期删除+惰性删除机制还是会留下一个严重的隐患:如果定期删除留下了很多已经过期的key,而且用户长时间都没有使用过这些过期key,导致过期key无法被惰性删除,从而导致过期key一直堆积在内存里,最终造成Redis内存块被消耗殆尽。那这个问题如何解决呢?这个时候Redis内存淘汰机制应运而生了。Redis内存淘汰机制提供了6种数据淘汰策略:
- volatile-1ru:从已设置过期时间的数据集中挑选最近最少使用的数据淘汰。
- volatile-tt1:从已设置过期时间的数据集中挑选将要过期的数据淘汰。
- volatile-random:从已设置过期时间的数据集中任意选择数据淘汰。
- allkeys-1ru:当内存不足以容纳新写入数据时移除最近最少使用的key。
- allkeys-random :从数据集中任意选择数据淘汰。
- no-enviction(默认):当内存不足以容纳新写入数据时,新写入操作会报错。
一般情况下,推荐使用volatile-1ru策略,对于配置信息等重要数据,不应该设置过期时间,这样Redis就永远不会淘汰这些重要数据。对于一般数据可以添加一个缓存时间,当数据失效则请求会从DB中获取并重新存入Redis中。
9.2.缓存击穿
首先我们来看下请求是如何取到数据的:当接收到用户请求,首先先尝试从Redis缓存中获取到数据,如果缓存中能取到数据则直接返回结果,当缓存中不存在数据时从DB获取数据,如果数据库成功取到数据,则更新Redis,然后返回数据,如果DB无数
定义
高并发的情况下,某个热门key突然过期,导致大量请求在Redis未找到缓存数据,进而全部去访问DB请求数据,引起DB压力瞬间增大。
解决方案
缓存击穿的情况下一般不容易造成DB的宕机,只是会造成对DB的周期性压力。对缓存击穿的解决方案一般可以这样:
- Redis中的数据不设置过期时间,然后在缓存的对象上添加一个属性标识过期时间,每次获取到数据时,校验对象中的过期时间属性,如果数据即将过期,则异步发起一个线程主动更新缓存中的数据。但是这种方案可能会导致有些请求会拿到过期的值,就得看业务能否可以接受。
- 如果要求数据必须是新数据,则最好的方案则为热点数据设置为永不过期,然后加一个互斥锁保证缓存的单线程写。
9.3.缓存穿透
定义
缓存穿透是指查询缓存和DB中都不存在的数据。比如通过id查询商品信息,id一般大于0,攻击者会故意传id为-1去查询,由于缓存是不命中则从DB中获取数据,这将会导致每次缓存都不命中数据导致每个请求都访问DB,造成缓存穿透。
解决方案
- 利用互斥锁,缓存失效的时候,先去获得锁,得到锁了,再去请求数据库。没得到锁,则休眠一段时间重试·采用异步更新策略,无论key是否取到值,都直接返回。value值中维护一个缓存失效时间,缓存如果过期,异步起一个线程去读数据库,更新缓存。需要做缓存预热(项目启动前,先加载缓存)操作。
- 提供一个能迅速判断请求是否有效的拦截机制,比如,利用布隆过滤器,内部维护一系列合法有效的key。迅速判断出,请求所携带的Key是否合法有效。如果不合法,则直接返回。
- 如果从数据库查询的对象为空,也放入缓存,只是设定的缓存过期时间较短,比如设置为60秒。
9.4.缓存雪崩
定义
缓存中如果大量缓存在一段时间内集中过期了,这时候会发生大星的缓存击穿现象,所有的请求都落在了DB上,由于查询数据量巨大,引起DB压力过大甚至导致DB宕机。
解决方案
- 给缓存的失效时间,加上一个随机值,避免集体失效。如果Redis是集群部署,将热点数据均匀分布在不同的Redis库中也能避免全部失效的问题(推荐)
- 使用互斥锁,但是该方案吞吐量明显下降了。·设置热点数据永远不过期。
- 双缓存。我们有两个缓存,缓存A和缓存B。缓存A的失效时间为20分钟,缓存B不设失效时间。自己做缓存预热操作。然后细分以下几个小点(推荐)
1.从缓存A读数据库,有则直接返回
2.A没有数据,直接从B读数据,直接返回,并且异步启动一个更新线程。
3.更新线程同时更新缓存A和缓存B。