title: redis
date: 2020/10/17
1.Nosql
- 方便扩展(数据之间没有关系,很好扩展!)
- 大数据量高性能(Redis一秒可以写8万次,读11万次,NoSQL的缓存记录级,是一种细粒度的缓存,性能会比较高!)
- 数据类型是多样型的!(不需要事先设计数据库,随取随用)
- 传统的 RDBMS 和 NoSQL
传统的 RDBMS(关系型数据库)
- 结构化组织
- SQL
- 数据和关系都存在单独的表中 row col
- 操作,数据定义语言
- 严格的一致性
- 基础的事务
Nosql
- 不仅仅是数据
- 没有固定的查询语言
- 键值对存储,列存储,文档存储,图形数据库(社交关系)
- 最终一致性
- CAP定理和BASE
- 高性能,高可用,高扩展
2. Nosql的四大分类
KV键值对
文档型数据库(bson数据格式)
MongoDB(掌握)
- 基于分布式文件存储的数据库。C++编写,用于处理大量文档。
- MongoDB是RDBMS和NoSQL的中间产品。MongoDB是非关系型数据库中功能最丰富的,NoSQL中最像关系型数据库的数据库。
列存储数据库
图关系数据库
3. Redis入门
3.1 Redis是什么?
Redis(Remote Dictionary Server ),即远程字典服务。
是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。
与memcached一样,为了保证效率,数据都是缓存在内存中。区别的是redis会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件,并且在此基础上实现了master-slave(主从)同步。
3.2 Redis能该干什么?
- 内存存储、持久化,内存是断电即失的,所以需要持久化(RDB、AOF)
- 高效率、用于高速缓冲
- 发布订阅系统
- 地图信息分析
- 计时器、计数器(eg:浏览量)
3.3 特性
- 多样的数据类型
- 持久化
- 集群
- 事务
注:Redis是单线程的,Redis是基于内存操作的。
误区1:高性能的服务器一定是多线程的?
误区2:多线程(CPU上下文会切换!)一定比单线程效率高!
核心:Redis是将所有的数据放在内存中的,所以说使用单线程去操作效率就是最高的,多线程(CPU上下文会切换:耗时的操作!),对于内存系统来说,如果没有上下文切换效率就是最高的,多次读写都是在一个CPU上的,在内存存储数据情况下,单线程就是最佳的方案。
3.4 常见操作
keys *
:查看当前数据库中所有的key。
flushdb
:清空当前数据库中的键值对。
flushall
:清空所有数据库的键值对。
exists key
:判断键是否存在
del key
:删除键值对
move key db
:将键值对移动到指定数据库
expire key second
:设置键值对的过期时间
type key`:查看value的数据类型
ENAME key newkey修改 key 的名称
RENAMENX key newkey`仅当 newkey 不存在时,将 key 改名为 newkey 。
3.5 五大数据类型
Redis是一个开源(BSD许可),内存存储的数据结构服务器,可用作数据库,高速缓存和消息队列代理。它支持字符串、哈希表、列表、集合、有序集合,位图,hyperloglogs等数据类型。内置复制、Lua脚本、LRU收回、事务以及不同级别磁盘持久化功能,同时通过Redis Sentinel提供高可用,通过Redis Cluster提供自动分区。
Redis-key:
在redis中无论什么数据类型,在数据库中都是以key-value形式保存,通过进行对Redis-key的操作,来完成对数据库中数据的操作。
# 基础语法:
# SET key value 设置一个key
# GET key 获取一个key对应value
# EXISTS key 查询key是否存在
# MOVE key n(数字) 将当前key移动到指定的几号数据库中
# KEYS * 查询当前数据库中全部的key
# EXPIRE key time 设置当前key的过期时间
# TTL key 查询当前key的存活时间
# TYPE key 查看key的数据类型
127.0.0.1:6379> set name xiaohuang # 设置key-value
OK
127.0.0.1:6379> get name # 查询key指定的value
"xiaohuang"
127.0.0.1:6379> EXISTS name # 查看当前key是否存在
(integer) 1
127.0.0.1:6379> EXISTS name1
(integer) 0
127.0.0.1:6379> MOVE name 1 # 将当前key移动到1号数据库
(integer) 1
127.0.0.1:6379> KEYS *
(empty list or set)
127.0.0.1:6379> SELECT 1 # 选择数据库
OK
127.0.0.1:6379[1]> KEYS *
1) "name"
127.0.0.1:6379[1]> EXPIRE name 10 # 设置当前key的过期时间,单位是秒
(integer) 1
127.0.0.1:6379[1]> KEYS *
2) "name"
127.0.0.1:6379[1]> KEYS *
3) "name"
127.0.0.1:6379[1]> KEYS *
4) "name"
127.0.0.1:6379[1]> KEYS *
5) "name"
127.0.0.1:6379[1]> ttl name # 查看指定key的存活时间,
(integer) -2 # 返回-2表示当前key已经过期,如果为-1,表示永不过期
127.0.0.1:6379[1]> KEYS *
(empty list or set)
127.0.0.1:6379> set name xiaojiejie
OK
127.0.0.1:6379> set age 26
OK
127.0.0.1:6379> KEYS *
6) "age"
7) "name"
127.0.0.1:6379> TYPE name # 查看指定key的value的数据类型
string
127.0.0.1:6379> TYPE age
string
127.0.0.1:6379>
String(字符串
# 语法:
# APPEND key appendValue 对指定key实现字符串拼接,如果key不存在,等同于set
# STRLEN key 查看指定key的长度
# INCR key 对指定key进行自增,类似于Java中的i++
# DECR key 自减,类似于Java的i--
# INCRBY key n 对指定key按照指定的步长值进行自增
# DECRBY key n 按照指定的步长值自减
# SETRANGE key index value 从指定key的索引开始,插入指定的value值。
# 如果key不存在且索引>1,那么当前的索引之前的数据,会用\x00代替并占用一个索引位置,相当于ASCII码中的null
# GETRANGE key startIndex endIndex 将指定key按照索引的开始和结束范围进行截取,成为一个新的key
# SETEX key time value 设置一个有存活时间的key
# SETNX key value 如果这个key不存在,即创建
# MSET key value ... 设置多个key value
# MGET key ... 获取多个key指定的value
# GETSET key value 先获取指定的key,然后再设置指定的value
127.0.0.1:6379> set k1 v1
OK
127.0.0.1:6379> get k1
"v1"
127.0.0.1:6379> KEYS * # 获取当前数据库中所有的key
1) "k1"
127.0.0.1:6379> APPEND k1 hello #给k1再继续追加value值
(integer) 7
127.0.0.1:6379> get k1
"v1hello"
127.0.0.1:6379> APPEND k1 ,xiaohuang
(integer) 17
127.0.0.1:6379> STRLEN k1 # 查看当前k1的长度
(integer) 17
127.0.0.1:6379> get k1
"v1hello,xiaohuang"
127.0.0.1:6379> KEYS *
2) "k1"
127.0.0.1:6379> APPEND name xiaohuang
(integer) 9
127.0.0.1:6379> get name
"xiaohuang"
127.0.0.1:6379> KEYS *
3) "k1"
4) "name"
###################实现自增自减效果################
127.0.0.1:6379> set views 0
OK
127.0.0.1:6379> get views
0
127.0.0.1:6379> INCR views # 设置value的自增效果
(integer) 1
127.0.0.1:6379> INCR views
(integer) 2
127.0.0.1:6379> get views
"2"
127.0.0.1:6379> DECR views # 设置value的自减效果
(integer) 1
127.0.0.1:6379> DECR views
(integer) 0
127.0.0.1:6379> get views
"0"
############可以在自增自减时设置步长##############
127.0.0.1:6379> INCRBY views 2 # 自增,设置步长为2
(integer) 2
127.0.0.1:6379> INCRBY views 2
(integer) 4
127.0.0.1:6379> get views
"4"
127.0.0.1:6379> DECRBY views 3 # 自减,设置步长为3
(integer) 1
127.0.0.1:6379> DECRBY views 3
(integer) -2
127.0.0.1:6379> get views
"-2"
# 注意:value的自增和自减只适用于Integer类型
127.0.0.1:6379> incr name
(error) ERR value is not an integer or out of range
#################实现字符串截取效果#################
127.0.0.1:6379> set k1 hello,xiaohuang
OK
127.0.0.1:6379> get k1
"hello,xiaohuang"
127.0.0.1:6379> GETRANGE k1 0 3 # 实现字符串截取,有起始索引和结束索引,相当于Java中的subString()
"hell"
# 如果结束索引为-1,则表示当前截取的字符串为全部
127.0.0.1:6379> GETRANGE k1 0 -1
"hello,xiaohuang"
###############实现字符串的替换效果#################
127.0.0.1:6379> set key2 abcdefg
OK
127.0.0.1:6379> get key2
"abcdefg"
127.0.0.1:6379> SETRANGE key2 2 hello # 实现字符串的替换效果,命令中的数字“2”表示从索引2的位置开始将其替换为指定字符串
(integer) 7
127.0.0.1:6379> get key2
"abhello"
##################################################
# setex(set with expire) # 设置过期时间
# setnx(set with not exist) # 如果key不存在,创建(分布式锁中常用)
127.0.0.1:6379> setex k3 10 v3 # 设置一个k3,过期时间为10秒
OK
127.0.0.1:6379> keys *
1) "k1"
2) "key2"
3) "name"
4) "views"
5) "k3"
# 10秒之后会自动删除
127.0.0.1:6379> keys *
1) "k1"
2) "key2"
3) "name"
4) "views"
127.0.0.1:6379> setnx lan redis # 如果key不存在,即创建
(integer) 1
127.0.0.1:6379> setnx lan mongodb
(integer) 0 # 0表示没有设置成功,也可理解为“有0行受到影响”
127.0.0.1:6379> get lan
"redis"
######################一次性设置(获取)多个键值对#####################
127.0.0.1:6379> mset k1 v1 k2 v2 k3 v3 # 同时设置多个值
OK
127.0.0.1:6379> KEYS *
5) "k2"
6) "k1"
7) "k3"
127.0.0.1:6379> mget k1 k2 k3 # 同时获取多个值
8) "v1"
9) "v2"
10) "v3"
# 也可以在这边的语法前面加上一个m,代表设置多个
127.0.0.1:6379> msetnx k1 vv1 k4 v4
(integer) 0
# 但是这边同时设置多个值,如果有一个key已经存在,那么这一条设置语句便不会执行成功,
# 因为Redis单条语句是原子操作,要么同时成功,要么同时失败
127.0.0.1:6379> keys *
1) "k2"
2) "k1"
3) "k3"
# 在Redis中,还推荐了一个比较有意思的东西
# 这是Redis中关于key的命名,可以用“:”来代表层次结构,可以对指定的key进行分类存储
127.0.0.1:6379> mset user:1:name xiaohuang user:1:age 21
OK
127.0.0.1:6379> mget user:1:name user:1:age
1) "xiaohuang"
2) "21"
127.0.0.1:6379> getset sqlan redis # 先获取当前key指定的value,如果不存在,会返回nil(null),然后设置新值
(nil)
127.0.0.1:6379> get sqlan
"redis"
127.0.0.1:6379> getset sqlan hbase
"redis"
127.0.0.1:6379> get sqlan
"hbase"
####################################################################
类似于Redis中String这样的使用场景,value值可以是字符串,也可以是其他类型
String的存储的字符串长度最大可以达到512M
主要用途
- 计数器
- 统计多单位的数量
- 一个用户的粉丝数
- 一个有过期时间的验证码
List(列表)
redis中的List列表可以做很多事情,可以将其看成数据结构中的栈,也可以是队列,或者阻塞队列
# 命令:
# LPUSH key value1 value2 ... 设置一个key,从头部插入数据(头插法)
# RPUSH key value1 value2 ... 设置一个key,从尾部插入数据(尾插法)
# LRANGE key startIndex endIndex 返回列表中从开始索引到结束索引位置的value值
# LPOP key 从key头部弹出一个value
# RPOP key 从尾部弹出一个value
# LINDEX index 返回key中指定索引的value值
# LREM key n value 删除key中的指定的value值,n代表删除几个
# LLEN key 返回key的长度
# LTRIM key startIndex endIndex 截取key,截取的范围从开始索引到结束索引
# LSET key index value 从当前key的索引开始插入指定的value值
# RPOPLPUSH key1 key2 从key1的尾部弹出一个元素,将此元素从key2的头部插入
# LINSERT key BEFORE|AFTER oldValue newValue 从指定key中已存在的value的前面或者后面插入一个指定的value
127.0.0.1:6379> LPUSH list one # 从list头部插入一个或者多个元素(从左边插入,看命令首字母)
(integer) 1
127.0.0.1:6379> LPUSH list two
(integer) 2
127.0.0.1:6379> LPUSH list three
(integer) 3
# 返回存储在列表中指定范围的元素,0和-1代表开始索引和结束索引,
# -1不代表实际位置的索引,它表示需要返回到这个列表的最后一个元素
127.0.0.1:6379> LRANGE list 0 -1
1) "three"
2) "two"
3) "one"
127.0.0.1:6379> LRANGE list 0 1 # 返回list中指定位置的元素
4) "three"
5) "two"
127.0.0.1:6379> RPUSH list right # 从list尾部插入一个或多个元素(从右边插入,同第一行一样)
(integer) 4
127.0.0.1:6379> LRANGE list 0 -1
6) "three"
7) "two"
8) "one"
9) "right"
127.0.0.1:6379> LPOP list # 从list头部(左)弹出(删除)一个元素
"three"
127.0.0.1:6379> RPOP list # 从list末尾(右)弹出一个元素
"right"
127.0.0.1:6379> LRANGE list 0 -1
10) "two"
11) "one"
######################################################################################################################
127.0.0.1:6379> LRANGE list 0 -1
12) "two"
13) "one"
127.0.0.1:6379> LINDEX list 0 # 从头部查询list中指定索引的元素
"two"
127.0.0.1:6379> LINDEX list 1
"one"
######################################################################################################################
127.0.0.1:6379> lrange list 0 -1
14) "three"
15) "two"
16) "one"
127.0.0.1:6379> LLEN list # 返回list的长度
(integer) 3
127.0.0.1:6379> LPUSH list three
(integer) 4
127.0.0.1:6379> lrange list 0 -1
17) "three"
18) "three"
19) "two"
20) "one"
# ================================================================================================================================
# 表示删除key中的value值
# 语法为 LREM key 删除个数 value
# ================================================================================================================================
127.0.0.1:6379> LREM list 1 three # 删除list中的1个three
(integer) 1
127.0.0.1:6379> lrange list 0 -1
1) "three"
2) "two"
3) "one"
##################信息截取操作#########################################################################################
127.0.0.1:6379> LRANGE mylist 0 -1
4) "zero"
5) "one"
6) "two"
7) "three"
8) "four"
127.0.0.1:6379> LTRIM mylist 0 2 # 截取mylist列表,只保留从开始索引到结束索引的元素
OK
127.0.0.1:6379> LRANGE mylist 0 -1
9) "zero"
10) "one"
11) "two"
127.0.0.1:6379>
######################################################################################################################
# 复杂命令:rpoplpush
# 语法:rpoplpush source(源列表,必须存在) destination(目标列表,如不存在,即创建)
127.0.0.1:6379> LRANGE mylist 0 -1
1) "zero"
2) "one"
3) "two"
4) "three"
5) "four"
# 表示移除mylist中的最后一个元素,将这个被删除掉的元素从头部进入一个list
127.0.0.1:6379> RPOPLPUSH mylist list
"four"
127.0.0.1:6379> lrange list 0 -1
1) "four"
127.0.0.1:6379> lrange mylist 0 -1
2) "zero"
3) "one"
4) "two"
5) "three"
######################################################################################################################
# lset 命令,类似于关系型数据库中的Update语句,将指定索引的value值修改为其他的指定value值
127.0.0.1:6379> EXISTS list # 判断key是否存在
(integer) 0
127.0.0.1:6379> lset list 0 item # 如果当前这个key不存在,无法修改,报错
(error) ERR no such key
127.0.0.1:6379> LPUSH list v1
(integer) 1
127.0.0.1:6379> LRANGE list 0 0
1) "v1"
127.0.0.1:6379> lset list 0 item01 # lset命令只对已存在的列表操作才是有效的
OK
127.0.0.1:6379> LRANGE list 0 0
2) "item01"
127.0.0.1:6379> lset list 1 item02 # 如果要更新的索引超过列表的长度,那么它也会报错
(error) ERR index out of range
######################################################################################################################
# linsert 命令,从列表中的某一个value值的前面或后面插入一个新的value值
# 语法:linsert key before|after 列表中的value值 新value值
127.0.0.1:6379> LPUSH mylist hello
(integer) 1
127.0.0.1:6379> LPUSH mylist xiaohuang
(integer) 2
127.0.0.1:6379> LRANGE mylist 0 -1
1) "xiaohuang"
2) "hello"
127.0.0.1:6379> LINSERT mylist after hello world
(integer) 3
127.0.0.1:6379> LRANGE mylist 0 -1
3) "xiaohuang"
4) "hello"
5) "world"
127.0.0.1:6379> LINSERT mylist before xiaohuang nihao
(integer) 4
127.0.0.1:6379> LRANGE mylist 0 -1
6) "nihao"
7) "xiaohuang"
8) "hello"
9) "world"
List列表实际上它是一个数据结构的链表
可以在Node节点的before或者after,left或者right插入值
key不存在,创建新链表
key存在,新增内容
如果移除了所有了value,也代表不存在
在Node节点的两边插入,效率最高!中间元素效率较低
Redis中可以将这个列表灵活的使用
栈(lpush lpop | rpush rpop),队列(lpush rpop | rpush lpop)
set(集合)
set集合无序不重复
# 命令:
# SADD key value1 value2 ... 设置一个key
# SMEMBERS key 查看当前key
# SISMEMBER key value 查看key中指定的value是否存在
# SCARD key 查看key的长度
# SREM key value 删除key中的指定value
# SPOP key 随机删除key中的一个value
# SRANDMEMBER key [n] 随机查看当前key中的一个或者多个value
# SMOVE key1 key2 key1Value 将key1中的value移动到key2中
# SDIFF key1 key2 两个key相交,求第一个key的补集
# SINTER key1 key2 两个key相交,求交集
# SUNION key1 key2 两个key相交,求并集
127.0.0.1:6379> SADD myset hello # myset集合添加元素
(integer) 1
127.0.0.1:6379> SADD myset xiaohuang
(integer) 1
127.0.0.1:6379> SADD myset love
(integer) 1
127.0.0.1:6379> SMEMBERS myset # 查看myset的所有值
1) "love"
2) "hello"
3) "xiaohuang"
127.0.0.1:6379> SISMEMBER myset hello # 判断hello是否在set集合中
(integer) 1
127.0.0.1:6379> SISMEMBER myset world
(integer) 0
127.0.0.1:6379> SADD myset hello # 如果往set集合中添加一个重复的值,不会报错,但是也不会插入成功,因为set集合无序且不重复
(integer) 0
127.0.0.1:6379> SCARD myset # 查看myset集合中的元素个数
(integer) 3
127.0.0.1:6379> SREM myset hello # 删除set集合中hello
(integer) 1
127.0.0.1:6379> SMEMBERS myset
4) "love"
5) "xiaohuang"
######################################################################################################################
# 在一个无序集合中,随机抽取一个数
127.0.0.1:6379> SMEMBERS nums
1) "1"
2) "2"
3) "3"
4) "4"
5) "5"
6) "6"
7) "7"
8) "8"
9) "9"
127.0.0.1:6379> SRANDMEMBER nums # 随机抽取
"8"
127.0.0.1:6379> SRANDMEMBER nums
"7"
127.0.0.1:6379> SRANDMEMBER nums
"5"
127.0.0.1:6379> SRANDMEMBER nums 2 # 随机抽取指定个数的元素
10) "5"
11) "4"
######################################################################################################################
# 删除指定key,随机删除key
# 以上面的数字集合为例
127.0.0.1:6379> SPOP nums # 随机删除一个set集合元素
"5"
127.0.0.1:6379> SPOP nums
"8"
127.0.0.1:6379> SPOP nums
"7"
127.0.0.1:6379> SMEMBERS nums
1) "1"
2) "2"
3) "3"
4) "4"
5) "6"
6) "9"
######################################################################################################################
# 一个set集合中的值移动到另外一个set集合中
# 语法: smove source(源set集合,必须存在) destination(目标集合,被添加元素) value(必须是源set集合中存在的value)
127.0.0.1:6379> SMEMBERS set01
1) "hello"
2) "xiaohuang"
3) "world"
127.0.0.1:6379> SMEMBERS set02
4) "me"
127.0.0.1:6379> SMOVE set01 set02 xiaohuang # 将set01中的xiaohuang放入到set02中
(integer) 1
127.0.0.1:6379> SMEMBERS set02
5) "xiaohuang"
6) "me"
127.0.0.1:6379> SMEMBERS set01
7) "hello"
8) "world"
######################################################################################################################
# 生活中的一个小现象,就比如说微信公众号,会有共同关注,还有QQ的共同好友
# 数学集合关系中的:交、并、补。微信公众号中的共同关注,以及QQ的共同好友,就是关系中的交!
127.0.0.1:6379> SMEMBERS k1
1) "b"
2) "c"
3) "a"
127.0.0.1:6379> SMEMBERS k2
4) "e"
5) "d"
6) "c"
# 上面的两个集合都有c这个元素
127.0.0.1:6379> SDIFF k1 k2 # k1 与 k2 之间的差集(以k1为主)
1) "b"
2) "a"
127.0.0.1:6379> SINTER k1 k2 # k1 和 k2 之间的交集,公众号的共同关注,QQ中的共同好友就可以这么来实现
3) "c"
127.0.0.1:6379> SUNION k1 k2 # k1 和 k2 之间的并集
4) "c"
5) "a"
6) "e"
7) "b"
8) "d"
# 在多聊一嘴,其实这些命令都可以在英语单词中找到一些规律
# 把SDIFF、SINTER还有SUNION这三个单词首字母去掉,可以得到
# DIFF:different,它代表不同的,用一句Redis官网的翻译来描述:返回的集合元素是第一个key的集合与后面所有key的集合的差集
# INTER:intersection,翻译过来为交叉,同样的,意指数学关系中的交集
# UNION:union,翻译为联合,与数学关系中的并集也是可以沾边的
######################################################################################################################
这里的命令,实际上也可以和生活中的东西都有关系,上面提到的共同关注,共同爱好,还有QQ当中的同一个星座,或者是“二度好友”,什么是二度好友,就是类似于QQ当中给你推荐QQ用户的意思,起源于六度分割理论(什么是六度分割理论,简单地说:就是你和任何一个陌生人之间所间隔的人不会超过五个,最多通过六个人你就能够认识任何一个陌生人)
Hash(哈希)
Redis中的哈希,本质上KV相同但是KV中的V,它也是一个键值对,本质和操作字符串区别不大
# 命令:
# HSET key field value 设置单个hash
# HGET key field 获取单个
# HMSET key field1 v1 field2 v2 设置多个
# HMGET key field 获取多个
# HGETALL key 获取hash中全部的field-value
# HLEN key 获取hash长度
# HEXISTS key field 查询hash中指定的field是否存在
# HKEYS key 只获取hash中的field
# HVALS key 只获取hash中value
# HINCRBY key field n 对hash中指定的field设置自增自减
127.0.0.1:6379> HSET myhash k1 v1
(integer) 1
127.0.0.1:6379> HGET myhash k1
"v1"
127.0.0.1:6379> HMSET myhash k2 v2 k3 v3 k4 v4
OK
127.0.0.1:6379> hmget myhash k1 k2 k3 k4
1) "v1"
2) "v2"
3) "v3"
4) "v4"
127.0.0.1:6379> HGETALL myhash # 获取myhash中全部的kv键值对
5) "k1"
6) "v1"
7) "k2"
8) "v2"
9) "k3"
10) "v3"
11) "k4"
12) "v4"
127.0.0.1:6379> HDEL myhash k4 # 删除myhash一个指定的键值对元素
(integer) 1
127.0.0.1:6379> HGETALL myhash
13) "k1"
14) "v1"
15) "k2"
16) "v2"
17) "k3"
18) "v3"
######################################################################################################################
127.0.0.1:6379> HLEN myhash # 获取当前hash中的value长度
(integer) 3
127.0.0.1:6379> HEXISTS myhash k1 # 判断当前hash中的kv键值对是否存在
(integer) 1
127.0.0.1:6379> HEXISTS myhash k4
(integer) 0
127.0.0.1:6379> HKEYS myhash # 只获取hash中的key
19) "k1"
20) "k2"
21) "k3"
127.0.0.1:6379> HVALS myhash # 只获取hash中key对应的value
22) "v1"
23) "v2"
24) "v3"
######################################################################################################################
# 备注:Redis中,有自增,没有自减,即使是没有自减,也可以在自增的步长当中设置一个负数即可
# hincrby key field 步长值
127.0.0.1:6379> hset myhash num 3
(integer) 1
127.0.0.1:6379> HINCRBY myhash num 2
(integer) 5
127.0.0.1:6379> HINCRBY myhash num 2
(integer) 7
127.0.0.1:6379> HSETNX myhash k4 v4 # 如果hash中的一个元素不存在,即可创建
(integer) 1
127.0.0.1:6379> HSETNX myhash k4 vv4 # 存在,即创建失败
(integer) 0
######################################################################################################################
# 可以使用hash做一些临时变更的数据,可以是用户信息,或者是经常变动的信息
# 上面的String也提到了使用“:”进行层次分割,不过hash更适合对象存储,String适合于文本的存储
127.0.0.1:6379> HMSET user:1 name xiaohuang age 21 sex boy
OK
127.0.0.1:6379> HGETALL user:1
1) "name"
2) "xiaohuang"
3) "age"
4) "21"
5) "sex"
6) "boy"
Zset(有序集合)
在set的基础上增加了一个score的值,相当于zset k1 score v1,使用score来对当前key中元素进行排序
# 命令:
# ZADD key score1 value1 score2 value2 ... zset中添加一个或多个元素
# ZRANGE key startIndex endIndex 查询从开始到结束索引的zset集合
# ZRANGEBYSCORE key min max [WITHSCORES] 对hash中按照指定数值进行升序排列
# ZREVRANGE key startIndex endIndex 对指定开始和结束索引进行降序排列
# ZREM key field 删除hash中指定的field
# ZCARD key 查询hash长度
# ZCOUNT key [min max] 查询hash数量,还可以增加最大值和最小值的范围
127.0.0.1:6379> ZADD myset 1 one
(integer) 1
127.0.0.1:6379> ZADD myset 2 two # zset集合添加一个值
(integer) 1
127.0.0.1:6379> ZRANGE myset 0 -1
1) "one"
2) "two"
127.0.0.1:6379> ZADD myset 3 three 4 four
(integer) 2
127.0.0.1:6379> ZRANGE myset 0 -1
3) "one"
4) "two"
5) "three"
6) "four"
######################################################################################################################
# 实现元素的排序
# 根据zset中score的值来实现元素的排序
127.0.0.1:6379> ZADD salary 3500 xiaohong 6500 xiaohuang 3900 zhangsan
(integer) 3
# 当前命令,inf在Unix系统中代表的意思是无穷,所以当前命令是指,将当前zset,以从小到大的形式进行排列
127.0.0.1:6379> ZRANGEBYSCORE salary -inf +inf
1) "xiaohong"
2) "zhangsan"
3) "xiaohuang"
127.0.0.1:6379> ZRANGEBYSCORE salary -inf +inf withscores # 在排列的同时,将score和指定的元素全部展示
4) "xiaohong"
5) "3500"
6) "zhangsan"
7) "3900"
8) "xiaohuang"
9) "6500"
127.0.0.1:6379> ZREVRANGE salary 0 -1 withscores # 将数据从大到小进行排列
10) "xiaohuang"
11) "6500"
12) "zhangsan"
13) "3900"
14) "xiaohong"
15) "3500"
127.0.0.1:6379> ZRANGEBYSCORE salary -inf 4000 withscores # 展示的同时还可以指示score的查询最大值,指定查询范围
16) "xiaohong"
17) "3500"
18) "zhangsan"
19) "3900"
127.0.0.1:6379> ZREM salary zhangsan # 删除zset中的一个元素
(integer) 1
127.0.0.1:6379> ZRANGE salary 0 -1
20) "xiaohong"
21) "xiaohuang"
127.0.0.1:6379> ZCARD salary
(integer) 2
######################################################################################################################
127.0.0.1:6379> ZADD myset 1 hello 2 world 3 xiaohuang 4 xiaohei 5 xiaolan
(integer) 5
# 语法:ZCOUNT key min max ,min和max包左也包右,它是一个闭区间
127.0.0.1:6379> ZCOUNT myset 2 5 # 获取指定区间的成员数量
(integer) 4
127.0.0.1:6379>
zset是Redis的数据类型,可以排序,生活中也有案例,班级成绩,员工工资
设置权重,1、普通消息;2、重要消息;添加权重进行消息判断其重要性
来一个更接地气的案例,可以打开B站,排行榜,B站会根据视频的浏览量和弹幕量进行综合评分,进行排名
4. 事务
Redis单条命令保持原子性,但是Redis事务不保证原子性
Redis事务本质,可以将Redis的事务看成是一个队列,将一组命令进行“入队”,然后一起执行
一个事务中的所有命令都会被序列化,在事务执行的过程中,会按照顺序执行
Redis事务的三个特性:一致性,顺序性,排他性
悲观锁:
- 很悲观,无论执行什么操作都会出现问题,所以会对所有的操作加锁
乐观锁
- 很乐观,任何情况下都不会出问题,所以不会加锁!但是在数据更新时需要判断在此之前是否有人修改过这个数据
- 可以添加一个字段叫version用来查询
- 在进行数据更新时对version进行比较
5. Jedis
Jedis是Redis官方推荐的Java连接Redis的连接开发工具!使用Java操作Redis的中间件
<!--导入Jedis-->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.2.0</version>
</dependency>
<!--fastjson-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.70</version>
</dependency>
编码测试
- 连接数据库
- 操作命令
- 断开连接
public class TestPing {
public static void main(String[] args) {
// new一个Jedis对象
Jedis jedis = new Jedis("192.168.1.107", 6379);
// Jedis中的API就是之前学习的命令
System.out.println(jedis.ping());
}
}
Redis是远程连接:
会出现连接超时或者是拒绝访问的问题,在这边需要做两件事情,当然,防火墙的关闭也是必不可少的
6. SpringBoot整合Redis
Jedis:采用直连,模拟多个线程操作会出现安全问题。为避免此问题,需要使用Jedis Pool连接池!类似于BIO模式
lettuce:采用netty网络框架,对象可以在多个线程中被共享,完美避免线程安全问题,减少线程数据,类似于NIO模式
导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
属性配置
# 配置Redis
spring.redis.host=192.168.1.107
spring.redis.port=6379
自定义RedisTemplate
// 自定义RedisTemplate
// 这是RedisTemplate的一个模板
@Bean
@SuppressWarnings("all")
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory)
throws UnknownHostException {
// 为了开发方便,可以直接使用<String, Object>
RedisTemplate<String, Object> template = new RedisTemplate<>();
// 序列化配置
Jackson2JsonRedisSerializer serializer = new Jackson2JsonRedisSerializer(Object.class);
template.setDefaultSerializer(serializer);
template.setConnectionFactory(redisConnectionFactory);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance);
serializer.setObjectMapper(om);
StringRedisSerializer srs = new StringRedisSerializer();
// 对于String和Hash类型的Key,可以采用String的序列化方式
template.setKeySerializer(srs);
template.setHashKeySerializer(srs);
// String和Hash类型的value可以使用json的方式进行序列化
template.setValueSerializer(serializer);
template.setHashValueSerializer(serializer);
template.afterPropertiesSet();
return template;
}
RedisUtil工具类
package com.zzr.utils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
@Component
public final class RedisUtil {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
// =============================common============================
/**
* 指定缓存失效时间
* @param key 键
* @param time 时间(秒)
*/
public boolean expire(String key, long time) {
try {
if (time > 0) {
redisTemplate.expire(key, time, TimeUnit.SECONDS);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 根据key 获取过期时间
* @param key 键 不能为null
* @return 时间(秒) 返回0代表为永久有效
*/
public long getExpire(String key) {
return redisTemplate.getExpire(key, TimeUnit.SECONDS);
}
/**
* 判断key是否存在
* @param key 键
* @return true 存在 false不存在
*/
public boolean hasKey(String key) {
try {
return redisTemplate.hasKey(key);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 删除缓存
* @param key 可以传一个值 或多个
*/
@SuppressWarnings("unchecked")
public void del(String... key) {
if (key != null && key.length > 0) {
if (key.length == 1) {
redisTemplate.delete(key[0]);
} else {
redisTemplate.delete((Collection<String>) CollectionUtils.arrayToList(key));
}
}
}
// ============================String=============================
/**
* 普通缓存获取
* @param key 键
* @return 值
*/
public Object get(String key) {
return key == null ? null : redisTemplate.opsForValue().get(key);
}
/**
* 普通缓存放入
* @param key 键
* @param value 值
* @return true成功 false失败
*/
public boolean set(String key, Object value) {
try {
redisTemplate.opsForValue().set(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 普通缓存放入并设置时间
* @param key 键
* @param value 值
* @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期
* @return true成功 false 失败
*/
public boolean set(String key, Object value, long time) {
try {
if (time > 0) {
redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
} else {
set(key, value);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 递增
* @param key 键
* @param delta 要增加几(大于0)
*/
public long incr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递增因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, delta);
}
/**
* 递减
* @param key 键
* @param delta 要减少几(小于0)
*/
public long decr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递减因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, -delta);
}
// ================================Map=================================
/**
* HashGet
* @param key 键 不能为null
* @param item 项 不能为null
*/
public Object hget(String key, String item) {
return redisTemplate.opsForHash().get(key, item);
}
/**
* 获取hashKey对应的所有键值
* @param key 键
* @return 对应的多个键值
*/
public Map<Object, Object> hmget(String key) {
return redisTemplate.opsForHash().entries(key);
}
/**
* HashSet
* @param key 键
* @param map 对应多个键值
*/
public boolean hmset(String key, Map<String, Object> map) {
try {
redisTemplate.opsForHash().putAll(key, map);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* HashSet 并设置时间
* @param key 键
* @param map 对应多个键值
* @param time 时间(秒)
* @return true成功 false失败
*/
public boolean hmset(String key, Map<String, Object> map, long time) {
try {
redisTemplate.opsForHash().putAll(key, map);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 向一张hash表中放入数据,如果不存在将创建
*
* @param key 键
* @param item 项
* @param value 值
* @return true 成功 false失败
*/
public boolean hset(String key, String item, Object value) {
try {
redisTemplate.opsForHash().put(key, item, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 向一张hash表中放入数据,如果不存在将创建
*
* @param key 键
* @param item 项
* @param value 值
* @param time 时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间
* @return true 成功 false失败
*/
public boolean hset(String key, String item, Object value, long time) {
try {
redisTemplate.opsForHash().put(key, item, value);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 删除hash表中的值
*
* @param key 键 不能为null
* @param item 项 可以使多个 不能为null
*/
public void hdel(String key, Object... item) {
redisTemplate.opsForHash().delete(key, item);
}
/**
* 判断hash表中是否有该项的值
*
* @param key 键 不能为null
* @param item 项 不能为null
* @return true 存在 false不存在
*/
public boolean hHasKey(String key, String item) {
return redisTemplate.opsForHash().hasKey(key, item);
}
/**
* hash递增 如果不存在,就会创建一个 并把新增后的值返回
*
* @param key 键
* @param item 项
* @param by 要增加几(大于0)
*/
public double hincr(String key, String item, double by) {
return redisTemplate.opsForHash().increment(key, item, by);
}
/**
* hash递减
*
* @param key 键
* @param item 项
* @param by 要减少记(小于0)
*/
public double hdecr(String key, String item, double by) {
return redisTemplate.opsForHash().increment(key, item, -by);
}
// ============================set=============================
/**
* 根据key获取Set中的所有值
* @param key 键
*/
public Set<Object> sGet(String key) {
try {
return redisTemplate.opsForSet().members(key);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 根据value从一个set中查询,是否存在
*
* @param key 键
* @param value 值
* @return true 存在 false不存在
*/
public boolean sHasKey(String key, Object value) {
try {
return redisTemplate.opsForSet().isMember(key, value);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将数据放入set缓存
*
* @param key 键
* @param values 值 可以是多个
* @return 成功个数
*/
public long sSet(String key, Object... values) {
try {
return redisTemplate.opsForSet().add(key, values);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 将set数据放入缓存
*
* @param key 键
* @param time 时间(秒)
* @param values 值 可以是多个
* @return 成功个数
*/
public long sSetAndTime(String key, long time, Object... values) {
try {
Long count = redisTemplate.opsForSet().add(key, values);
if (time > 0)
expire(key, time);
return count;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 获取set缓存的长度
*
* @param key 键
*/
public long sGetSetSize(String key) {
try {
return redisTemplate.opsForSet().size(key);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 移除值为value的
*
* @param key 键
* @param values 值 可以是多个
* @return 移除的个数
*/
public long setRemove(String key, Object... values) {
try {
Long count = redisTemplate.opsForSet().remove(key, values);
return count;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
// ===============================list=================================
/**
* 获取list缓存的内容
*
* @param key 键
* @param start 开始
* @param end 结束 0 到 -1代表所有值
*/
public List<Object> lGet(String key, long start, long end) {
try {
return redisTemplate.opsForList().range(key, start, end);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 获取list缓存的长度
*
* @param key 键
*/
public long lGetListSize(String key) {
try {
return redisTemplate.opsForList().size(key);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 通过索引 获取list中的值
*
* @param key 键
* @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推
*/
public Object lGetIndex(String key, long index) {
try {
return redisTemplate.opsForList().index(key, index);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
*/
public boolean lSet(String key, Object value) {
try {
redisTemplate.opsForList().rightPush(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
* @param key 键
* @param value 值
* @param time 时间(秒)
*/
public boolean lSet(String key, Object value, long time) {
try {
redisTemplate.opsForList().rightPush(key, value);
if (time > 0)
expire(key, time);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @return
*/
public boolean lSet(String key, List<Object> value) {
try {
redisTemplate.opsForList().rightPushAll(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @param time 时间(秒)
* @return
*/
public boolean lSet(String key, List<Object> value, long time) {
try {
redisTemplate.opsForList().rightPushAll(key, value);
if (time > 0)
expire(key, time);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 根据索引修改list中的某条数据
*
* @param key 键
* @param index 索引
* @param value 值
* @return
*/
public boolean lUpdateIndex(String key, long index, Object value) {
try {
redisTemplate.opsForList().set(key, index, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 移除N个值为value
*
* @param key 键
* @param count 移除多少个
* @param value 值
* @return 移除的个数
*/
public long lRemove(String key, long count, Object value) {
try {
Long remove = redisTemplate.opsForList().remove(key, count, value);
return remove;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
}
7. Redis持久化
RDB持久化
优点:RDB文件紧凑,体积小,网络传输快,适合全量复制;恢复速度比AOF快很多。当然,与AOF相比,RDB最重要的优点之一是对性能的影响相对较小。
缺点:RDB文件的致命缺点在于其数据快照的持久化方式决定了必然做不到实时持久化,而在数据越来越重要的今天,数据的大量丢失很多时候是无法接受的,因此AOF持久化成为主流。此外,RDB文件需要满足特定格式,兼容性差(如老版本的Redis不兼容新版本的RDB文件)。
AOF持久化
与RDB持久化相对应,AOF的优点在于支持秒级持久化、兼容性好,缺点是文件大、恢复速度慢、对性能影响大。
8. 缓存穿透、缓存击穿、缓存雪崩解决方案
缓存穿透
指查询一个一定不存在的数据,如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到 DB 去查询,可能导致 DB 挂掉。
解决方案:
i. 查询返回的数据为空,仍把这个空结果进行缓存,但过期时间会比较短
ii. 布隆过滤器:将所有可能存在的数据哈希到一个足够大的 bitmap 中,一个一定不存在的数据会被这个 bitmap 拦截掉,从而避免了对 DB 的查询。
缓存击穿
对于设置了过期时间的 key,缓存在某个时间点过期的时候,恰好这时间点对这个 Key 有大量的并发请求过来,这些请求发现缓存过期一般都会从后端 DB 加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把 DB 压垮。
解决方案:
i. 使用互斥锁:当缓存失效时,不立即去 load db,先使用如 Redis 的 setnx 去设置一个互斥锁,当操作成功返回时再进行 load db 的操作并回设缓存,否则重试 get 缓存的方法。
ii 永远不过期:物理不过期,但逻辑过期(后台异步线程去刷新)。
缓存雪崩
设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到 DB, DB 瞬时压力过重雪崩。与缓存击穿的区别:雪崩是很多 key,击穿是某一个key 缓存。
解决方案:
将缓存失效时间分散开,比如可以在原有的失效时间基础上增加一个随机值,比如 1-5 分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。
9. Redis 的集群模式
主从复制
当从数据库启动时,会向主数据库发送sync命令,主数据库接收到sync后开始在后台保存快照rdb,在保存快照期间收到的命令缓存起来,当快照完成时,主数据库会将快照和缓存的命令一块发送给从**。复制初始化结束。 之后,主每收到1个命令就同步发送给从。 当出现断开重连后,2.8之后的版本会将断线期间的命令传给重数据库。增量复制
主从复制是乐观复制,当客户端发送写执行给主,主执行完立即将结果返回客户端,并异步的把命令发送给从,从而不影响性能。也可以设置至少同步给多少个从主才可写。 无硬盘复制:如果硬盘效率低将会影响复制性能,2.8之后可以设置无硬盘复制,repl-diskless-sync yes
哨兵模式
哨兵的作用:
1、监控redis主、从数据库是否正常运行
2、主出现故障自动将从数据库转换为主数据库。
哨兵的核心知识
1、哨兵至少需要 3 个实例,来保证自己的健壮性。
2、哨兵 + redis 主从的部署架构,是不保证数据零丢失的,只能保证 redis 集群的高可用性。
3、对于哨兵 + redis 主从这种复杂的部署架构,尽量在测试环境和生产环境,都进行充足的测试和演练。
4、配置哨兵监控一个系统时,只需要配置其监控主数据库即可,哨兵会自动发现所有复制该主数据库的从数据库。
10. Redis分布式锁
先拿setnx来争抢锁,抢到之后,再用expire给锁加一个过期时间防止锁忘记了释放。
如果在setnx之后执行expire之前进程意外crash或者要重启维护了,这个锁就永远得不到释放了,使用set指令把setnx和expire合成一条指令来用
11. Redis 和 Mysql 的数据不一致怎么办
采用延时双删策略
第二个删redis。如果删除了缓存Redis,还没有来得及写库MySQL,另一个线程就来读取,发现缓存为空,则去数据库中读取数据写入缓存,此时缓存中为脏数据。
第一个删redis。如果先写了库,在删除缓存前,写库的线程宕机了,没有删除掉缓存,则也会出现数据不一致情况。
1)先淘汰缓存
(2)再写数据库(这两步和原来一样)
(3)休眠800ms,再次淘汰缓存
这么做,可以将800ms内所造成的缓存脏数据,再次删除。