Redis 学习笔记
1.Redis 的基本知识及基本命令
Redis 默认有16个数据库。
可以查看redis.conf文件:
默认使用的是第0个。
1.1 RedisKey 基本命令
切换数据库(select)
127.0.0.1:6379> select 3 #切换数据库
OK
127.0.0.1:6379[3]> dbsize #查看数据库大小
(integer) 0
设值和取值(set/get)
127.0.0.1:6379[1]> set name jeff
OK
127.0.0.1:6379[1]> get name
"jeff"
查看当前数据库所有的key(keys *)
127.0.0.1:6379[1]> keys *
1) "name"
查看是否包含某个key(exists)
127.0.0.1:6379> set name jeff
OK
127.0.0.1:6379> set age 20
OK
127.0.0.1:6379> keys *
1) "age"
2) "name"
127.0.0.1:6379> exists name #如果存在这个key会返回1
(integer) 1
127.0.0.1:6379> exists name1 #不存在这个key会返回0
(integer) 0
移除key(move)
127.0.0.1:6379> keys *
1) "age"
2) "name"
127.0.0.1:6379> move name 1 #1表示从当前数据库移除
(integer) 1
设置过期时间 (expire)
127.0.0.1:6379> keys *
1) "age"
127.0.0.1:6379> get age
"20"
127.0.0.1:6379> expire age 10 #表示age这个key在10s后过期
(integer) 1 #设置成功
127.0.0.1:6379> ttl age #ttl 查看后面的key还有多久过期
(integer) 5 #表示当前查询的key还差5s过期
127.0.0.1:6379> ttl age
(integer) 3
127.0.0.1:6379> ttl age
(integer) 1
127.0.0.1:6379> ttl age
(integer) -2 #返回-2表示当前查询的key已经过期
127.0.0.1:6379> get age
(nil)
查看当前key存储的数据类型(type)
127.0.0.1:6379> set name jeff
OK
127.0.0.1:6379> set age 20
OK
127.0.0.1:6379> keys *
1) "age"
2) "name"
127.0.0.1:6379> type name
string
127.0.0.1:6379> type age
string
清空当前数据库(flushdb)
127.0.0.1:6379> flushdb
OK
127.0.0.1:6379> keys *
(empty array)
清空全部数据库(flushall)
127.0.0.1:6379> flushall
OK
其他命令可以查看官网:
1.2 Redis 基本知识
Redis 是单线程的
Redis 是基于内存操作,CPU 不是Redis 性能瓶颈,Redis 的瓶颈是根据机器的内存和网络带宽。
Redis 是C语言写的
官方提供的数据为 100000+的QPS,完全不比同样是使用Key-value的Memecache差
Redis 单线程为什么这么快
- 多线程并不一定就比单线程快,因为多线程涉及到CPU上下文的切换
- Redis 将所有的数据全部放在内存中,对于内存系统来说,没有上下文的切换效率就是最高的
1.3 Redis 的数据类型
Redis是一个开源(BSD许可),内存存储的数据结构服务器,可用作数据库,高速缓存和消息队列代理。它支持字符串、哈希表、列表、集合、有序集合,位图,hyperloglogs等数据类型。内置复制、Lua脚本、LRU收回、事务以及不同级别磁盘持久化功能,同时通过Redis Sentinel提供高可用,通过Redis Cluster提供自动分区。
2. Redis 基本操作
2.1 String 字符串类型
#########################################################################################################
127.0.0.1:6379> get name
"jeff"
127.0.0.1:6379> append name "hello" #追加字符串,如果当前key不存在,就相当于set key
(integer) 9
127.0.0.1:6379> get name
"jeffhello"
127.0.0.1:6379> strlen name #获取字符串的长度
(integer) 9
127.0.0.1:6379> set views 0
OK
127.0.0.1:6379> get views
"0"
127.0.0.1:6379> type views
string
127.0.0.1:6379> incr views #自增1
(integer) 1
127.0.0.1:6379> incr name #如果incr用在非数字型key上,就会报错
(error) ERR value is not an integer or out of range
127.0.0.1:6379> get views
"1"
127.0.0.1:6379> decr views #自减1
(integer) 0
127.0.0.1:6379> get views
"0"
127.0.0.1:6379> decr views
(integer) -1
127.0.0.1:6379> get views
"-1"
127.0.0.1:6379> incrby views 10 #可以设置步长,指定增量
(integer) 9
127.0.0.1:6379> get views
"9"
127.0.0.1:6379> decrby views 3 #可以设置步长,指定减量
(integer) 6
127.0.0.1:6379> get views
"6"
#########################################################################################################
# 字符串范围 range
127.0.0.1:6379> set key1 "hello,jeff"
OK
127.0.0.1:6379> get key1
"hello,jeff"
127.0.0.1:6379> getrange key1 0 3 #获取字符串【0-3】
"hell"
127.0.0.1:6379> getrange key1 0 -1 #获取全部的字符串,相当于get key
"hello,jeff"
#替换
127.0.0.1:6379> set key2 hello
OK
127.0.0.1:6379> get key2
"hello"
127.0.0.1:6379> setrange key2 1 xx #用后面的字符串替换,从第2个字符开始替换
(integer) 5 #返回替换后的字符串的长度
127.0.0.1:6379> get key2
"hxxlo"
#########################################################################################################
#setex (set with expire) 设置某个key的值及过期时间
#setnx (set if not exists) 不存在才设置某个值(在分布式锁中会常常使用)
127.0.0.1:6379> setex key1 30 "hello" #设置key1的值为hello,同时设置了过期时间为30s
OK
127.0.0.1:6379> ttl key1 #查看key1的剩余时间
(integer) 20
127.0.0.1:6379> get key1
"hello"
127.0.0.1:6379> ttl key1
(integer) 10
127.0.0.1:6379> setnx mykey "redis" #如果mykey不存在,就创建mykey并设置值为redis
(integer) 1 #设置成功
127.0.0.1:6379> setnx mykey "redis1" #如果mykey不存在,就创建mykey并设置值为redis1
(integer) 0 #这个key已经存在,设置失败
127.0.0.1:6379> get mykey #获取mykey的值,还是第一次设置的值
"redis"
127.0.0.1:6379> ttl key1
(integer) -2 #key1已经达到了过期时间
#########################################################################################################
#mset 批量设置多个值
#mget 同时获取多个值
#msetnx 在所有的key都不存在时,同时设置多个值,注意这是一个原子性的操作
127.0.0.1:6379> mset k1 v1 k2 v2 k3 v3 #同时设置多个值
OK
127.0.0.1:6379> keys *
1) "k1"
2) "k3"
3) "k2"
127.0.0.1:6379> mget k1 k2 #同时获取k1,k2
1) "v1"
2) "v2"
127.0.0.1:6379> mget k1 k2 k3 #同时获取k1,k2,k3
1) "v1"
2) "v2"
3) "v3"
127.0.0.1:6379> msetnx k1 v1 k4 v4 #同时设置k1,k4。由于k1已经存在,不会成功
(integer) 0 #返回0代表失败,因为这是一个原子性的操作,k1失败了就都会失败
127.0.0.1:6379> get k4
(nil) #k4为nil,说明k4没有设置成功
127.0.0.1:6379> get k1
"v1"
127.0.0.1:6379> keys *
1) "k1"
2) "k3"
3) "k2"
#对象
#set user:id {name:jeff,age:2} 设置一个key为user:1,值为json字符串来保存的一个对象
#这里的key是一个巧妙的设计:user:{id}
127.0.0.1:6379> set user:1 {name:jeff,age:3}
OK
127.0.0.1:6379> get user:1
"{name:jeff,age:3}"
127.0.0.1:6379>
#同时设置user:1的name和age
127.0.0.1:6379> mset user:1:name jeff user:1:age 2
OK
127.0.0.1:6379> mget user:1:name user:1:age
1) "jeff"
2) "2"
127.0.0.1:6379> mget user:1
1) (nil)
#########################################################################################################
#getset 先get然后set
127.0.0.1:6379> getset db redis #设置db的值,并返回设置前的值,如果原来不存在,就返回nil
(nil)
127.0.0.1:6379> get db
"redis"
127.0.0.1:6379> getset db mongodb #设置db的值,并返回设置前的值,设置前的值为redis
"redis"
127.0.0.1:6379> get db #设置后的值,变成了mongodb
"mongodb"
#########################################################################################################
String的使用场景:
- 计数器
- 统计多单位的数量
- 粉丝数
- 对象缓存存储
注意:value除了是字符串还可以是数字。
2.2 List 列表类型
在Redis里面,可以把list当成栈、队列、阻塞队列。
所有的list命令都是以l开头的。
#########################################################################################################
#push 插入元素
127.0.0.1:6379> lpush list one #将一个值或者多个值,从左边依次插入到列表的头部
(integer) 1
127.0.0.1:6379> lpush list tow
(integer) 2
127.0.0.1:6379> lpush list three
(integer) 3
127.0.0.1:6379> lrange list 0 -1 #获取列表中的所有值
1) "three"
2) "tow"
3) "one"
127.0.0.1:6379> lrange list 0 1 #获取列表中从0到1的值
1) "three"
2) "tow"
127.0.0.1:6379> rpush list four #将一个值或者多个值,从右边依次插入到列表的尾部
(integer) 4
127.0.0.1:6379> lrange list 0 -1
1) "three"
2) "tow"
3) "one"
4) "four"
#########################################################################################################
#pop 移除元素
127.0.0.1:6379> lrange list 0 -1
1) "three"
2) "tow"
3) "one"
4) "four"
127.0.0.1:6379> lpop list #从左边移除一个元素
"three"
127.0.0.1:6379> lrange list 0 -1
1) "tow"
2) "one"
3) "four"
127.0.0.1:6379> rpop list 2 #从右边移除2个元素,指定数量为2
1) "four"
2) "one"
127.0.0.1:6379> lrange list 0 -1
1) "tow"
#########################################################################################################
#lindex
127.0.0.1:6379> lrange list 0 -1
1) "one"
2) "tow"
127.0.0.1:6379> lindex list 1 #通过下表获取值,1表示第2个元素
"tow"
#llen 获取list的长度
127.0.0.1:6379> lrange list 0 -1
1) "one"
2) "tow"
127.0.0.1:6379> llen list
(integer) 2
#lrem 移除元素
127.0.0.1:6379> lrange list 0 -1
1) "one"
2) "tow"
127.0.0.1:6379> lpush list one
(integer) 3
127.0.0.1:6379> lrange list 0 -1
1) "one"
2) "one"
3) "tow"
127.0.0.1:6379> lrem list 2 one #移除2个one元素
(integer) 2
127.0.0.1:6379> lrange list 0 -1
1) "tow"
#########################################################################################################
#trim 修剪
127.0.0.1:6379> rpush mylist hello hello1 hello2 hello3#从右边依次插入元素
(integer) 4
127.0.0.1:6379> lrange mylist 0 -1
1) "hello"
2) "hello1"
3) "hello2"
4) "hello3"
127.0.0.1:6379> ltrim mylist 0 1 #通过下表指定截取从0到1位置的元素,并将list的值改为截取后的
OK
127.0.0.1:6379> lrange mylist 0 -1
1) "hello"
2) "hello1"
#rpoplpush 从一个list中移除后面的一个元素并加入到新的list中
127.0.0.1:6379> lrange mylist 0 -1
1) "hello"
2) "hello1"
3) "hello2"
127.0.0.1:6379> rpoplpush mylist otherlist #从右边移除hello2元素到新创建的otherlist中
"hello2"
127.0.0.1:6379> lrange mylist 0 -1
1) "hello"
2) "hello1"
127.0.0.1:6379> lrange otherlist 0 -1
1) "hello2"
#########################################################################################################
#1set 将列表中指定下标的值替换为另外一个值,更新操作
127,0.0.1:6379> EXISTS 1ist # 判断这个列表是否存在
(integer) O
127,0.0.1:6379>1set 1ist 0 item # 如果不存在列表我们去更新就会报错
(error) ERR no such key
127,0.0.1:6379>lpush list valuel
(integer) 1
127.0.0.1:6379> LRANGE 1ist 0 0
1) "valuel"
127,0.0.1:6379>lset list 0 item # 如果存在,更新当前下标的值
OK
127,0.0.1:6379>LRANGE 1ist 0 0
1) "item"
127.0.0.1:6379> 1set 1ist 1 other
(error) ERR index out of range
#########################################################################################################
#linsert 将某个元素插入到列表中指定元素的前面或者后面
127.0.0.1:6379> lrange mylist 0 -1
1) "hello"
2) "world"
127.0.0.1:6379> linsert mylist before world other #将other元素插入到mylist中的world元素前面
(integer) 3
127.0.0.1:6379> lrange mylist 0 -1
1) "hello"
2) "other"
3) "world"
127.0.0.1:6379> linsert mylist after world new #将new元素插入到mylist中的world元素后面
(integer) 4
127.0.0.1:6379> lrange mylist 0 -1
1) "hello"
2) "other"
3) "world"
4) "new"
#########################################################################################################
-
Redis中的list是一个链表,左边右边都可以插入值
-
如果key不存在,自动创建新的链表
-
如果移除了所有的值,那么链表也将不存在
-
在两边插入或者改动值,效率最高,中间元素,相对效率低一点
运用:
- 消息队列(lpush rpop)(先进先出)
- 栈(lpush lpop)(先进后出)
2.3 Set 集合类型
#########################################################################################################
#set是无序不重复集合
#添加元素
127.0.0.1:6379> sadd myset hellp #往set集合中添加元素
(integer) 1
127.0.0.1:6379> sadd myset jeff
(integer) 1
127.0.0.1:6379> smembers myset #查看指定set集合的所有元素
1) "jeff"
2) "hellp"
127.0.0.1:6379> sismember myset hello #判断某个值是不是在set集合中
(integer) 0 #返回0表示没有在集合中
127.0.0.1:6379> sismember myset hellp #返回1表示在集合中
(integer) 1
#移除元素
127.0.0.1:6379> smembers myset
1) "jeff"
2) "hellp"
127.0.0.1:6379> scard myset
(integer) 2
127.0.0.1:6379> srem myset hellp #移除set中指定元素
(integer) 1
127.0.0.1:6379> smembers myset
1) "jeff"
#抽取元素
127.0.0.1:6379> smembers myset
1) "jeff2"
2) "jeff1"
3) "jeff"
4) "jeff3"
127.0.0.1:6379> srandmember myset #随机从myset中抽取一个元素
"jeff1"
127.0.0.1:6379> srandmember myset
"jeff2"
127.0.0.1:6379> srandmember myset 2 #随机从myset中抽取2个元素
1) "jeff1"
2) "jeff2"
127.0.0.1:6379> srandmember myset 2 #随机从myset中抽取2个元素
1) "jeff2"
2) "jeff3"
#########################################################################################################
#随机移除元素
#移除指定的元素到目标set中
127.0.0.1:6379> smembers myset
1) "jeff2"
2) "jeff1"
3) "jeff"
4) "jeff3"
127.0.0.1:6379> spop myset #随机移除一个myset中的元素
"jeff"
127.0.0.1:6379> smembers myset
1) "jeff2"
2) "jeff1"
3) "jeff3"
127.0.0.1:6379> smove myset myset2 jeff1 #将myset中的jeff1移除到myset2中
(integer) 1
127.0.0.1:6379> smembers myset
1) "jeff2"
2) "jeff3"
127.0.0.1:6379> smembers myset2
1) "jeff1"
#########################################################################################################
#集合的运算
#- 差集 sdiff
#- 交集 sinter
#- 并集 sunion
127.0.0.1:6379> sadd key1 a b c
(integer) 3
127.0.0.1:6379> smembers key1
1) "c"
2) "b"
3) "a"
127.0.0.1:6379> sadd key2 c d e
(integer) 3
127.0.0.1:6379> smembers key2
1) "d"
2) "e"
3) "c"
127.0.0.1:6379> sdiff key1 key2 #查看key1,key2的差集
1) "a"
2) "b"
127.0.0.1:6379> sinter key1 key2 #查看key1,key2的交集
1) "c"
127.0.0.1:6379> sunion key1 key2 #查看key1,key2的并集
1) "c"
2) "a"
3) "b"
4) "d"
5) "e"
#########################################################################################################
2.4 Hash 哈希类型
#########################################################################################################
#Map 集合,key-Map(key-value)
127.0.0.1:6379> hset myhash key1 jeff #set一个具体的key value
(integer) 1
127.0.0.1:6379> hget myhash key1 #获取指定key的值
"jeff"
127.0.0.1:6379> hmset myhash key1 jeff1 key2 jeff2 #set多个值
OK
127.0.0.1:6379> hmget myhash key1 key2 #get多个值
1) "jeff1"
2) "jeff2"
127.0.0.1:6379> hgetall myhash #获取全部的数据
1) "key1"
2) "jeff1"
3) "key2"
4) "jeff2"
127.0.0.1:6379> hdel myhash key1 #删除hash指定的key字段,对应的value值也就消失了
(integer) 1
127.0.0.1:6379> hgetall myhash
1) "key2"
2) "jeff2"
#########################################################################################################
127.0.0.1:6379> hgetall myhash
1) "key2"
2) "jeff2"
127.0.0.1:6379> hlen myhash #获取myhash的长度
(integer) 1
127.0.0.1:6379> hexists myhash key1 #判断指定hash中是否存在某个field
(integer) 0 #返回0表示不存在
127.0.0.1:6379> hexists myhash key2
(integer) 1 #返回1表示存在
127.0.0.1:6379> hkeys myhash #获取指定hash中所有的key
1) "key2"
127.0.0.1:6379> hvals myhash #获取指定hash中所有的value
1) "jeff2"
#########################################################################################################
#incr
127.0.0.1:6379> hset myhash field1 5
(integer) 1
127.0.0.1:6379> hincrby myhash field1 1 #指定field1的值增长1
(integer) 6
127.0.0.1:6379> hincrby myhash field1 -1 #指定增长-1,相当于decr,但是没有hdecrby的命令
(integer) 5
127.0.0.1:6379> hget myhash field1
"5"
127.0.0.1:6379> hsetnx myhash field1 4 #myhash中如果存在field1,则设置值为4
(integer) 0 #因为已经有field1,所以设置失败
127.0.0.1:6379> hget myhash field1
"5"
127.0.0.1:6379> hsetnx myhash field2 3 #myhash中没有field2
(integer) 1 #设值成功
127.0.0.1:6379> hget myhash field2
"3"
#########################################################################################################
运用:
hash更适合于对象的存储,String更适合字符串的存储。
尤其是用户信息之类的,经常变动的信息,user,name,age。
2.5 Zset 有序集合
#########################################################################################################
#在set的基础上增加了一个值
127.0.0.1:6379> zadd myset 1 one #添加一个元素
(integer) 1
127.0.0.1:6379> zadd myset 2 tow 3 three #添加多个元素
(integer) 2
127.0.0.1:6379> zrange myset 0 -1 #列出所有元素
1) "one"
2) "tow"
3) "three"
127.0.0.1:6379> zadd salary 2500 xiaohong
(integer) 1
127.0.0.1:6379> zadd salary 5000 zhangsan
(integer) 1
127.0.0.1:6379> zadd salary 500 jeff
(integer) 1
127.0.0.1:6379> zrange salary 0 -1 #升序显示所有用户信息
1) "jeff"
2) "xiaohong"
3) "zhangsan"
127.0.0.1:6379> zrevrange salary 0 -1 #降序显示所有用户信息
1) "zhangsan"
2) "xiaohong"
3) "jeff"
127.0.0.1:6379> zrangebyscore salary -inf +inf #升序显示所有用户
1) "jeff"
2) "xiaohong"
3) "zhangsan"
127.0.0.1:6379> zrangebyscore salary -inf +inf withscores #升序显示所有用户信息并加上分数即工资
1) "jeff"
2) "500"
3) "xiaohong"
4) "2500"
5) "zhangsan"
6) "5000"
127.0.0.1:6379> zrangebyscore salary -inf 2500 withscores #升序显示工资小于等于2500的所有用户信息及分数
1) "jeff"
2) "500"
3) "xiaohong"
4) "2500"
#########################################################################################################
#移除元素 zrem
#显示元素个数 zcard
127.0.0.1:6379> zrange salary 0 -1
1) "jeff"
2) "xiaohong"
3) "zhangsan"
127.0.0.1:6379> zrem salary xiaohong #移除指定元素
(integer) 1
127.0.0.1:6379> zrange salary 0 -1 #显示所有元素
1) "jeff"
2) "zhangsan"
127.0.0.1:6379> zcard salary #显示有序集合中的元素个数
(integer) 2
#########################################################################################################
#获取指定区间的成员数量 zcount
127.0.0.1:6379> zadd myset 1 hello
(integer) 1
127.0.0.1:6379> zadd myset 2 world 3 jeff
(integer) 2
127.0.0.1:6379> zcount myset 1 3 #获取分数在1到3之间的成员数量
(integer) 3
127.0.0.1:6379> zcount myset 1 2
(integer) 2
#########################################################################################################
运用:
- 排行榜
- 工资排序、学生成绩排序等
2.6 geospatial 地理位置
Redis 的 Geo 功能可以推算地理位置的信息,两地之间的距离,方圆几里的人。
| 命令 | 描述 |
|---|---|
| Redis GEOHASH 命令 | 返回一个或多个位置元素的 Geohash 表示 |
| Redis GEOPOS 命令 | 从key里返回所有给定位置元素的位置(经度和纬度) |
| Redis GEODIST 命令 | 返回两个给定位置之间的距离 |
| Redis GEORADIUS 命令 | 以给定的经纬度为中心, 找出某一半径内的元素 |
| Redis GEOADD 命令 | 将指定的地理空间位置(纬度、经度、名称)添加到指定的key中 |
| Redis GEORADIUSBYMEMBER 命令 | 找出位于指定范围内的元素,中心点是由给定的位置元素决定 |
GEOADD
127.0.0.1:6379> geoadd china:city 116.40 39.90 beijing #添加一个位置
(integer) 1
127.0.0.1:6379> geoadd china:city 121.47 31.23 shanghai
(integer) 1
127.0.0.1:6379> geoadd china:city 106.50 29.53 chongqing 114.05 22.52 shenzhen #添加多个位置
(integer) 2
127.0.0.1:6379> geoadd china:city 120.16 30.24 hangzhou 108.96 34.26 xian
(integer) 2
GETPOS
127.0.0.1:6379> geopos china:city beijing #获取指定城市的经度和纬度
1) 1) "116.39999896287918091"
2) "39.90000009167092543"
127.0.0.1:6379> geopos china:city hangzhou
1) 1) "120.1600000262260437"
2) "30.2400003229490224"
GEODIST
如果两个位置之间的其中一个不存在, 那么命令返回空值。
指定单位的参数 unit 必须是以下单位的其中一个:
- m 表示单位为米。
- km 表示单位为千米。
- mi 表示单位为英里。
- ft 表示单位为英尺。
如果用户没有显式地指定单位参数, 那么 GEODIST 默认使用米作为单位。
127.0.0.1:6379> geodist china:city beijing shanghai #北京到上海的距离,默认单位m
"1067378.7564"
127.0.0.1:6379> geodist china:city beijing shanghai km #北京到上海的距离,单位km
"1067.3788"
GEORADIUS
127.0.0.1:6379> georadius china:city 110 30 500 km #显示110 30位置周边500km的城市
1) "chongqing"
2) "xian"
127.0.0.1:6379> georadius china:city 110 30 500 km withcoord withdist #显示110 30位置周边500km的城市,带出经纬度及距离
1) 1) "chongqing"
2) "341.9374"
3) 1) "106.49999767541885376"
2) "29.52999957900659211"
2) 1) "xian"
2) "483.8340"
3) 1) "108.96000176668167114"
2) "34.25999964418929977"
127.0.0.1:6379> georadius china:city 110 30 500 km withcoord withdist count 1 #指定数量查询,只显示1个城市
1) 1) "chongqing"
2) "341.9374"
3) 1) "106.49999767541885376"
2) "29.52999957900659211"
GEORADIUSBYMEMBER
#找出位于指定元素周围的其他元素
127.0.0.1:6379> georadiusbymember china:city beijing 1000 km
1) "beijing"
2) "xian"
127.0.0.1:6379> georadiusbymember china:city beijing 400 km
1) "beijing"
GEOHASH
#将二维的经纬度转换为一维的字符串,如果两个字符串越接近,那么距离就越近
127.0.0.1:6379> geohash china:city beijing chongqing
1) "wx4fbxxfke0"
2) "wm5xzrybty0"
GEO底层实现的原理就是Zset,我们可以使用Zset命令来操作GEO
127.0.0.1:6379> zrange china:city 0 -1 #查看地图中全部的元素
1) "chongqing"
2) "xian"
3) "shenzhen"
4) "hangzhou"
5) "shanghai"
6) "beijing"
127.0.0.1:6379> zrem china:city beijing #移除指定的元素
(integer) 1
127.0.0.1:6379> zrange china:city 0 -1
1) "chongqing"
2) "xian"
3) "shenzhen"
4) "hangzhou"
5) "shanghai"
2.7 Hyperloglog 基数统计
什么是基数?
不重复的元素。
**优点:**占用的内存是固定的,2^64不同的元素的基数,只需要消耗12KB的内存。
**注意:**有0.81%的错误率。
如果从内存的角度来比较的话,Hyperloglog是首选。
127.0.0.1:6379> pfadd mykey a b c d e f g h i j #创建第一组元素
(integer) 1
127.0.0.1:6379> pfcount mykey #统计第一组元素的基数数量
(integer) 10
127.0.0.1:6379> pfadd mykey a #mykey中已经有a元素,添加失败
(integer) 0
127.0.0.1:6379> pfcount mykey #mykey中基数数量还是10
(integer) 10
127.0.0.1:6379> pfadd mykey2 i j z x c v b n m #创建第二组元素
(integer) 1
127.0.0.1:6379> pfcount mykey2
(integer) 9
127.0.0.1:6379> pfmerge mykey3 mykey mykey2 #合并mykey mykey2 => mykey3 并集
OK
127.0.0.1:6379> pfcount mykey3 #看并集的基数数量
(integer) 15
运用:
网页的UV(一个人访问一个网站多次,但是还是算作一个人)。
如果不允许容错,就必须使用set或者自己的数据类型。
2.8 Bitmap 位图场景
位存储
使用bitmap 来记录周一到周日的打卡:
127.0.0.1:6379> setbit sign 0 1 #设置周一为打卡
(integer) 0
127.0.0.1:6379> setbit sign 1 0 #设置周二为未打卡
(integer) 0
127.0.0.1:6379> setbit sign 2 0
(integer) 0
127.0.0.1:6379> setbit sign 3 1
(integer) 0
127.0.0.1:6379> setbit sign 4 1
(integer) 0
127.0.0.1:6379> setbit sign 5 0
(integer) 0
127.0.0.1:6379> setbit sign 6 0 #设置周日为未打卡
(integer) 0
127.0.0.1:6379> getbit sign 3 #查询周四是否打卡
(integer) 1 #返回1为打卡状态
127.0.0.1:6379> getbit sign 6
(integer) 0
127.0.0.1:6379> bitcount sign #统计所有打卡的天数
(integer) 3
3. Redis 基本的事务操作
3.1 Redis 事务的本质
一组命令的集合,一个事务中的所有命令都会被序列化,在事务的执行过程中,会按照顺序执行。
**注意:**Redis事务没有隔离级别的概念,所有的命令在事务中,并没有直接被执行,只有发起执行命令的时候才会执行。
Redis 单条命令是保证原子性的,但是事务不保证原子性。
Redis 的事务:
- 开启事务(multi)
- 命令入队(....)
- 执行事务(exec)
3.2 基本事务操作
正常执行事务
127.0.0.1:6379> multi #开启事务
OK
127.0.0.1:6379(TX)> set k1 v1 #设置值
QUEUED #命令入队
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> get k2 #取k2的值
QUEUED #命令入队
127.0.0.1:6379(TX)> set k3 v3
QUEUED
127.0.0.1:6379(TX)> exec #执行事务
1) OK #按命令入队时的顺序执行的结果
2) OK
3) "v2"
4) OK
放弃事务
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set k1 v1
QUEUED
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> set k4 v4
QUEUED
127.0.0.1:6379(TX)> discard #放弃事务
OK
127.0.0.1:6379> get k1
(nil)
编译型异常
#(代码有问题,命令有错,进对列的时候就会报错)---事务中的所有命令都不会被执行
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set k1 v1
QUEUED
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> set k3 v3
QUEUED
127.0.0.1:6379(TX)> getset k3 #错误的命令
(error) ERR wrong number of arguments for 'getset' command
127.0.0.1:6379(TX)> set k4 v4
QUEUED
127.0.0.1:6379(TX)> exec #执行事务报错
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6379> get k1 #取不到结果,说明所有的命令都不会被执行
(nil)
运行时异常
#一条或几条命令执行失败,其他的命令可以正常执行,其他的命令会执行成功
127.0.0.1:6379> set k1 v1
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> incr k1 #对字符串值自增1
QUEUED
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> set k3 v3
QUEUED
127.0.0.1:6379(TX)> get k3
QUEUED
127.0.0.1:6379(TX)> exec
1) (error) ERR value is not an integer or out of range #字符串自增的命令报错
2) OK #其他命令执行成功
3) OK
4) "v3"
127.0.0.1:6379> get k3
"v3"
4. Redis 实现乐观锁
4.1 悲观锁
很悲观,无论做什么都加锁。
特点:可以完全保证数据的独占性和正性,因为每次请求都会先对数据进行加锁, 然后进行数据操作,最后再解锁,而加锁释放锁的过程会造成消耗,所以性能不高。
4.2 乐观锁
采用version的机制。
特点:乐观锁是一种并发类型的锁,其本身不对数据进行加锁通而是通过业务实现锁的功能,不对数据进行加锁就意味着允许多个请求同时访问数据,同时也省掉了对数据加锁和解锁的过程,这种方式因为节省了悲观锁加锁的操作,所以可以一定程度的的提高操作的性能,不过在并发非常高的情况下,会导致大量的请求冲突,冲突导致大部分操作无功而返而浪费资源,所以在高并发的场景下,乐观锁的性能却反而不如悲观锁。
Redis 监视测试
正常执行成功
127.0.0.1:6379> set money 100
OK
127.0.0.1:6379> set out 0
OK
127.0.0.1:6379> watch money #监视money对象
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> decrby money 20
QUEUED
127.0.0.1:6379(TX)> incrby out 20
QUEUED
127.0.0.1:6379(TX)> exec #事务正常结束,期间数据没有发生变动,执行成功
1) (integer) 80
2) (integer) 20
测试多线程修改值,使用watch当做Redis的乐观锁操作
127.0.0.1:6379> watch money #监视money对象
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> decrby money 10
QUEUED
127.0.0.1:6379(TX)> incrby out 10
QUEUED
127.0.0.1:6379(TX)> exec #执行之前,另外一个线程修改了money的值,导致事务执行失败
(nil)
5.通过Jedis操作Redis
5.1 导入对应的依赖
<dependencies>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>4.1.0</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.61</version>
</dependency>
</dependencies>
5.2 编码测试
public static void main(String[] args) {
Jedis jedis = new Jedis("127.0.0.1",6379);
System.out.println(jedis.ping());
jedis.close();
}
6. SpringBoot 集成Redis
6.1 导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
6.2 Redis 配置
spring.redis.host=localhost
spring.redis.port=6379
6.3 测试
@Test
void contextLoads() {
redisTemplate.opsForValue().set("test", "testValue");
redisTemplate.opsForList().leftPush("testList", "杨过");
System.out.println(redisTemplate.opsForValue().get("test"));
System.out.println(redisTemplate.opsForList().leftPop("testList"));
}
7. 自定义RedisTemplate
@Configuration
public class RedisConfg {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance,
ObjectMapper.DefaultTyping.NON_FINAL,
JsonTypeInfo.As.WRAPPER_ARRAY);
jackson2JsonRedisSerializer.setObjectMapper(om);
template.setKeySerializer(stringRedisSerializer);
template.setHashKeySerializer(stringRedisSerializer);
template.setValueSerializer(jackson2JsonRedisSerializer);
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
}
8. Redis.conf 详解
-
配置文件对大小写不敏感
# 1k => 1000 bytes # 1kb => 1024 bytes # 1m => 1000000 bytes # 1mb => 1024*1024 bytes # 1g => 1000000000 bytes # 1gb => 1024*1024*1024 bytes # # units are case insensitive so 1GB 1Gb 1gB are all the same. -
INCLUDES可以引用其他的配置文件
################################## INCLUDES ################################### # Include one or more other config files here. This is useful if you # have a standard template that goes to all Redis servers but also need # to customize a few per-server settings. Include files can include # other files, so use this wisely. # # Note that option "include" won't be rewritten by command "CONFIG REWRITE" # from admin or Redis Sentinel. Since Redis always uses the last processed # line as value of a configuration directive, you'd better put includes # at the beginning of this file to avoid overwriting config change at runtime. # # If instead you are interested in using includes to override configuration # options, it is better to use include as the last line. # # include /path/to/local.conf # include /path/to/other.conf -
NETWORK网络
bind 0.0.0.0 -::1 #绑定ip protected-mode yes #是否开启保护模式 port 6379 #端口 -
GENERAL通用
daemonize yes #以守护进程的方式运行 pidfile /var/run/redis_6379.pid #如果以后台的方式运行,我们就需要指定一个pid文件 #日志信息 # Specify the server verbosity level. # This can be one of: # debug (a lot of information, useful for development/testing) # verbose (many rarely useful info, but not a mess like the debug level) # notice (moderately verbose, what you want in production probably) # warning (only very important / critical messages are logged) loglevel notice #指定日志级别为notice,生产环境用,一般不用改 # Specify the log file name. Also the empty string can be used to force # Redis to log on the standard output. Note that if you use standard # output for logging but daemonize, logs will be sent to /dev/null logfile "" #日志文件的位置名 # Set the number of databases. The default database is DB 0, you can select # a different one on a per-connection basis using SELECT <dbid> where # dbid is a number between 0 and 'databases'-1 databases 16 #数据库数量,默认16个 always-show-logo no #是否总是显示Logo -
SNAPSHOTTING快照
持久化,在规定的时间内,执行了多少次的操作,则会持久化到文件.rdb,.aof
Redis是内存数据库,如果没有持久化操作,那么数据断电就会丢失。
# Unless specified otherwise, by default Redis will save the DB: # * After 3600 seconds (an hour) if at least 1 key changed # * After 300 seconds (5 minutes) if at least 100 keys changed # * After 60 seconds if at least 10000 keys changed # # You can set these explicitly by uncommenting the three following lines. # # save 3600 1 #如果3600s内,至少有1个key进行了修改,就进行持久化操作 # save 300 100 #如果300s内,至少有100个key进行了修改,就进行持久化操作 # save 60 10000 #如果60s内,至少有10000个key进行了修改,就进行持久化操作 #以上这个需要自己根据实际情况定义 stop-writes-on-bgsave-error yes #持久化如果出错,是否还要继续工作 rdbcompression yes #是否压缩rdb文件,需要消耗一些cpu资源 rdbchecksum yes #保存rdb文件的时候,进行错误的检查校验 dir ./ #rdb文件保存的目录,默认就是当前目录下 -
REPLICATION复制,后面讲主从复制的时候再专门讲解
-
SECURITY安全
# requirepass foobared #默认没有设置的,可以修改,如下: requirepass 123456 -
CLIENTS客户端限制
# maxclients 10000 #设置能连接上redis的最大客户端的数量 # maxmemory <bytes> #Redis配置最大的内存容量 # The default is: # maxmemory-policy noeviction #内存达到上限后的处理策略,默认是永不过期 #有以下几种方式选择: # volatile-lru -> Evict using approximated LRU, only keys with an expire set. # allkeys-lru -> Evict any key using approximated LRU. # volatile-lfu -> Evict using approximated LFU, only keys with an expire set. # allkeys-lfu -> Evict any key using approximated LFU. # volatile-random -> Remove a random key having an expire set. # allkeys-random -> Remove a random key, any key. # volatile-ttl -> Remove the key with the nearest expire time (minor TTL) # noeviction -> Don't evict anything, just return an error on write operations. -
APPEND ONLY MODE aof配置
appendonly no #默认是不开启aof模式的,默认使用rdb方式持久化,在大部分情况下,rdb完全够用 appendfilename "appendonly.aof" #持久化的文件的名字 #同步策略配置 # appendfsync always #每次修改都会sync,消耗性能 appendfsync everysec #默认,每秒执行一次sync,可能会丢失这1s的数据 # appendfsync no #不执行sync,这个时候操作系统自己同步数据,速度最快
9. 持久化之RDB操作
什么是RDB(Redis DataBase)
在指定的时间间隔内,将内存的数据集快照写入磁盘,生成Snapshot快照,它恢复时将快照文件直接读到内存里。
Redis会单独创建(fork)一个子进程来进行持久化,先将数据写入到一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。
整个过程中,主进程是不进行任何IO操作的,确保了极高的性能。
触发机制
- save的规则满足的情况下,会自动触发rdb规则
- 执行
flushall命令,也会触发rdb规则 - 退出Redis,也会产生rdb规则
备份就自动生成一个dump.rdb文件。
如何回复rdb文件
-
只需要将rdb文件放在Redis启动目录就可以,redis启动的时候就会自动检查dump.rdb,并恢复其中的数据
-
查看rdb文件需要存放的位置
127.0.0.1:6378> config get dir #通过config get dir命令查看dir的目录 1) "dir" 2) "/usr/local/bin" #这个目录就是dump.rdb文件的目录,启动就会自动恢复其中的数据
优点:
- 适合大规模的数据恢复
- 对数据的完整性要求不高
缺点:
- 需要一定的事件间隔进行操作,如果Redis意外宕机了,这个最后一次修改的数据就没有了
- fork进程的时候,会占用一定的内存空间
10. 持久化之AOF操作
Append Only File
将所有的命令都记录下来,history,恢复的时候会把所有的命令重新执行一次。
Aof保存的是appendonly.aof文件。
以日志的形式来记录每个写的操作,将Redis执行过的所有指令记录下来(读操作不记录),只许追加文件不能改写文件,Redis启动之后会读取该文件重新构建数据。
启动方式
默认是不开启的,需要手动进行配置,我们只需要将appendonly改为yes就开启了aof
重启,Redis就可以生效了。
注意:
如果aof文件有错误,那么Redis就启动不了,我们需要修复这个aof文件。
Redis提供了工具:redis-check-aof --fix
通过下面的指令修复:
redis-check-aof --fix appendonly.aof
优点:
- 每一次修改都会同步,文件的完整性更好
- 每秒同步一次,可能会丢失1s的数据
- 从不同步,效率最高
缺点:
- 相对于数据文件来说,aof远远大于rdb,修复速度也更慢
- aof运行效率也要比rdb慢,所以我们redis默认的配置就是rdb持久化
扩展:
-
只做缓存,如果只希望数据在服务器运行时候存在,可以不适用任何持久化
-
同时开启两种持久化方式
- 当redis重启的时候会优先载入AOF文件来恢复原始的数据,因为在通常情况下AOF文件保存的数据集要比RDB文件保存的数据集要完整
- RDB的数据不实时,同时使用两者时服务器重启也只会找AOF文件,那要不要只使用AOF呢?建议不要,因为RDB更适合用于备份数据库,快速重启,而且不会有AOF可能潜在的bug,留着作为一个万一的手段。
-
性能建议
-
因为RDB文件只用做后备用途,建议只在Slave上持久化RDB文件,而且只要15分钟备份一次就够了,只保留
save 900 1这条规则; -
如果Enable AOF,好处是在最恶劣的情况下也只会丢失不超过2s的数据,启动脚本较简单只load自己的AOF文件就可以了,代价一就是带来了持续的IO,二是AOF rewrite的最后将rewrite过程中产生的新数据写到新文件是不可避免的,只要硬盘许可,应该尽量减少AOF rewrite的频率,AOF重写的基础大小默认值64M太小,可以设到5G以上,默认超过原大小100%大小重写可以改到适当的数值。
auto-aof-rewrite-percentage 100 auto-aof-rewrite-min-size 64mb -
如果不Enable AOF,仅靠Master-Slave Replication 实现高可用性也可以,能省掉一大笔IO,也减少了rewrite时带来的系统波动。代价是如果Master/Slave同时倒掉(断电),会丢失十几分钟的数据,启动脚本也要比较两个Master/Slave中的RDB文件,载入较新的那个,微博就是这种架构。
-
11. Redis 发布订阅
Redis 发布订阅(pub/sub)是一种消息通信模式:发送者发送消息,订阅者接收消息。
Redis 客户端可以订阅任意数量的频道。
订阅/发布消息图:
第一个:消息发送者;第二个:频道;第三个:消息订阅者
测试:
订阅端
127.0.0.1:6378> subscribe jeffshuo #订阅一个频道 jeffshuo
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "jeffshuo"
3) (integer) 1
#等待读取推送的消息
1) "message" #消息
2) "jeffshuo" #消息所在的频道
3) "you are so cool" #消息的具体内容
发送端
127.0.0.1:6378> publish jeffshuo "you are so cool" #发布者发布消息到jeffshuo频道
(integer) 1
运用:
- 实时消息系统
- 实时聊天(频道当做聊天室,将信息回显给所有人即可)
- 订阅,关注系统
稍微复杂的场景我们就使用消息中间件MQ
12. 主从复制
12.1 概念
主从复制,是指将一台Redis服务器的数据,复制到其他的Redis服务器。前者称为主节点(master/leader),后者称为从节点(slave/follower);数据的复制是单向的,只能由主节点到从节点。Master以写为主,Slave以读为主。 默认情况下,每台Redis服务器都是主节点;且一个主节点可以有多个从节点(或没有从节点),但一个从节点只能有一个主节点。
主从复制的作用主要包括:
1、数据冗余:主从复制实现了数据的热备份,是持久化之外的-种数据冗余方式。
2、故障恢复:当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复;实际上是一种服务的冗余。
3、负载均衡:在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务(即写Redis数据时应用主节点,读Redis数据时应用连接从节点),分担服务器负载;尤其是在写少读多的场景下,通过多个从节点分担读负载,可以大大提高Redis服务器的并发量。
4、高可用基石:除了上述作用以外,主从复制还是哨兵和集群能够实施的基础,因此说主从复制是Redis高可用的基础。
一般来说,要将Redis运用于工程项目中,只使用一台Redis是万万不能的,原因如下:
1、从结构上,单个Redis服务器会发生单点故障,并且一台服务器需要处理所有的请求负栽,压力较大
2、从容量上,单个Redis服务器内存容量有限,就算一台Redis服务器内存容置为256G,也不能将所有内存用作Redis存储内存,==一般来说,单台Redis最大使用内存不应该超过20G。==
电商网站上的商品,一般都是一次上传,无数次浏览的说专业点也就是“多读少写”。
对于这种场景,我们可以使用如下这种架构:
主从复制,读写分离!80% 的情况下都是在进行读操作,减缓服务器的压力,架构中经常使用:一主二从!
12.2 环境搭建
只配置从库,不用配置主库。
127.0.0.1:6378> info replication #查看当前库的信息
# Replication
role:master #角色 master
connected_slaves:0
master_failover_state:no-failover
master_replid:b8b072927ee17c72e7cb1faa935ac992115f2a72
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:0
second_repl_offset:-1
repl_backlog_active:0
repl_backlog_size:1048576
repl_backlog_first_byte_offset:0
repl_backlog_histlen:0
复制3个配置文件,然后修改对应的信息
- 端口
- pid名称
- log文件名字
- dump.rdb名字
- 如果主机是配置了Redis密码的,那么还要配置
masterauth属性,主机从机都要设置且要跟主机保持一致
以第一个配置文件为例:
[root@iZ2zeefawmgg7o4jarp3hqZ myRedisConfig]# ls
redis79.conf redis80.conf redis81.conf redis.conf
#redis79.conf 改如下几个地方:
port 6379
pidfile /var/run/redis_6379.pid
logfile "6379.log"
dbfilename dump6379.rdb
masterauth <master-password> #如果主机Redis配置了密码,需要配置
一主二从
默认情况下,每台Redis服务器都是主节点,我们一般情况下只用配置从机就好了。
认老大!
- 命令的方式
- 修改conf配置文件
#命令的方式:slaveof ip port(z主机的ip和端口)
127.0.0.1:6380> slaveof 127.0.0.1 6379 #执行slaveof命令,将6379的端口作为主机
OK
127.0.0.1:6380> info replication #重新查看主从信息
# Replication
role:slave #角色从主机变为了从机
master_host:127.0.0.1 #认的老大,主机的ip
master_port:6379 #认的老大,主机的端口
master_link_status:up
master_last_io_seconds_ago:7
master_sync_in_progress:0
slave_read_repl_offset:14
slave_repl_offset:14
slave_priority:100
slave_read_only:1
replica_announced:1
connected_slaves:0
master_failover_state:no-failover
master_replid:70a9f8e0df3af88981636777b9c6eef1e96489ba
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:14
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:14
#查看主机的信息
127.0.0.1:6379> info replication
# Replication
role:master #角色是主机
connected_slaves:1 #显示从机数量是1
slave0:ip=127.0.0.1,port=6380,state=online,offset=56,lag=1 #从机的配置
master_failover_state:no-failover
master_replid:4c4f10ed51a674a9000c291f6f034716e285d178
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:56
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:56
#如果配置了2个从机,那么主机就会显示两个从机的信息
127.0.0.1:6379> info replication
# Replication
role:master
connected_slaves:2 #显示从机数量是2
slave0:ip=127.0.0.1,port=6380,state=online,offset=476,lag=0
slave1:ip=127.0.0.1,port=6381,state=online,offset=476,lag=0
master_failover_state:no-failover
master_replid:4c4f10ed51a674a9000c291f6f034716e285d178
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:476
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:476
#修改从机的配置文件的方式,永久设置
replicaof <masterip> <masterport>
注意:
主机才能写,从机只能读。
如果是使用命令行来配置的主从,从机如果重启了,从机就会变回主机!只要重新变为从机,立马就会从主机中获取值!
#在从机中进行写的操作,会报错
127.0.0.1:6381> set k2 v2
(error) READONLY You can't write against a read only replica.
测试:主机断开连接,从机依旧连接到主机的,但是没有写操作,这个时候,主机如果回来了,从机依旧可以直接获取到主机写的信息!
主机宕机后手动配置主机
如果主机宕机了,怎么将其中一个从机手动配置为主机呢?
谋朝篡位!
可以在需要变为主机的从机中使用命令slaveof no one命令,让自己变为主机,其他的节点就可以自动连接到最新的这个主节点。
如果老大修复了,那就需要重新连接老大(即重新使用lsaveof masterip masterport)。
12.3 复制原理
Slave 启动成功连接到master 后会发送一个sync同步命令 Master 接到命令,启动后台的存盘进程,同时收集所有接收到的用于修改数据集命令,在后台进程执行完毕之后,master将传送整个数据文件到slave,并完成一次完全同步。
**全量复制:**而slave服务在接收到数据库文件数据后,将其存盘并加载到内存中。 **增量复制:**Master 继续将新的所有收集到的修改命令依次传给slave,完成同步
但是只要是重新连接master一次完全同步(全量复制)将被自动执行!我们的数据一定可以在从机中看到!
12.4 哨兵模式
自动选举老大的模式。
概述
主从切换技术的方法是:当主服务器宕机后,需要手动把一台从服务器切换为主服务器,这就需要人工干预,费事费力,还会造成一段时间内服务不可用。这不是一种推荐的方式,更多时候,我们优先考虑哨兵模式。
Redis从2.8开始正式提供了Sentinel(哨兵)架构来解决这个问题。
谋朝篡位的自动版,能够后台监控主机是否故障,如果故障了根据投票数自动将从库转换为主库。
哨兵模式是一种特殊的模式,首先Redis提供了哨兵的命令,哨兵是一个独立的进程,作为进程,它会独立运行。其原理是哨兵通过发送命令,等待Redis服务器响应,从而监控运行的多个Redis实例。
这里的哨兵有两个作用
- 通过发送命令,让Redis服务器返回监控其运行状态,包括主服务器和从服务器。
- 当哨兵监测到master宕机,会自动将slave切换成master,然后通过发布订阅模式通知其他的从服务器,修改配置文件,让它们切换主机。
然而一个哨兵进程对Redis服务器进行监控,可能会出现问题,为此,我们可以使用多个哨兵进行监控。各个哨兵之间还会进行监控,这样就形成了多哨兵模式。
用文字描述一下故障切换(failover)的过程。假设主服务器宕机,哨兵1先检测到这个结果,系统并不会马上进行failover过程,仅仅是哨兵1主观的认为主服务器不可用,这个现象成为主观下线。当后面的哨兵也检测到主服务器不可用,并且数量达到一定值时,那么哨兵之间就会进行一次投票,投票的结果由一个哨兵发起,进行failover操作。切换成功后,就会通过发布订阅模式,让各个哨兵把自己监控的从服务器实现切换主机,这个过程称为客观下线。这样对于客户端而言,一切都是透明的。
如果主机此时回来了,只能归并到新的主机下,当做从机,这就是哨兵模式的规则。
优点
- 哨兵集群,基于主从复制模式,所有的主从配置优点,它全有
- 主从可以切换,故障可以转移系统的可用性就会更好
- 哨兵模式就是主从模式的升级,手动到自动,更加健壮!
缺点
- Redis不好啊在线扩容的,朱群容量一旦到达上限在线扩容就十分麻烦!
- 实现哨兵模式的配置其实是很麻烦的,里面有很多选择!
13. Redis缓存穿透和雪崩
13.1 缓存穿透
概念
缓存穿透的概念很简单,用户想要查询一个数据,发现redis内存数据库没有,也就是缓存没有命中,于是向持久层数据库查询。发现也没有,于是本次查询失败。当用户很多的时候,缓存都没有命中(秒杀!),于是都去请求了持久层数据库。这会给持久层数据库造成很大的压力,这时候就相当于出现了缓存穿透。
解决方案
布隆过滤器
布隆过滤器是一种数据结构,对所有可能查询的参数以hash形式存储在控制层先进行校验,不符合则丢弃,从而避免了对底层存储系统的查询压力;
缓存空对象
当存储层不命中后,即使返回空对象也将其缓存起来,同时设置一个过期时间,之后再访问这个数据将会从缓存中获取,保护了后端数据源。
但是这种方法会存在两个问题:
- 空值缓存,意味着需要更多的空间去存储;
- 即使设置了过期时间,还是会出现数据不一致的情况,对需要保持一致性的业务会有影响。
13.2 缓存击穿
概念
缓存击穿是指一个key非常热点,在不停的扛着大并发,集中访问,当这个key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像一个屏幕上凿开了一个洞。
当某个key在过期的瞬间,有大量的请求并发访问,这类数据一般是热点数据,由于缓存过期,会同时访问数据库来查询最新数据,并且回写缓存,会导致数据库瞬间压力过大。
解决方案
设置热点数据永不过期
从缓存层面来看,没有设置过期时间,所以不会出现热点key过期后产生的问题。
加互斥锁
分布式锁,使用分布式锁,保证对于每个key同时只有一个线程去查询后端服务,其他线程没有获得分布式锁的权限,因此只需要等待即可。
这种方式将高并发的压力转移到了分布式锁,因此对分布式锁的考验很大。
13.3 缓存雪崩
概念
缓存雪崩,是指在某一个时间段,缓存集中过期失效。
产生雪崩的原因之一,比如在写本文的时候,马上就要到双十二零点,很快就会迎来一波抢购,这波商品时间比较集中的放入了缓存,假设缓存一个小时。那么到了凌晨一点钟的时候,这批商品的缓存就都过期了。而对这批商品的访问查询,都落到了数据库上,对于数据库而言,就会产生周期性的压力波峰。于是所有的请求都会达到存储层,存储层的调用量会暴增,造成存储层也会挂掉的情况。
其实集中过期,倒不是非常致命,比较致命的缓存雪崩,是缓存服务器某个节点宕机或断网。因为自然形成的缓存雪崩,一定是在某个时间段集中创建缓存,这个时候,数据库也是可以顶住压力的。无非就是对数据库产生周期性的压力而已。而缓存服务节点的宕机,对数据库服务器造成的压力是不可预知的,很有可能瞬间就把数据库压垮。
解决方案
redis高可用
这个思想的含义是,既然redis有可能挂掉,那我多增设几台redis,这样一台挂掉之后其他的还可以继续工作,其实就是搭建的集群。(异地多活!)
限流降级
这个解决方案的思想是,在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。
数据预热
数据预热的含义就是在正式部署之前,我先把可能的数据先预先访问一遍,这样部分可能大量访问的数据就会加载到缓存中。在即将发生大并发访问前手动触发加载缓存不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀。