(小狂神)学习方式:基础理论先学习,再将知识融会贯通
读写分离(垂直拆分数据库【各个数据库数据一致】):
分库分表(水平拆分)集群(各个集群数据不一致):
非关系型数据库:NoSQL(Not Only SQL)
关系型数据库:表格、行、列、POI、MySQL…
泛指非关系性数据库,生于 Web2.0 互联网,传统关系型数据库很难应付web2.0。大规模高并发社区…
Redis 是发展最快的 Nosql,键值对形式
NoSql特点:
传统RDBMS(关系型数据库):
- 结构化组织
- SQL
- 数据和关系都存在单独的表中
- 操作操作,数据定义语言
- 严格的一致性
- 事务性
- ...
NoSQL
- 不仅仅是数据
- 没有固定的查询语言
- 键值对存储,列存储,文档存储,图形数据库(社交关系)
- 最终一致性
- CAP定理 和 BASE ***
- 高性能、高可用、高可扩展
- ...
了解:3V + 3高
大数据3V:描述问题
互联网3高:
总结:四个需要学习:Redis、MongoDB、HBase、Neo4j
Redis(Remote Dictinary Server):远程字典服务,是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。
免费且开源,是当下最热门的NoSQL 技术之一,亦被称为 结构化数据库
应用
特性
官网命令查询: http://www.redis.cn/commands.html
127.0.0.1:6379> select 2 // 切换数据库
OK
127.0.0.1:6379[2]> DBSIZE // 查看数据库数据
(integer) 0
让 key n秒后过期自动消除数据(xxx 的 key 在10s后过期):expire xxx 10
Redis 为什么是单线程的?
Redis 是基于内存操作的,CPU 不是 Redis 的性能瓶颈,Redis 的瓶颈是根据及其的内存和网络带宽,既然单线程容易实现,而且CPU不会成为瓶颈。
Redis 为什么单线程还这么快?
Redis 是 C 写的,官方提供的数据 100000+ QPS,不比同样使用 Key-Value 的 Memecache 差!
误区1:高性能的服务器一定是多线程的
误区2:多线程(CPU 上下文会切换)一定比单线程效率高
首先了解处理速度:CPU > 内存 > 硬盘
核心:Redis 将所有的数据放在内存中,所以说使用单线程去操作效率是最高的;多线程(CPU 上下文会切换:非常耗时),对于内存系统来说,没有上下文切换效率是最高的。多次读写都是在一个CPU上
Redis 优势 与 弊端
redis 采用网络IO多路复用技术来保证:在多连接的时候系统的高吞吐量。**多路-指的是多个socket连接,复用-指的是复用一个线程。**多路复用主要有三种技术:select,poll,epoll。epoll是最新的也是目前最好的多路复用技术。这里“多路”指的是多个网络连接,“复用”指的是复用同一个线程。
采用多路I/O复用技术可以让单个线程高效的处理多个连接请求(尽量减少网络IO的时间消耗),且Redis在内存中操作数据的速度非常快(内存内的操作不会成为这里的性能瓶颈),主要以上两点造就了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)。
127.0.0.1:6379> set key I # 设置值key = “key”,value = “I”
OK
127.0.0.1:6379> get key # 获取值
"I"
127.0.0.1:6379> keys * # 查看当前数据库所有数据
1) "age"
2) "key"
3) "name"
127.0.0.1:6379> EXISTS key # 验证 key 是否存在
(integer) 1
127.0.0.1:6379> APPEND key " Love" # 追加字符串 " Love",若当前 key 不存在,则创建 key
(integer) 6
127.0.0.1:6379> get key
"I Love"
127.0.0.1:6379> STRLEN key # 查看 key 的字符串长度
(integer) 6
127.0.0.1:6379> APPEND key " U"
(integer) 8
127.0.0.1:6379> STRLEN key
(integer) 8
127.0.0.1:6379> get key
"I Love U"
#####################################################
127.0.0.1:6379> set views 0 # 设置浏览量views 为0
OK
127.0.0.1:6379> get views
"0"
127.0.0.1:6379> incr views # 自增 1
(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 # 自减 1
(integer) 1
127.0.0.1:6379> decr views
(integer) 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 # 设置步长,制定自增量 n
(integer) 9
127.0.0.1:6379> INCRBY views 10
(integer) 19
127.0.0.1:6379> get views
"19"
127.0.0.1:6379> DECRBY views 6 # 设置步长,制定自减量 n
(integer) 13
127.0.0.1:6379> get views
"13"
#####################################################
# 字符串范围 range
127.0.0.1:6379> set key1 "hello,my little pony"
OK
127.0.0.1:6379> get key1
"hello,my little pony"
127.0.0.1:6379> GETRANGE key1 0 5 # 截取字符串长度 [0,5]
"hello,"
127.0.0.1:6379> GETRANGE key1 0 -1 # 截取整个字符串,相当于 get key
"hello,my little pony"
# 替换字符
127.0.0.1:6379> set key2 abcdefg
OK
127.0.0.1:6379> get key2
"abcdefg"
127.0.0.1:6379> SETRANGE key2 0 z
(integer) 7
127.0.0.1:6379> get key2
"zbcdefg"
#####################################################
# setex(set with expire) # 设置key ,并且设置过期时间
# setnx(set if not exist) # 若不存在key,则设置key;若存在,则设置失败
127.0.0.1:6379> setex key1 30 "my little pony"
OK
127.0.0.1:6379> ttl key1
(integer) 23
127.0.0.1:6379> get key1
"my little pony"
127.0.0.1:6379> setnx key1 "what?" # 设置key1,此时存在,返回值0,设置key1 失败
(integer) 0
127.0.0.1:6379> get key1
(nil)
127.0.0.1:6379> setnx key1 "what?" # 设置key1,此时不存在,返回值1,设置key1 成功
(integer) 1
127.0.0.1:6379> get key1
"what?"
127.0.0.1:6379> keys *
1) "key1"
#####################################################
mset(msetex,msetnx) # 原子性操作,要么一起成功,要么全部失败
mget
127.0.0.1:6379> mset k1 v1 k2 v2 k3 v3
OK
127.0.0.1:6379> mget k1 k2 k3
1) "v1"
2) "v2"
3) "v3"
127.0.0.1:6379> mset k4 v4 k1 vm
OK
127.0.0.1:6379> get k1
"vm"
127.0.0.1:6379> msetnx k5 v5 k2 vm # k2 存在,设置失败,一起设置也失败
(integer) 0
# 设置对象
set user:1{name:zhangsan, age:3}
127.0.0.1:6379> mset user:1:name lisi user:1:age 11
OK
127.0.0.1:6379> mget user:1:name user:1:age
1) "lisi"
2) "11"
#####################################################
# getset 组合命令:先get 后set
127.0.0.1:6379> getset db redis # 若key 不存在,则返回null,并设置为redis
(nil)
127.0.0.1:6379> getset db mongodb # 若key 存在,则返回key的value,并设置为mongodb
"redis"
127.0.0.1:6379> get db
"mongodb"
String类型使用场景: value除了是字符串,还可以是数字(进行数字操作)
# Lpush 头插法
# Rpush 尾插法
# Lrange 从头端获取范围值
# Rrange 从尾端获取范围值
127.0.0.1:6379> LPUSH list1 one # 头插法
(integer) 1
127.0.0.1:6379> LPUSH list1 two
(integer) 2
127.0.0.1:6379> LPUSH list1 three
(integer) 3
127.0.0.1:6379> keys *
1) "list1"
127.0.0.1:6379> LRANGE list1 0 -1 # 出来的顺序
1) "three"
2) "two"
3) "one"
127.0.0.1:6379> RPUSH list1 four # 尾插法
(integer) 4
127.0.0.1:6379> LRANGE list1 0 -1 # 出来的顺序
1) "three"
2) "two"
3) "one"
4) "four"
#####################################################
# Lpop 从头端移除
# Rpop 从尾端移除
127.0.0.1:6379> LPOP list1
"three"
127.0.0.1:6379> RPOP list1
"four"
127.0.0.1:6379> LRANGE list1 0 -1
1) "two"
2) "one"
#####################################################
# Lindex 从头端开始获取下标对应的值
127.0.0.1:6379> LINDEX list1 0
"two"
127.0.0.1:6379> LINDEX list1 1
"one"
#####################################################
# Llen 获取链表list 的长度
127.0.0.1:6379> LRANGE list1 0 -1
1) "pillow"
2) "banana"
3) "apple"
4) "two"
5) "one"
127.0.0.1:6379> LLEN list1
(integer) 5
#####################################################
# Lrem 移除指定个数的value值
127.0.0.1:6379> LRANGE list1 0 -1
1) "one"
2) "two"
3) "one"
4) "one"
5) "pillow"
6) "banana"
7) "apple"
8) "two"
9) "one"
127.0.0.1:6379> LREM list1 3 one
(integer) 3
127.0.0.1:6379> LRANGE list1 0 -1
1) "two"
2) "pillow"
3) "banana"
4) "apple"
5) "two"
6) "one"
#####################################################
# Ltrim 截取指定开始——结束下标范围的链表,会直接改变原链表
127.0.0.1:6379> LRANGE list1 0 -1
1) "two"
2) "pillow"
3) "banana"
4) "apple"
5) "two"
6) "one"
127.0.0.1:6379> LTRIM list1 1 3
OK
127.0.0.1:6379> LRANGE list1 0 -1
1) "pillow"
2) "banana"
3) "apple"
#####################################################
# Lset 更新从头端指定下标的值
127.0.0.1:6379> keys *
1) "list1"
127.0.0.1:6379> EXISTS list1 # 判断链表是否存在
(integer) 1
127.0.0.1:6379> LRANGE list1 0 -1
1) "pillow"
2) "banana"
3) "apple"
127.0.0.1:6379> lset list1 0 "fuckxxx, kk" # 如果list1 不存在,则报错
OK
127.0.0.1:6379> LRANGE list1 0 -1
1) "fuckxxx, kk"
2) "banana"
3) "apple"
#####################################################
# Linsert 将某个具体的value插入到链表中指定(有相同则第一个)value的前边或后边
127.0.0.1:6379> LRANGE list1 0 -1
1) "fuckxxx, kk"
2) "banana"
3) "apple"
127.0.0.1:6379> LINSERT list1 before "banana" "folish"
(integer) 4
127.0.0.1:6379> LRANGE list1 0 -1
1) "fuckxxx, kk"
2) "folish"
3) "banana"
4) "apple"
#####################################################
# RpopLpush 组合命令,同String,先右pop,后左push
127.0.0.1:6379> LRANGE list1 0 -1
1) "fuckxxx, kk"
2) "boy"
3) "folish"
4) "folish"
5) "banana"
6) "apple"
127.0.0.1:6379> LPUSH mylist watermelem
(integer) 1
127.0.0.1:6379> LPUSH mylist vegetable
(integer) 2
127.0.0.1:6379> RPOPLPUSH mylist list1
"watermelem"
127.0.0.1:6379> LRANGE list1 0 -1
1) "watermelem"
2) "fuckxxx, kk"
3) "boy"
4) "folish"
5) "folish"
6) "banana"
7) "apple"
# Sadd 往 set 添加元素,set 集合元素不可重复!!
# Smembers 查看 set 所有元素
# Sismember 判断 set 中指定的 value 是否存在
127.0.0.1:6379> SADD myset "I"
(integer) 1
127.0.0.1:6379> SADD myset "Love"
(integer) 1
127.0.0.1:6379> SADD myset "U"
(integer) 1
127.0.0.1:6379> SADD myset "U" # Set 集合元素不可重复!!!
(integer) 0
127.0.0.1:6379> SMEMBERS myset
1) "I"
2) "Love"
3) "U"
127.0.0.1:6379> SISMEMBER myset U
(integer) 1
127.0.0.1:6379> SISMEMBER myset love
(integer) 0
#####################################################
# Scard 获取 set 中的元素个数
127.0.0.1:6379> SMEMBERS myset
1) "I"
2) "Love"
3) "U"
127.0.0.1:6379> SCARD myset
(integer) 3
#####################################################
### Srem 移除 set 中指定value的元素
127.0.0.1:6379> SMEMBERS myset
1) "I"
2) "Love"
3) "U"
127.0.0.1:6379> SREM myset U
(integer) 1
127.0.0.1:6379> SMEMBERS myset
1) "I"
2) "Love"
### Spop 随机移除 set 中的n个元素
127.0.0.1:6379> SMEMBERS myset
1) "Mixue"
2) "Love"
3) "U"
4) "I"
5) "Ice Scream"
127.0.0.1:6379> SPOP myset
"Mixue"
127.0.0.1:6379> SPOP myset 2
1) "Love"
2) "Ice Scream"
127.0.0.1:6379> SMEMBERS myset
1) "U"
2) "I"
#####################################################
# Set 集合是无序不重复
# Srandmember 随机抽选n个元素
127.0.0.1:6379> SRANDMEMBER myset
"U"
127.0.0.1:6379> SRANDMEMBER myset
"U"
127.0.0.1:6379> SRANDMEMBER myset
"I"
127.0.0.1:6379> SRANDMEMBER myset
"Mixue"
127.0.0.1:6379> SRANDMEMBER myset 2
1) "Mixue"
2) "U"
127.0.0.1:6379> SRANDMEMBER myset 2
1) "Mixue"
2) "Love"
127.0.0.1:6379> SRANDMEMBER myset 2
1) "Love"
2) "Ice Scream"
#####################################################
# Smove 将 set 中指定的值,移动到另一个 set 中
127.0.0.1:6379> clear
127.0.0.1:6379> SMEMBERS myset
1) "I"
2) "Love"
3) "U"
127.0.0.1:6379> SMEMBERS myset2
1) "and"
2) "I"
3) "U"
127.0.0.1:6379> SMOVE myset2 myset and
(integer) 1
127.0.0.1:6379> SMEMBERS myset
1) "and"
2) "I"
3) "Love"
4) "U"
#####################################################
数学:
- 交集 Sinter
- 并集 Sunion
- 差集 Sdiff set1 set2 指的是set1 中元素减去 set2中元素
127.0.0.1:6379> SADD set1 1 2 3 4 5
(integer) 5
127.0.0.1:6379> SADD set2 3 4 5 6 7 11
(integer) 6
127.0.0.1:6379> SINTER set1 set2
1) "3"
2) "4"
3) "5"
127.0.0.1:6379> Sunion set1 set2
1) "1"
2) "2"
3) "3"
4) "4"
5) "5"
6) "6"
7) "7"
8) "11"
127.0.0.1:6379> Sdiff set1 set2
1) "1"
2) "2"
Map集合,形如 Key-<Key,Value>,即Key-Map的形式
# hset 设置>
# hget 获取Key 中的 key 对应的 value
# hmset 设置Key 的多组
# hmget 获得Key 中的多组 key 对应的 value
#hgetall 获取Key中所有的
127.0.0.1:6379> hset myhash field1 I
(integer) 1
127.0.0.1:6379> hset myhash field2 Love
(integer) 1
127.0.0.1:6379> hset myhash field3 U
(integer) 1
127.0.0.1:6379> hget myhash field1
"I"
127.0.0.1:6379> hmset myhash field4 U field5 and field6 I
OK
127.0.0.1:6379> hmget myhash field4 field5 field6
1) "U"
2) "and"
3) "I"
127.0.0.1:6379> hgetall myhash
1) "field1"
2) "I"
3) "field2"
4) "Love"
5) "field3"
6) "U"
7) "field4"
8) "U"
9) "field5"
10) "and"
11) "field6"
12) "I"
#####################################################
# hdel 删除Key中指定的key,其value也会删除
127.0.0.1:6379> HDEL myhash field1
(integer) 1
127.0.0.1:6379> hgetall myhash
1) "field2"
2) "Love"
3) "field3"
4) "U"
5) "field4"
6) "U"
7) "field5"
8) "and"
9) "field6"
10) "I"
#####################################################
# hlen 获取hash表的字段数量
127.0.0.1:6379> hgetall myhash
1) "field2"
2) "Love"
3) "field3"
4) "U"
5) "field4"
6) "U"
7) "field5"
8) "and"
9) "field6"
10) "I"
127.0.0.1:6379> HLEN myhash
(integer) 5
#####################################################
# hexists 判断hash表中指定字段是否存在
127.0.0.1:6379> HEXISTS myhash field1
(integer) 0
127.0.0.1:6379> HEXISTS myhash field2
(integer) 1
#####################################################
# hkeys 只获得所有key
# hvals 只获得所有value
127.0.0.1:6379> HKEYS myhash
1) "field2"
2) "field3"
3) "field4"
4) "field5"
5) "field6"
127.0.0.1:6379> HVALS myhash
1) "Love"
2) "U"
3) "U"
4) "and"
5) "I"
#####################################################
# Hincrby 指定key的增量(可以为负),没有自减
127.0.0.1:6379> hset myhash field1 11
(integer) 1
127.0.0.1:6379> HINCRBY myhash field1 5
(integer) 16
127.0.0.1:6379> HINCRBY myhash field1 -5
(integer) 11
#####################################################
# hsetnx (同String)如果存在则设置失败,否则成功
127.0.0.1:6379> HSETNX myhash field2 love
(integer) 0
127.0.0.1:6379> HSETNX myhash field2 ai
(integer) 0
127.0.0.1:6379> HSETNX myhash field7 win
(integer) 1
Hash应用场景:
在Set的基础上,增加了一个值(下标?),
- set k1 v1
- zset k1 score v1
# Zadd 添加一个/组
# Zrange 还是取范围值
127.0.0.1:6379> ZADD myzset 1 one
(integer) 1
127.0.0.1:6379> ZADD myzset 2 two
(integer) 1
127.0.0.1:6379> ZADD myzset 3 three 4 four
(integer) 2
127.0.0.1:6379> ZRANGE myzset 0 -1
1) "one"
2) "two"
3) "three"
4) "four"
#####################################################
【排序】实现:
# Zrangebyscore Key -inf(-无穷/min值) +inf(+无穷/max值)
127.0.0.1:6379> ZADD myzset 200 zhangsan
(integer) 1
127.0.0.1:6379> ZADD myzset 2000 leego
(integer) 1
127.0.0.1:6379> ZADD myzset 8000 kuku
(integer) 1
127.0.0.1:6379> ZRANGEBYSCORE myzset -inf +inf
1) "zhangsan"
2) "leego"
3) "kuku"
# 返回所有符合条件1 < score <= 5的成员;
ZRANGEBYSCORE zset (5 (10
#####################################################
# Zrem 移除Key中指定的key
# Zcard 统计Key中所有字段数
# ZRevRange xxx 0(start) -1(stop) 让指定区间的(所有的字段)从大到小排序(score)
# Zcount xxx min max 获取指定区间的成员数量
Zset应用案例:
- 班级成绩表
- 工资表
- 热门榜(排行榜)
geospatial:地理空间、附近的人...
Geoadd china:city(Key) 116(经度【-+180-+】) 40(纬度【85.05112878】) beijing(名称) ...
# 将指定的地理空间位置(纬度、经度、名称)添加到指定的key(组)中
Geopos china:city beijing
# 从key里返回所有给定位置元素的位置(经度和纬度)。
Geodist china:city beijing shanghai unit(默认为m,可自定义)
# 返回两个给定位置之间的距离。
# 指定单位的参数 unit 必须是以下单位的其中一个:
- m 表示单位为米。
- km 表示单位为千米。
- mi 表示单位为英里。
- ft 表示单位为英尺。
Georadius china:city 110 30 500km temp(额外定义的参数)
# 以给定的经纬度为中心,给的距离为半径,搜索在这个Key内所有符合的 key
# 额外的信息(temp多个可以组合):
- WITHDIST: 在返回位置元素的同时, 将位置元素与中心之间的距离也一并返回。 距离的单位和用户给定的范围 单位保持一致。
- WITHCOORD: 将位置元素的经度和维度也一并返回。
- WITHHASH: 以 52 位有符号整数的形式, 返回位置元素经过原始 geohash 编码的有序集合分值。 这个选项 主要用于底层应用或者调试, 实际中的作用并不大。
127.0.0.1:6379> GEOADD city 113.27 23.15 guangdong
(integer) 1
127.0.0.1:6379> GEOADD city 116 39 beijing
(integer) 1
127.0.0.1:6379> GEOADD city 113.883 22.55 shenzhen
(integer) 1
127.0.0.1:6379> GEOADD city 114.16 22.27 xianggang
(integer) 1
127.0.0.1:6379> GEOADD city 121.49 31.41 shanghai
(integer) 1
127.0.0.1:6379> GEOADD city 114.02 30.58 wuhan
(integer) 1
127.0.0.1:6379> GEORADIUS city 110 30 500 km withdist
1) 1) "wuhan"
2) "391.4185"
127.0.0.1:6379> GEORADIUS city 110 30 1000 km withdist
1) 1) "shenzhen"
2) "914.4598"
2) 1) "xianggang"
2) "954.6239"
3) 1) "guangdong"
2) "828.2964"
4) 1) "wuhan"
2) "391.4185"
127.0.0.1:6379> GEORADIUS city 110 30 1000 km withdist count 1
1) 1) "wuhan"
2) "391.4185"
127.0.0.1:6379> GEORADIUS city 110 30 1000 km withdist withcoord count 1
1) 1) "wuhan"
2) "391.4185"
3) 1) "114.01999980211257935"
2) "30.58000021509926825"
127.0.0.1:6379> GEORADIUS city 110 30 1000 km withdist withcoord
1) 1) "shenzhen"
2) "914.4598"
3) 1) "113.88299793004989624"
2) "22.5500010475923105"
2) 1) "xianggang"
2) "954.6239"
3) 1) "114.16000038385391235"
2) "22.27000054000478002"
3) 1) "guangdong"
2) "828.2964"
3) 1) "113.27000051736831665"
2) "23.14999996266175941"
4) 1) "wuhan"
2) "391.4185"
3) 1) "114.01999980211257935"
2) "30.58000021509926825"
#####################################################
# Geohash
# 该命令将返回11个字符的Geohash字符串,将二维的经纬度转为一维的字符串
底层原理的实现:Zset,所以命令可以通用
基数:不重复的元素
Redis Hyperloglog:基数统计的算法
# PFadd xxx a b c d e 添加元素
# PFcount xxx 统计不重复元素
# PFmerge xxx xxx xxx 将多个 Hyperloglog 合并成一个 Hyperloglog
127.0.0.1:6379> PFADD mykey1 a b c d f t e s d
(integer) 1
127.0.0.1:6379> PFADD mykey2 d w e t t g f d a s
(integer) 1
127.0.0.1:6379> PFCOUNT mykey1
(integer) 8
127.0.0.1:6379> PFCOUNT mykey2
(integer) 7
127.0.0.1:6379> PFMERGE mykey3 mykey1 mykey2
OK
127.0.0.1:6379> PFCOUNT mykey3
(integer) 9
# 位存储:统计用户信息、(不)活跃、(未)登录、(未)打卡
# 两个状态的数据,尝试使用 bitmaps
127.0.0.1:6379> SETBIT day 0 0
(integer) 0
127.0.0.1:6379> SETBIT day 1 0
(integer) 0
127.0.0.1:6379> SETBIT day 2 1
(integer) 0
127.0.0.1:6379> SETBIT day 3 1
(integer) 0
127.0.0.1:6379> SETBIT day 4 1
(integer) 0
127.0.0.1:6379> SETBIT day 5 0
(integer) 0
127.0.0.1:6379> SETBIT day 6 0
(integer) 0
127.0.0.1:6379> SETBIT day 7 1
(integer) 0
127.0.0.1:6379> GETBIT day 3
(integer) 1
127.0.0.1:6379> GETBIT day 5
(integer) 0
127.0.0.1:6379> BITCOUNT day
(integer) 4
# Redis 事务本质:一组命令的集合,一个事务中所有命令都会被序列化,在事务执行过程中,会按照顺序执行
# 一次性、顺序性、排他性
Redis 单条命令保存原子性,但是其事务不保证原子性
Redis 事务没有隔离级别的概念:命令需等待发起执行命令时才执行,一条一条执行,故无隔离级别
Redis 事务:
# 开启事务 multi(所有执行的命令入队列)
# 执行事务 exec (按照入队的顺序执行命令)
----------------------------一段事务开始----------------------------
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
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
----------------------------一段事务结束----------------------------
##################################################################
# 取消事务 discard
# 取消后事务后,则此次事务的命令不会被执行
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> set k1 v1
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 k4
(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)> setget ke # 该命令出错,编译出错,事务中所有命令均不执行!!!
(error) ERR unknown command `setget`, with args beginning with: `ke`,
127.0.0.1:6379(TX)> set k3 v3
QUEUED
127.0.0.1:6379(TX)> exec
(error) EXECABORT Transaction discarded because of previous errors.
##################################################################
# 运行时异常(如 1/0 操作)
# 如果事务队列中存在语法错误,那么执行命令的时候,其他命令可以正常执行,错误命令则抛出异常
127.0.0.1:6379> set k1 "helo"
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> INCR k1 # 语法出错,该条命令不执行并抛出异常
QUEUED
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> get k1
QUEUED
127.0.0.1:6379(TX)> get k2
QUEUED
127.0.0.1:6379(TX)> exec
1) (error) ERR value is not an integer or out of range
2) OK
3) "helo"
4) "v2"
悲观锁:
- 认为什么时候都会出问题,无论做什么都会加锁
乐观锁:基于数据版本的记录机制实现。使用watch监视键值对,如果事务提交exec时,watch监视的键值对发生变化,事务将被取消
- 认为什么时候都不会出问题,不会上锁。更新数据的时候会判断,此期间是否有人修改过这个数据
- 获取 Version
- 更新时比较 Version
# Redis 使用《乐观锁》
# watch 加乐观锁,比较当前数据的版本,若有变更则事务提交失败
------------------------------第一个线程------------------------------
127.0.0.1:6379> set money 1000
OK
127.0.0.1:6379> watch money
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> set out 0
QUEUED
127.0.0.1:6379(TX)> DECRBY money 100
QUEUED
127.0.0.1:6379(TX)> INCRBY out 100
QUEUED
------------------------------第二个线程------------------------------
127.0.0.1:6379> set money 10
OK
------------------------------第一个线程------------------------------
127.0.0.1:6379(TX)> exec
(nil)
# 1、如果事务执行失败,先解锁监视对象 unwatch
# 2、获取watch监视的最新数据 version
# 3、比对监视的值是否发生了变化,如果没有变化,则可执行成功;否则失败
1、引入相关的包
<dependencies>
<dependency>
<groupId>redis.clientsgroupId>
<artifactId>jedisartifactId>
<version>3.6.3version>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>fastjsonartifactId>
<version>1.2.76version>
dependency>
dependencies>
2、编码测试
// 1、连接数据库
Jedis jedis = new Jedis("127.0.0.1", 6379);
// 2、操作命令
- String
- List
- Set
- Hash
- Zset
- geospatial
- hyperloglog
- bitmaps
// 3、断开连接
jedis.close();
Jedis 事务:简单Java 运用
public class MyTest {
@Test
public void test() {
Jedis jedis = new Jedis("127.0.0.1", 6379);
jedis.flushDB();
JSONObject jsonObject = new JSONObject();
jsonObject.put("name", "dengyi");
jsonObject.put("age", 11);
String result1 = jsonObject.toJSONString();
jsonObject.put("cat", "little pony");
String result2 = jsonObject.toJSONString();
Transaction multi = jedis.multi(); // 开启事务
try {
multi.set("user1", result1);
multi.set("user2", result2);
multi.exec(); // 执行事务
} catch (Exception e) {
multi.discard(); // 放弃事务
e.printStackTrace();
} finally {
System.out.println(jedis.get("user1"));
System.out.println(jedis.get("user2"));
jedis.close(); // 关闭连接
}
}
}
SpringBoot 操作数据:SpringData、JPA、JDBC、Mongodb、Redis…
Jedis:采用直连,多个线程操作不安全;若想避免不安全,使用Jedis Pool 连接池。BIO模式
Lettuce:采用netty,实例可以再多个线程中进行共享,不存在线程不安全,可以减少线程数据。NIO模式
整合测试:
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
# 配置 Redis
spring:
redis:
host: 127.0.0.1
port: 6379
database: 0
/*
- redisTemplate 操作不同的数据类型,api 和Redis 指令是基本一致的
- redisTemplate.opsForValue 操作字符串,类似String
- opsForList List
- opsForSet
- opsForHash
- opsForZSet
- opsForGeo
- opsForHyperloglog
除了基本操作,常用的方法可以直接通过redisTemplate 操作,事务、CRUD...
获取Redis 的连接对象
RedisConnection connection = redisTemplate.getConnectionFactory.getConnection();
connection.flushDb();
connection.flushAll();
*/
自定义 RedisConfig 类,序列化 key,版本较旧
@Configuration
public class RedisConfig {
// 编写自己的 RedisTemplate
@SuppressWarnings({ "rawtypes", "unchecked" })
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory){
RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
template.setConnectionFactory(factory);
//Json序列化配置
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
//String序列号配置
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
//key和hash的key都采用String的序列化配置
template.setKeySerializer(stringRedisSerializer);
template.setHashKeySerializer(stringRedisSerializer);
//value和hash的value采用Json的序列化配置
template.setValueSerializer(jackson2JsonRedisSerializer);
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
}
企业级的开发套路:工具类 RedisUtil 包装 redisTemplate的一些方法,更加方便简洁地使用
拿来即用!
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import javax.annotation.Resource;
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 {
@Resource
private RedisTemplate<String, Object> redisTemplate;
// =============================common============================
/**
* 指定缓存失效时间
*
* @param key 键
* @param time 时间(秒)
*/
public boolean expire(String key, long time, TimeUnit timeUnit) {
try {
if (time > 0) {
redisTemplate.expire(key, time, timeUnit);
}
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, TimeUnit timeUnit) {
try {
redisTemplate.opsForHash().putAll(key, map);
if (time > 0) {
expire(key, time, timeUnit);
}
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, TimeUnit timeUnit) {
try {
redisTemplate.opsForHash().put(key, item, value);
if (time > 0) {
expire(key, time, timeUnit);
}
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, TimeUnit timeUnit, Object... values) {
try {
Long count = redisTemplate.opsForSet().add(key, values);
if (time > 0) {
expire(key, time, timeUnit);
}
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,TimeUnit timeUnit) {
try {
redisTemplate.opsForList().rightPush(key, value);
if (time > 0) {
expire(key, time,timeUnit);
}
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,TimeUnit timeUnit) {
try {
redisTemplate.opsForList().rightPushAll(key, value);
if (time > 0) {
expire(key, time,timeUnit);
}
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;
}
}
// ===============================HyperLogLog=================================
public long pfadd(String key, String value) {
return redisTemplate.opsForHyperLogLog().add(key, value);
}
public long pfcount(String key) {
return redisTemplate.opsForHyperLogLog().size(key);
}
public void pfremove(String key) {
redisTemplate.opsForHyperLogLog().delete(key);
}
public void pfmerge(String key1, String key2) {
redisTemplate.opsForHyperLogLog().union(key1, key2);
}
}
存放目录:cd /usr/local/bin/dconfig(自定义)/redis.conf
1、网络:
bind 127.0.0.1 # 绑定ip
protected-mode yes # 保护模式
port 6379 # 启动端口
2、通用GENERAL
daemonize yes # 以守护进程的方式运行,默认是no,我们需要自己开启为yes
pidfile /var/run/redis_6379.pid # 如果以后台的方式运行,需要执行一个 pid 文件
# 日志
loglevel notice #
logFile "" # 日志文件位置名
database 16 # 数据库数量,默认16
always-show-logo yes # 是否总是显示LOGO
3、快照
持久化,在规定时间内,执行了多少次操作,则会持久化到文件 .rdb.aof
# 系统默认
save 900 1 # 900s内(15min)修改 1 个 Key ,则持久化
save 300 10 # 300s内修改过 10 个Key,则持久化
save 60 10000 # 60s内修改过 60 个Key,则持久化
# 习持久化后,可自定义
stop-writes-on-bgsave-error yes # 持久化出问题,则拒绝新的写入,另开一个新的后台
rdbcompression yes # 是否压缩 rdb 文件,需要消耗 cpu 资源
rdbchecksum yes # 保存 rdb 文件的时候,进行错误的检查校验
dir ./ # rdb 文件保存的目录
4、复制 REPLICATION(主从复制讲解),从机配置主机ip和端口
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4QGpJJRU-1633155659388)(Redis入门.assets/image-20210927005527132.png)]
replicaof <masterip> <masterport>
masterauth <master-password>
5、安全 SECURITY
在此可以设置Redis 密码,默认无密码
# 重启redis
> config get requirepass # 获取 redis 密码
1) "requirepass"
2) ""
> config set requirepass "123456" # 设置 redis 密码
OK
> config get requirepass
ERROR
> auth 123456 # 登录
OK
> config get requirepass
1) "requirepass"
2) "123456"
6、限制 CLIENTS
maxclients 10000 # 默认客户端最大连接数
maxmemory <bytes> # redis 配置最大的内存容量
### Redis 的六种淘汰策略
maxmemory-policy noeviction # 内存到达上限之后的处理策略
- 1、volatile-lru: 只对设置了过期时间的key进行LRU(默认值)
- 2、allkeys-lru: 删除lru算法的key
- 3、volatile-random: 随机删除即将过期key
- 4、allkeys-random: 随机删除
- 5、volatile-ttl: 删除即将过期的
- 6、noeviction: 永不过期,返回错误
7、APPEND ONLY MODE
AOF配置
appendonly no # 默认不开启AOF模式,默认RDB方式持久化。大部分情况下RDB够用
appendfilename "appendonly.aof" # 持久化文件名字
#appendfsync always # 每次修改都会 sync,消耗性能
appendfsync everysec # 每秒执行一次 sync(同步),可能丢失这1s数据
#appendfsync no # 不执行 sync,此时操作系统自己同步数据,速度max
RDB(Redis DataBase):在指定的时间间隔内将内存中的数据集快照写入磁盘,即 Snapshot快照,恢复时是将快照文件直接读到内存。
持久化快照保存:==Redis会单独创建(fork)一个子进程来进行持久化,会先将数据写入一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。==整个过程中,主进程是不进行任何IO操作的,这就确保了极高的性能。如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那RDB方式要比AOF方式更加高效。RDB的缺点是最后一次持久化后的数据可能丢失。(默认RDB)
【生产环境会将RDB文件备份】
RDB文件保存:dump.rdb
触发机制:
1、save 规则满足情况下,会触发 RDB 规则(/usr/local/bin 生成 dump.rdb 文件)
2、执行 flushall 命令,也会触发 RDB 规则
3、退出redis,也会产生 RDB 文件
系统默认
save 900 1 # 900s内(15min)修改 1 个 Key ,则持久化
save 300 10 # 300s内修改过 10 个Key,则持久化
save 60 10000 # 60s内修改过 60 个Key,则持久化
如何恢复 RDB 文件
config get dir
命令),redis 启动时会自动检测 dump.rdb ,并恢复其中的数据config get dir
:127.0.0.1:6379> config get dir
1) "dir"
2) "D:\\Redis\\Redis-x64-5.0.9"
使用 RDB 优点弊端:
AOF(Append Only File):把所有命令都记录下来(history),恢复时把这个文件全部执行一遍
以日志的形式记录每个读写操作。将 Redis 执行过的所有指令记录下来(读操作不记录),只许追加文件不可更改文件,Redis 启动之初就会读取该文件重新构建数据。
Redis 重启就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作。
AOF 文件保存:appendonly.aof
默认不开启,配置文件 redis.conf 修改开启
触发机制:
1、save 规则触发(1.no 2.everysecond 3.always)
2、重启 Redis,生成 appendonly.aof
如果 aof 文件出错,Redis 启动失败,可以使用自带的修复文件
/usr/local/bin/redis-check-aof
(命令:redis-check-aof --fix appendonly.aof
)
AOF 优点弊端:
appendonly no # 默认不开启AOF模式,默认RDB方式持久化。大部分情况下RDB够用
appendfilename "appendonly.aof" # 持久化文件名字
#appendfsync always # 每次修改都会 sync,消耗性能
appendfsync everysec # 每秒执行一次 sync(同步),可能丢失这1s数据
#appendfsync no # 不执行 sync,此时操作系统自己同步数据,速度max
Reids 发布订阅(pub / sub)是一种《消息通信模式》
Redis 客户端可以订阅任意数量的频道
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-h4g46FFl-1633155659389)(Redis入门.assets/image-20210926154107111.png)]
测试:订阅端————发送端
# 订阅端:
127.0.0.1:6379> SUBSCRIBE dj # 订阅频道 dj
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "dj"
3) (integer) 1 # 等待推送的信息
1) "message"
2) "dj"
3) "4396"
1) "message"
2) "dj"
3) "7777"
# 发送端:
127.0.0.1:6379> PUBLISH dj "4396" # 发布者发布信息到频道
(integer) 1
127.0.0.1:6379> PUBLISH dj "7777"
(integer) 1
原理:
Redis 使用 C 实现,通过分析 Redis 源码里的 pubsub.c 文件,了解发布和订阅机制的底层实现,加深对 Redis 的理解。
Redis 通过 PUBLICSH、SUBSCRIBE、PSUBSCRIBE 等命令实现发布和订阅功能
通过 SUBSCRIBE 命令订阅某频道后,redis-server 里维护了一个字典,字典的键就是一个个 channel,而字典的值则是一个链表,链表中保存了所有订阅这个 channel 的客户端。SUBSCRIBE 命令的关键就是将客户端添加到给定 channel 的订阅链表中。
通过 PUBLISH 命令向订阅者发送信息,redis-server 会使用给定的频道作为键,在它所维护的 channel 字典中查找记录了订阅这个频道的所有客户端的链表,遍历这个链表,将消息发布给所有订阅者
PUB / SUB 从字面上理解就是发布(PUBLISH)和订阅(SUBSCRIBE),在 Redis 中,你可以设定对某一个 key 值进行消息发布和消息订阅,当一个 key 值进行了消息发布后,所有订阅他的客户端都会受到对应的消息。这一功能最明显的用法就是用作实时消息系统,比如普通的即时聊天,群聊等…
主从复制:是指将一台Redis 服务器的数据,复制到其他的Redis 服务器。前者为主节点(master/ leader),后者为从节点(slave/ follower);数据的复制是单向的,只能从主节点到从节点。Master 以写为主,Slave 以读为主。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ESUmfCA4-1633155659390)(Redis入门.assets/image-20210927002035615.png)]
主从复制作用:(读写分离)
1、数据冗余:主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式
2、故障恢复:当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复;实际上是一种服务的冗余;
3、负载均衡:在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务(即写 Redis 数据时应用连接主节点,读Redis 数据时应用连接从节点),分担服务器负载,尤其是在写少读多的场景下,通过多个从节点分担读负载,可以大大提高 Redis 服务器的并发量。
4、高可用基石:主从复制还是哨兵和集群能够实施的基础,因此主从复制是 Redis 高可用的基础
一般来说,Redis 运用于工程项目中,不只只是用一台 Redis,原因:
1、从结构上,单个Redis 服务器会发生单点故障,并且一台服务器需要处理所有的请求负载,压力较大
2、从容量上,单个 Redis 服务器内存容量有限,就算一台 Redis 服务器内容容量为 256G,也不能将所有内存用作 Redis 存储内存。一般来说,单台Redis 最大使用内存不超过20G。
只配置从库,不配置主库。
# 查看当前库的信息
127.0.0.1:6379> info replication
# Replication
role:master # 角色
connected_slaves:0 # 从机数:0
master_failover_state:no-failover
master_replid:18514ff86efd5031b8eed28a36fea4fa3205aade
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
1、复制3个 redis.conf 配置文件(redis79.conf、redis80.conf、redis81.conf)
2、修改对应端口
3、修改 pid 名字
4、修改 log 文件名字
5、修改 dump79.rdb
6、复制3个会话(模拟3台服务器),分别启动 79,80,81 端口。
redis-server dconfig/redis79.conf
7、此时,每台Redis 服务器都为主节点【默认】,开始配置从节点
8、一主二从:
Master:79
Slave:80、81
8.1
slaveof 127.0.0.1 6379
(从机)命令修改,暂时实现 8.2 配置 redis.conf 修改,永久!
# 查看进程信息
[root@hadoop2 bin]# ps -ef|grep redis
root 2081 1 0 00:28 ? 00:00:00 redis-server *:6379
root 2394 1 0 00:39 ? 00:00:00 redis-server 127.0.0.1:6380
root 2402 1 0 00:39 ? 00:00:00 redis-server 127.0.0.1:6381
root 2421 2189 0 00:40 pts/3 00:00:00 grep --color=auto redis
# 主机信息
127.0.0.1:6379> INFO replication
# Replication
role:master
connected_slaves:2
slave0:ip=127.0.0.1,port=6380,state=online,offset=70,lag=1
slave1:ip=127.0.0.1,port=6381,state=online,offset=70,lag=1
master_failover_state:no-failover
master_replid:b2cc2ca3b35001607c7a5e14a38e8bf0aea80521
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:70
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:70
主机:只能写
从机:只能读
测试:主机断开重连,主机写,从机依然能读
如果使用命令来配置主从机,若从机断开,再重连时其变为主机,不再能够读取原主机新写入的数据。原主机的从机数-1
复制原理:
Slave 启动成功连接到 Master 后会发送一个sync同步命令,Master 收到命令,启动后台的存盘进程,同时收集所有接收到用于修改数据集命令,在后台进程执行完毕后,Master 将传送整个数据文件到 Slave,并完成一次完全同步
全量复制:Slave 服务器在接收到数据库文件数据后,将其存盘并加载到内存中
增量复制:Master 继续将新的所有收集到的修改命令依次传给 Slave,完成同步
Slave 只要重新连接 Master,一次完全同步(全量复制)自动执行。
谋朝篡位:
slaveof no one
命令,当前从节点的主机断开连接,从机变成主机的命令
主从切换技术:当主服务器宕机后,需要收到把一台 从服务器 切换为 主服务器,需要人工干预,费时费力,还会造成一段时间内服务不可用。Redis 从2.8 开始正式提供了 Sentinel(哨兵)架构。
哨兵模式:谋朝篡位自动版,能够后台监控主机是否故障,如果故障会根据投票数自动将从库转为主库;它是一种特殊的模式,首先Redis 提供了哨兵的命令,哨兵是一个独立的进程,作为进程,他会独立运行。原理是 哨兵通过发送命令,等待 Redis 服务器响应,从而监控运行的多个 Redis 实例。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-naYDrgtu-1633155659391)(Redis入门.assets/image-20210927125827604.png)]
哨兵作用:
1、通过发送命令,让 Redis 服务器返回监控其运行状态,包括主服务器和从服务器
2、当哨兵检测到 Master 宕机,会自动将 Slave 切换成 Master,然后通过 发布订阅模式 通知其他的从服务器,修改配置文件,让他们切换为主机
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZILNpWjr-1633155659392)(Redis入门.assets/image-20210927125937784.png)]
假设主服务器宕机,哨兵1会先检测到这个结果,系统并不会马上进行 failover(故障转移) 过程,仅仅是哨兵1主观的认为主服务器不可用。这个现象称为**主观下线。当后面的哨兵也检测到主服务器不可用,并且数量到达一定值,那么哨兵之间就会进行一次投票,投票结果由一个哨兵发起,进行 failover 操作。切换成功后,就会通过 发布订阅模式,让各个哨兵把自己监控的从服务器实现切换主机,这个过程称为客观下线** 。
测试:
# sentinel monitor 被监控名字 host port 1(判定客观下线票数临界点)
sentinel monitor myredis 172.0.0.1 6379 1
# redis-sentinel dconfig/sentinel.conf
[root@hadoop2 bin]# redis-sentinel dconfig/sentinel.conf
2825:X 27 Sep 2021 14:06:36.513 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
2825:X 27 Sep 2021 14:06:36.513 # Redis version=6.2.5, bits=64, commit=00000000, modified=0, pid=2825, just started
2825:X 27 Sep 2021 14:06:36.513 # Configuration loaded
2825:X 27 Sep 2021 14:06:36.514 * Increased maximum number of open files to 10032 (it was originally set to 1024).
2825:X 27 Sep 2021 14:06:36.514 * monotonic clock: POSIX clock_gettime
_._
_.-``__ ''-._
_.-`` `. `_. ''-._ Redis 6.2.5 (00000000/0) 64 bit
.-`` .-```. ```\/ _.,_ ''-._
( ' , .-` | `, ) Running in sentinel mode
|`-._`-...-` __...-.``-._|'` _.-'| Port: 26379
| `-._ `._ / _.-' | PID: 2825
`-._ `-._ `-./ _.-' _.-'
|`-._`-._ `-.__.-' _.-'_.-'|
| `-._`-._ _.-'_.-' | https://redis.io
`-._ `-._`-.__.-'_.-' _.-'
|`-._`-._ `-.__.-' _.-'_.-'|
| `-._`-._ _.-'_.-' |
`-._ `-._`-.__.-'_.-' _.-'
`-._ `-.__.-' _.-'
`-._ _.-'
`-.__.-'
2825:X 27 Sep 2021 14:06:36.515 # WARNING: The TCP backlog setting of 511 cannot be enforced because /proc/sys/net/core/somaxconn is set to the lower value of 128.
2825:X 27 Sep 2021 14:06:36.516 # Sentinel ID is b59150a702fa9baa145633bb1fc9cb6499e20255
2825:X 27 Sep 2021 14:06:36.516 # +monitor master myredis 127.0.0.1 6379 quorum 1
哨兵模式:优缺点:
哨兵模式配置:
# Example sentinel.conf
# 哨兵sentinel实例运行的端口 默认26379
port 26379
# 哨兵sentinel的工作目录
dir /tmp
# 哨兵sentinel监控的redis主节点的 ip port
# master-name 可以自己命名的主节点名字 只能由字母A-z、数字0-9 、这三个字符".-_"组成。
# quorum 配置多少个sentinel哨兵统一认为master主节点失联 那么这时客观上认为主节点失联了
# sentinel monitor
sentinel monitor mymaster 127.0.0.1 6379 2
# 当在Redis实例中开启了requirepass foobared 授权密码 这样所有连接Redis实例的客户端都要提供密码
# 设置哨兵sentinel 连接主从的密码 注意必须为主从设置一样的验证密码
# sentinel auth-pass
sentinel auth-pass mymaster MySUPER--secret-0123password
# 指定多少毫秒之后 主节点没有应答哨兵sentinel 此时 哨兵主观上认为主节点下线 默认30秒
# sentinel down-after-milliseconds
sentinel down-after-milliseconds mymaster 30000
# 这个配置项指定了在发生failover主备切换时最多可以有多少个slave同时对新的master进行 同步,这个数字越小,完成failover所需的时间就越长,但是如果这个数字越大,就意味着越 多的slave因为replication而不可用。可以通过将这个值设为 1 来保证每次只有一个slave 处于不能处理命令请求的状态。
# sentinel parallel-syncs
sentinel parallel-syncs mymaster 1
# 故障转移的超时时间 failover-timeout 可以用在以下这些方面:
#1. 同一个sentinel对同一个master两次failover之间的间隔时间。
#2. 当一个slave从一个错误的master那里同步数据开始计算时间。直到slave被纠正为向正确的master那里同步数据时。
#3.当想要取消一个正在进行的failover所需要的时间。
#4.当进行failover时,配置所有slaves指向新的master所需的最大时间。不过,即使过了这个超时,slaves依然会被正确配置为指向master,但是就不按parallel-syncs所配置的规则来了
# 默认三分钟
# sentinel failover-timeout
sentinel failover-timeout mymaster 180000
# SCRIPTS EXECUTION
#配置当某一事件发生时所需要执行的脚本,可以通过脚本来通知管理员,例如当系统运行不正常时发邮件通知相关人员。
#对于脚本的运行结果有以下规则:
#若脚本执行后返回1,那么该脚本稍后将会被再次执行,重复次数目前默认为10
#若脚本执行后返回2,或者比2更高的一个返回值,脚本将不会重复执行。
#如果脚本在执行过程中由于收到系统中断信号被终止了,则同返回值为1时的行为相同。
#一个脚本的最大执行时间为60s,如果超过这个时间,脚本将会被一个SIGKILL信号终止,之后重新执行。
#通知型脚本:当sentinel有任何警告级别的事件发生时(比如说redis实例的主观失效和客观失效等等),将会去调用这个脚本,这时这个脚本应该通过邮件,SMS等方式去通知系统管理员关于系统不正常运行的信息。调用该脚本时,将传给脚本两个参数,一个是事件的类型,一个是事件的描述。如果sentinel.conf配置文件中配置了这个脚本路径,那么必须保证这个脚本存在于这个路径,并且是可执行的,否则sentinel无法正常启动成功。
#通知脚本
# shell编程
# sentinel notification-script
sentinel notification-script mymaster /var/redis/notify.sh
# 客户端重新配置主节点参数脚本
# 当一个master由于failover而发生改变时,这个脚本将会被调用,通知相关的客户端关于master地址已经发生改变的信息。
# 以下参数将会在调用脚本时传给脚本:
#
# 目前总是“failover”,
# 是“leader”或者“observer”中的一个。
# 参数 from-ip, from-port, to-ip, to-port是用来和旧的master和新的master(即旧的slave)通信的
# 这个脚本应该是通用的,能被多次调用,不是针对性的。
# sentinel client-reconfig-script
sentinel client-reconfig-script mymaster /var/redis/reconfig.sh # 一般都是由运维来配置!
Redis 缓存的使用,极大提升了应用程序的性能和效率,特别是数据查询方面。同时也带来一些问题,最重要的问题是 数据的一致性 问题。如果对 数据一致性 要求很高,那么便不能使用缓存
缓存的处理流程:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5fbX8axQ-1633155659394)(Redis入门.assets/image-20210928170550225.png)]
概念:
用户查询一个数据,发现Redis 内存数据没有,即缓存没有命中,于是向持久层数据库查询,发现也没有,于是本次查询失败。当用户很多时候,缓存都没有命中(秒杀),于是都去请求了持久层数据库,这会给持久层数据库造成很大的压力,这时候就相当于出现了缓存穿透。
解决方案:
布隆过滤器
缓存空对象
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2mXnYJH9-1633155659395)(Redis入门.assets/image-20210927143722711.png)]
概念:
缓存击穿,是指一个key非常热点,在不停的扛着大并发,大并发集中对着一个点进行访问,当这个key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个屏障上凿开了一个洞。
当某个key在过期的瞬间,有大量的请求并发访问,这类数据一般是热点数据,由于缓存过期,会同时访问数据库来查询最新数据,并且写回缓存,会导致数据库瞬间压力过大
解决方案:
概念:
缓存雪崩,指在某一个时间段,缓存集中过期失效。Redis 宕机
产生雪崩的原因之一:如双十二零点抢购热潮,商品时间比较集中的放入缓存。假设缓存一个小时,那么凌晨一点的时候,这批商品的缓存都过期了。而对这批商品的访问查询,都落到了数据库上边,对于数据库而言,会产生周期性的压力波峰。于是所有请求都会到达存储层,存储层的调用激增,造成存储层也挂掉。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pX6zRC9K-1633155659396)(Redis入门.assets/image-20210927144955788.png)]
缓存集中过期,其实并不是非常致命,比较致命的缓存雪崩,是缓存服务器某个节点宕机或断网
解决方案:
Redis 缓存的使用,极大提升了应用程序的性能和效率,特别是数据查询方面。同时也带来一些问题,最重要的问题是 数据的一致性 问题。如果对 数据一致性 要求很高,那么便不能使用缓存
缓存的处理流程:
[外链图片转存中…(img-5fbX8axQ-1633155659394)]
概念:
用户查询一个数据,发现Redis 内存数据没有,即缓存没有命中,于是向持久层数据库查询,发现也没有,于是本次查询失败。当用户很多时候,缓存都没有命中(秒杀),于是都去请求了持久层数据库,这会给持久层数据库造成很大的压力,这时候就相当于出现了缓存穿透。
解决方案:
布隆过滤器
缓存空对象
[外链图片转存中…(img-2mXnYJH9-1633155659395)]
概念:
缓存击穿,是指一个key非常热点,在不停的扛着大并发,大并发集中对着一个点进行访问,当这个key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个屏障上凿开了一个洞。
当某个key在过期的瞬间,有大量的请求并发访问,这类数据一般是热点数据,由于缓存过期,会同时访问数据库来查询最新数据,并且写回缓存,会导致数据库瞬间压力过大
解决方案:
概念:
缓存雪崩,指在某一个时间段,缓存集中过期失效。Redis 宕机
产生雪崩的原因之一:如双十二零点抢购热潮,商品时间比较集中的放入缓存。假设缓存一个小时,那么凌晨一点的时候,这批商品的缓存都过期了。而对这批商品的访问查询,都落到了数据库上边,对于数据库而言,会产生周期性的压力波峰。于是所有请求都会到达存储层,存储层的调用激增,造成存储层也挂掉。
[外链图片转存中…(img-pX6zRC9K-1633155659396)]
缓存集中过期,其实并不是非常致命,比较致命的缓存雪崩,是缓存服务器某个节点宕机或断网
解决方案: