学习链接:https://www.bilibili.com/video/BV1S54y1R7SB?from=search&seid=7621763557629341311
学习路径:基础理论学习+实操,将知识融会贯通历史、发展、理论、实践
1,单机MySQL
DAL:数据库操作(CRUD)接口
90年代,网站访问量不大,单个数据库足够
网站更多使用静态html,服务器没啥压力
瓶颈:数据量如果太大,一个mysql放不下;数据索引太大放不下;数据访问量太大(当时是读写分离的),服务器承受不了
2,缓存+MySQL+垂直拆分(读写分离)
80%都是在读,利用缓存提高读取速度(优化数据结构和索引-文件缓存-Memcached)
读写分离
3,分库分表+水平拆分+MySQL集群
数据库的本质:读+写
MyISAM:只有表锁,高并发下十分影响效率
转为InnoDB:支持行锁
4,数据类型发生变化、数据量也暴增,RDBMS不够用了
使用MySQL存储大的文件(博客、图片等),数据库表大,效率低
- 本身MySQL读写会变慢
- 硬件也不会无限度的优化
可使用其他类型数据库存储这些类型的数据。
为什么用NoSQL?
存储爆发增长的、多种类型的数据
Not Only SQL,非关系型数据库
数据存储不是固定格式
特点:
- 方便扩展(数据间没有关系)
- 大数据量高性能(Redis一秒写8万次,读取11万次)NoSQL缓存记录级是一种细粒度的缓存,性能高
- 数据类型多样(无需事先设计数据库,随取随用)
大数据时代的3V+3高:
3V——描述问题
- 海量Volume
- 多样Variety
- 实时Velocity
Value高价值
3高——对程序的要求
- 高并发
- 高可拓
- 高性能
KV键值对:Redis Memcached
文档数据库:MongoDB(一个基于分布式文件存储的数据库,用于处理大量的文档)
MongoDB是非关系型数据库中最像关系型数据库的
列存储数据库:HBase Cassandra 分布式文件系统
图数据库:Neo4j(存关系的,例如社交网络、广告推荐)
CAP理论的核心:一个分布式系统不可能很好的同时满足一致性、可用性和分区容错,最多同时只能较好满足其中两个
为解决RDBMS强一致性要求引起的可用性降低提出的方案
BASE的思想:让系统放松对某一时刻对数据一致性的要求来换取系统整体伸缩性和性能上的改观。例如现在大型系统往往由于地域分布和极高性能的要求,不可能采用分布式事务完成这些指标,所以只要保证最终数据一致即可,有些地域的数据晚一点到没啥影响
Remote Dictionary Server 远程字典服务(5M大小),用于数据库、缓存和消息中间件MQ
一个开源的使用C语言编写、支持网络、基于内存和持久化的日志型、key-value型数据库,提供多种API
Redis能干嘛
特性
多样数据类型
持久化
TTL
集群
事务
分片(扩展写性能)
分片是将数据划分为多个部分,对数据的划分可基于键包含的ID、基于键的散列值或基于两者的某种组合。通过对数据分片,可将数据存储到多台机器中,也可从多台机器中获取数据,分片方法在解决某些问题是可获得线性级别的性能提升
复制
…
与Memcached相比,Memcached是高性能键值缓存服务器,两者都可用于存储键值映射,Redis可自动以AOF/RDB方式将数据持久化入硬盘中,数据类型多,Memcached只能存储字符串键。
【注:Memcached的使用方法:用户只能用APPEDN命令将数据添加到已有字符串的末尾,删除元素的方法是通过黑名单(blacklist)来隐藏列表中的元素从而避免对元素执行读取、更新、写入等操作。相比较Redis的直接删除操作而言,每次访问都得遍历整个数据库判断元素是否已被加入黑名单,读性能大大下降】
redis-benchmark是redis自带的压力测试工具
Redis有16个数据库,默认使用0号数据库,可使用select切换数据库
127.0.0.1:6379> select 0
OK
127.0.0.1:6379> set name cc
OK
127.0.0.1:6379> get name
"cc"
# 列出所有的键
127.0.0.1:6379> keys *
1) "counter:__rand_int__"
2) "myset:__rand_int__"
3) "mylist"
4) "image"
5) "key:__rand_int__"
6) "capture"
7) "name"
8) "iamge"
# 清空数据库
127.0.0.1:6379> flushdb
OK
127.0.0.1:6379> keys *
(empty list or set)
127.0.0.1:6379>
# flushall清空所有数据库的内容
Redis是基于内存操作,CPU不是Redis性能瓶颈,Redis性能瓶颈是根据机器的内存和网络带宽。Redis是单线程的 10万的QPS
- 高性能服务器不一定是单线程的
- 多线程(CPU上下文会切换)不一定比单线程效率高
速度:CPU > 内存 > 硬盘
Redis将所有的数据都放在内存中,单线程操作效率最高(多线程由于上下文切换,是十分耗时的)
对于内存系统而言,如果没有上下文切换的话效率是最高的。因为多次读写都在一个CPU上,在内存存放数据是最佳选择
# keys *列出所有的键
127.0.0.1:6379> keys *
(empty list or set)
# select db_num 切换数据库
127.0.0.1:6379> select 0
OK
127.0.0.1:6379> keys *
(empty list or set)
# set key value 插入一个value为string类型的key-value键值对
127.0.0.1:6379> set name ciery
OK
127.0.0.1:6379> keys *
1) "name"
# exists key_name 查看key是否存在
127.0.0.1:6379> exists name
(integer) 1
127.0.0.1:6379> exists names
(integer) 0
# move key 1 移除key
127.0.0.1:6379> move name 1
(integer) 1
127.0.0.1:6379> exists name
(integer) 0
127.0.0.1:6379> keys *
(empty list or set)
127.0.0.1:6379> set name ciery
OK
127.0.0.1:6379> keys *
1) "name"
127.0.0.1:6379> get name
"ciery"
# expire key time 设置key的过期时间
127.0.0.1:6379> expire name 10
(integer) 1
# ttl key 查看key的存活时间
127.0.0.1:6379> ttl name
(integer) 7
127.0.0.1:6379> ttl name
(integer) 5
127.0.0.1:6379> ttl name
(integer) -2
# key过期之后无法查看(单点登录中的session过期失效实现)
127.0.0.1:6379> get name
(nil)
127.0.0.1:6379> set name 10
OK
127.0.0.1:6379> expire name 10
(integer) 1
127.0.0.1:6379> ttl name
(integer) 7
# 可以重复在key未过期的时候通过expire key time命令增长过期时间(单点登录session更新实现)
127.0.0.1:6379> expire name 10
(integer) 1
127.0.0.1:6379> ttl name
(integer) 9
127.0.0.1:6379> set name cici
OK
# type key 查看key的类型
127.0.0.1:6379> type name
string
Redis不区分大小写!
String类型可加“”,也可不加系统自动识别
# set key value 插入一个String类型的键值对
127.0.0.1:6379> set key1 v1
OK
# get key 获取key对应的值
127.0.0.1:6379> get key1
"v1"
# keys * 获取当前系统所有的键
127.0.0.1:6379> keys *
1) "capture"
2) "key1"
3) "image"
4) "counter:__rand_int__"
5) "mylist"
6) "myset:__rand_int__"
7) "name"
8) "key:__rand_int__"
9) "iamge"
# exits key 判断一个key是否存在
127.0.0.1:6379> exists key1
(integer) 1
# append key value给key原本的value追加值,结果返回追加后value的长度
127.0.0.1:6379> append key1 "hello"
(integer) 7
# strlen key查看key对应value的长度
127.0.0.1:6379> strlen key1
(integer) 7
127.0.0.1:6379> get key1
"v1hello"
127.0.0.1:6379> set views 0
OK
127.0.0.1:6379> get views
"0"
# incr key累加1
127.0.0.1:6379> incr views
(integer) 1
127.0.0.1:6379> incr views
(integer) 2
127.0.0.1:6379> incr views
(integer) 3
127.0.0.1:6379> get views
"3"
# decr key累减1
127.0.0.1:6379> decr views
(integer) 2
127.0.0.1:6379> decr views
(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"
# incrby key num 累加步长num
127.0.0.1:6379> incrby views 10
(integer) 9
127.0.0.1:6379> get vies
(nil)
127.0.0.1:6379> get views
"9"
# decrby key num累减步长num
127.0.0.1:6379> decrby views 5
(integer) 4
127.0.0.1:6379> get views
"4"
127.0.0.1:6379> set range1 "hello,ciery"
OK
127.0.0.1:6379> get range1
"hello,ciery"
# getrange key start end 获取从start到end的value截取值[start,end]结果为左闭右闭
127.0.0.1:6379> getrange range1 0 3
"hell"
# getrange key 0 -1 获取所有的值
127.0.0.1:6379> getrange range1 0 -1
"hello,ciery"
# setrange key start value 从start开始替换key对应的值
127.0.0.1:6379> setrange range1 1 xx
(integer) 11
127.0.0.1:6379> getrange range1 0 -1
"hxxlo,ciery"
# setex 设置一个键+过期时间
127.0.0.1:6379> setex key2 6 hello
OK
127.0.0.1:6379> get key2
"hello"
127.0.0.1:6379> ttl key2
(integer) -2
# 过期后查看不到这个key
127.0.0.1:6379> get key2
(nil)
# setnx key value当key不存在则新建一个key,对应值为value;当存在的时候不做任何处理
127.0.0.1:6379> setnx mykey cci
(integer) 1
127.0.0.1:6379> get mykey
"cci"
127.0.0.1:6379> setnx mykey mongo
(integer) 0
127.0.0.1:6379> get mykey
"cci"
# mset key1 value1 key2 value2 ...批量添加key-value
127.0.0.1:6379> mset k1 v1 k2 v2
OK
127.0.0.1:6379> keys *
1) "mykey"
2) "range1"
3) "capture"
4) "k1"
5) "key1"
6) "image"
7) "k2"
8) "ragne1"
9) "counter:__rand_int__"
10) "views"
11) "mylist"
12) "myset:__rand_int__"
13) "name"
14) "key:__rand_int__"
15) "iamge"
# mget key1 key2... 批量获取key
127.0.0.1:6379> mget k1 k2
1) "v1"
2) "v2"
#msetnx key1 value1 key2 value2... 批量插入当不存在的时候新建;如果其中有key已经存在,鉴于Redis的原子性,整体操作无效
127.0.0.1:6379> msetnx k1 v2 k3 v3
(integer) 0
127.0.0.1:6379> keys *
1) "mykey"
2) "range1"
3) "capture"
4) "k1"
5) "key1"
6) "image"
7) "k2"
8) "ragne1"
9) "counter:__rand_int__"
10) "views"
11) "mylist"
12) "myset:__rand_int__"
13) "name"
14) "key:__rand_int__"
15) "iamge"
127.0.0.1:6379> get k3
(nil)
# getset key value先获取key,然后再set;如果key不存在返回nil,并且插入key
127.0.0.1:6379> getset db redis
(nil)
127.0.0.1:6379> get db
"redis"
# getset 如果key存在返回这个key对应的value,并将新的value值覆盖掉原有值
127.0.0.1:6379> getset db mongddb
"redis"
127.0.0.1:6379> get db
"mongddb"
# 巧妙设置key值,实现业务功能
127.0.0.1:6379> mset user:1:name ciery user:1:age 20
OK
127.0.0.1:6379> mget user:1:name user:1:age
1) "ciery"
2) "20"
String的使用场景:(set get strlen append exits getrange setrange setex setnx mset mget msetnx getset incr decr incrby decrby
setnx如果键存在不做任何处理
- 计数器
- 统计多单位的数量,例如用户关注数;粉丝数;浏览量等,可以通过uid:22333:follow uid:22333:fans等标记,并通过incr累加记录
- 粉丝数
- 对象缓存存储
基本的数据类型,列表
Redis中可以通过list实现栈、队列、阻塞队列
# 清空当前数据库(默认0号)
127.0.0.1:6379> flushdb
OK
127.0.0.1:6379> keys *
(empty list or set)
# lpush key value往队列key中左插入一个值value。如果key不存在则新建一个并插入新值
127.0.0.1:6379> lpush list one
(integer) 1
# 如果这个key存在,则在队列左侧继续追加值
127.0.0.1:6379> lpush list two
(integer) 2
127.0.0.1:6379> lpush list three
(integer) 3
# lrange key 0 -1 查看队列所有元素(返回结果为从左到右遍历队列的结果)
127.0.0.1:6379> lrange list 0 -1
1) "three"
2) "two"
3) "one"
# lrange key start end 查看队列中从start到end的所有元素
127.0.0.1:6379> lrange list 0 1
1) "three"
2) "two"
# rpush key value 右侧插入值
127.0.0.1:6379> rpush list right
(integer) 4
127.0.0.1:6379> lrange list 0 -1
1) "three"
2) "two"
3) "one"
# lrange查看可发现新值right插入到列表list中的最右侧
4) "right"
# lpop key从列表key的左侧弹出一个值
127.0.0.1:6379> lpop list
"three"
# lrange可见第一个值被弹出
127.0.0.1:6379> lrange list 0 -1
1) "two"
2) "one"
3) "right"
# rpop key从列表右侧弹出一个值
127.0.0.1:6379> rpop list
"right"
# lrange可见右侧第一个值被弹出
127.0.0.1:6379> lrange list 0 -1
1) "two"
2) "one"
# lindex key num返回列表key中指定索引num对应的值(索引值从0开始)
127.0.0.1:6379> lindex list 1
"one"
# 2索引不存在,返回nil
127.0.0.1:6379> lindex list 2
(nil)
127.0.0.1:6379> lindex list 0
"two"
# llen key查看列表的长度
127.0.0.1:6379> llen list
(integer) 2
127.0.0.1:6379> lpush list two
(integer) 3
127.0.0.1:6379> lrange list 0 -1
1) "two"
2) "two"
3) "one"
# lrem key count value 删除队列key中指定数量count的value值
127.0.0.1:6379> lrem list 1 two
(integer) 1
# lrange查看发现一个two元素被删除
127.0.0.1:6379> lrange list 0 -1
1) "two"
2) "one"
127.0.0.1:6379> lpush list two
(integer) 3
# lrem删除列表中两个two元素
127.0.0.1:6379> lrem list 2 two
(integer) 2
# lrange查看发现两个two被删除
127.0.0.1:6379> lrange list 0 -1
1) "one"
127.0.0.1:6379> rpush list hello
(integer) 2
127.0.0.1:6379> rpush list world
(integer) 3
127.0.0.1:6379> rpush list hello1
(integer) 4
127.0.0.1:6379> lrange list 0 -1
1) "one"
2) "hello"
3) "world"
4) "hello1"
# ltrim key start end截取key列表从start到end的值
127.0.0.1:6379> ltrim list 1 2
OK
# 发现list被截取,剩下从1到2索引指定的值
127.0.0.1:6379> lrange list 0 -1
1) "hello"
2) "world"
# rpoplpush key1 key2将key1中的右侧第一个元素弹出,写入key2的左侧
127.0.0.1:6379> rpoplpush list otherlist
"world"
# lrange查看key1的右侧第一个元素被弹出
127.0.0.1:6379> lrange list 0 -1
1) "hello"
# lrange查看key2左侧被插入key1弹出的元素
127.0.0.1:6379> lrange otherlist 0 -1
1) "world"
# exist key 查看一个列表是否存在
127.0.0.1:6379> exists list
(integer) 1
# lset key num value更新key中num索引对应的值
127.0.0.1:6379> lset list 0 item
OK
# 可见0索引对应的value被更新
127.0.0.1:6379> lrange list 0 -1
1) "item"
# 如果num插入key的范围,报错
127.0.0.1:6379> lset list 1 item
(error) ERR index out of range
# 如果key不存在,报错
127.0.0.1:6379> lset list1 0 item
(error) ERR no such key
# lindex key before/after value1 value2在列表key的value1之前/之后插入一个value2
127.0.0.1:6379> linsert list before item "hello"
(integer) 2
127.0.0.1:6379> lrange list 0 -1
1) "hello"
2) "item"
127.0.0.1:6379> linsert list after item "world"
(integer) 3
127.0.0.1:6379> lrange list 0 -1
1) "hello"
2) "item"
3) "world"
127.0.0.1:6379>
List实际是一个列表:lpush lpop rpush rpop exists llen lrange lindex rpoplpush lset linsert ltrim lrem 列表中元素可重复
- 如果key不存在,则创建一个新的链表
- 如果key存在,新增内容
- 如果移除了所有的值,空链表表示不存在
- 在两边插入/改动值,效率高;往中间查数据效率相对较低
List的使用场景:
- 消息排队
- 消息队列(lpush rpop)FIFO
- 栈(lpush lpop)FILO
set集合中元素不可重复且无序
127.0.0.1:6379> flushdb
OK
# sadd key value往集合中添加元素
127.0.0.1:6379> sadd set hello
(integer) 1
127.0.0.1:6379> sadd set world
(integer) 1
127.0.0.1:6379> sadd set !
(integer) 1
# smembers key获取集合key中所有元素
127.0.0.1:6379> smembers set
1) "world"
2) "hello"
3) "!"
# sismember key value判断集合key中是否存在值value
127.0.0.1:6379> sismember set hello
(integer) 1
127.0.0.1:6379> sismember set he
(integer) 0
# scard key查看集合set中的元素个数
127.0.0.1:6379> scard set
(integer) 3
# srem key value 移除集合key中的value值
127.0.0.1:6379> srem set hello
(integer) 1
127.0.0.1:6379> scard set
(integer) 2
127.0.0.1:6379> sismember set hello
(integer) 0
127.0.0.1:6379> smembers set
1) "world"
2) "!"
# srandmember key随机获取集合key中的一个值
127.0.0.1:6379> SRANDMEMBER set
"!"
127.0.0.1:6379> SRANDMEMBER set
"world"
# srandmember key num 随机获取集合key中的num个值
127.0.0.1:6379> SRANDMEMBER set 1
1) "!"
127.0.0.1:6379> SRANDMEMBER set 2
1) "world"
2) "!"
127.0.0.1:6379> sadd set ffffff
(integer) 1
127.0.0.1:6379> SRANDMEMBER set 2
1) "world"
2) "!"
127.0.0.1:6379> SRANDMEMBER set 2
1) "world"
2) "ffffff"
127.0.0.1:6379> SMEMBERS set
1) "world"
2) "ffffff"
3) "!"
# spop key 随机弹出一个值
127.0.0.1:6379> spop set
"!"
127.0.0.1:6379> spop set
"ffffff"
127.0.0.1:6379> SMEMBERS set
1) "world"
# smove key1 key2 value将key1中的value移到key2集合中,如果value不存在返回0
127.0.0.1:6379> smove set set1 world
(integer) 1
127.0.0.1:6379> SMEMBERS set
(empty list or set)
127.0.0.1:6379> SMEMBERS set1
1) "world"
127.0.0.1:6379> smove set set1 world
(integer) 0
127.0.0.1:6379> SMEMBERS set1
1) "world"
# sdiff key1 key2 求key1与key2的差集
# sdiff key2 key1 求key2与key1的差集
# sinter key1 key2 求key1与key2的交集
# sunion key1 key2 求key1与key2的并集
127.0.0.1:6379> sadd key1 1
(integer) 1
127.0.0.1:6379> sadd key1 2
(integer) 1
127.0.0.1:6379> sadd key1 3
(integer) 1
127.0.0.1:6379> SMEMBERS key1
1) "1"
2) "2"
3) "3"
127.0.0.1:6379> sadd key2 1
(integer) 1
127.0.0.1:6379> sadd key2 4
(integer) 1
127.0.0.1:6379> sadd key2 2
(integer) 1
127.0.0.1:6379> SMEMBERS key2
1) "1"
2) "2"
3) "4"
127.0.0.1:6379> sdiff key1 key2
1) "3"
127.0.0.1:6379> sdiff key2 key1
1) "4"
127.0.0.1:6379> sinter key1 key2
1) "1"
2) "2"
127.0.0.1:6379> SUNION key1 key2
1) "1"
2) "2"
3) "3"
4) "4"
127.0.0.1:6379>
微博中,A用户可以将所有关注的人放在一个set集合中,将他的粉丝也放在一个set集合中
可通过两个用户的关注集合的交集获取共同关注列表
可实现功能:
- 共同关注
- 共同爱好
- 二度好友
- 推荐好友(六度分割理论:你和任何一个陌生人之间所间隔的人不会超五个,也就是说,最多通过六个人你就能够认识任何一个陌生人,有六个相同好友即上升到推荐列表)
Map集合,key-map。
value是一个map集合,本质和String类型没有太大区别,还是一个简单的key-value
127.0.0.1:6379> flushdb
OK
127.0.0.1:6379> keys *
(empty list or set)
# hset key field value 往哈希表中添加filed-value一个键值对
127.0.0.1:6379> hset myhash field1 cieyr
(integer) 1
# hget key field 查看哈希表中field对应的值
127.0.0.1:6379> hget myhash field
(nil)
127.0.0.1:6379> hget myhash field1
"cieyr"
# hmset key field1 value1 field2 value2批量插入,如果field已存在,则覆盖原值
127.0.0.1:6379> hmset myhash field1 ciery field2 hello field world
OK
# hmget key field1 field2批量获取哈希表中field对应的value值
127.0.0.1:6379> hmget myhash field1 field2 field3
1) "ciery"
2) "hello"
3) (nil)
127.0.0.1:6379> hmget myhash field1 field2 field
1) "ciery"
2) "hello"
3) "world"
# hgetall key获取哈希表中所有的field-value
127.0.0.1:6379> hgetall myhash
1) "field1"
2) "ciery"
3) "field2"
4) "hello"
5) "field"
6) "world"
# hdel key field 删除key哈希表中field
127.0.0.1:6379> hdel myhash field1
(integer) 1
127.0.0.1:6379> hgetall myhash
1) "field2"
2) "hello"
3) "field"
4) "world"
# hlen key查看哈希表中的元素个数
127.0.0.1:6379> hlen myhash
(integer) 2
127.0.0.1:6379> hmset myhash field1 ciery field3 pupupu
OK
127.0.0.1:6379> hlen myhash
(integer) 4
127.0.0.1:6379> HGETALL myhash
1) "field2"
2) "hello"
3) "field"
4) "world"
5) "field1"
6) "ciery"
7) "field3"
8) "pupupu"
# hexists key field查看key中field是否存在
127.0.0.1:6379> hexists myhash field1
(integer) 1
127.0.0.1:6379> hexists myhash field4
(integer) 0
# exists key查看key是否存在
127.0.0.1:6379> exists myhahs
(integer) 0
127.0.0.1:6379> exists myhash
(integer) 1
# hkeys key查看哈希表key对应的所有field
127.0.0.1:6379> hkeys myhash
1) "field2"
2) "field"
3) "field1"
4) "field3"
# hvals key查看key对应的所有value
127.0.0.1:6379> HVALS myhash
1) "hello"
2) "world"
3) "ciery"
4) "pupupu"
127.0.0.1:6379> hset myhash field4 4
(integer) 1
# hincrby key field value 给field增加步长value
127.0.0.1:6379> hincrby myhash field4 1
(integer) 5
127.0.0.1:6379> HVALS myhash
1) "hello"
2) "world"
3) "ciery"
4) "pupupu"
5) "5"
# hsetnx key field value如果field不存在,插入一个新的field-value键值对到key对一个的map集合中
127.0.0.1:6379> hsetnx myhash field5 world
(integer) 1
# 如果field存在,不做任何操作
127.0.0.1:6379> hsetnx myhash field5 hello
(integer) 0
127.0.0.1:6379> HVALS myhash
1) "hello"
2) "world"
3) "ciery"
4) "pupupu"
5) "5"
6) "world"
127.0.0.1:6379>
hash操作:hset hget hmset hmget hlen hgetall hdel hkeys hvals hsetnx hexists
hash可用于保存用户经常变更的信息,更适合于对象的存储
String更适合字符串的存储
在set的基础上,增加了一个score值
# zadd key score value往集合key中添加值value,权重为score
127.0.0.1:6379> zadd myset 1 one
(integer) 1
127.0.0.1:6379> zadd myset 2 two
(integer) 1
# zadd支持批量添加
127.0.0.1:6379> zadd myset 3 three 4 four
(integer) 2
# zrange key 0 -1返回zset中的所有值
127.0.0.1:6379> zrange myset 0 -1
1) "one"
2) "two"
3) "three"
4) "four"
# zrangebyscore key -inf +inf从小到大返回所有的值
127.0.0.1:6379> ZRANGEBYSCORE myset -inf +inf
1) "one"
2) "two"
3) "three"
4) "four"
# zrangebyscore key -inf +inf withscores 返回结果携带score权重值
127.0.0.1:6379> ZRANGEBYSCORE myset -inf +inf withscores
1) "one"
2) "1"
3) "two"
4) "2"
5) "three"
6) "3"
7) "four"
8) "4"
# -inf +inf表示负无穷和正无穷
127.0.0.1:6379> ZRANGEBYSCORE myset -inf 2 withscores
1) "one"
2) "1"
3) "two"
4) "2"
# zrem key value从zset中移除值value
127.0.0.1:6379> zrem myset one
(integer) 1
127.0.0.1:6379> zrange myset 0 -1
1) "two"
2) "three"
3) "four"
# zcard key 返回zset的元素个数
127.0.0.1:6379> zcard myset
(integer) 3
# zrevrange key 0 -1 从大到小返回所有的值
127.0.0.1:6379> ZREVRANGE myset 0 -1
1) "four"
2) "three"
3) "two"
# zcount key min max 返回score在区间[min,max]的元素个数
127.0.0.1:6379> zcount myset 1 3
# two three都在区间中
(integer) 2
127.0.0.1:6379> zcount myset 1 2
(integer) 1
127.0.0.1:6379> zcount myset 1 1
(integer) 0
127.0.0.1:6379>
hset:zadd zcard zrem zrange zrevrange zrangebyscore zcount
set排序 存储班级成绩表实现按照成绩进行排序
- 排行榜
- 普通消息1 重要消息2 带权重进行判断
查看朋友的定位,附近的人,打车距离计算
- 有效的经度从-180度到180度。
- 有效的纬度从-85.05112878度到85.05112878度。
# geoadd key longtitude latitude value 插入一个地理位置
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
# geoadd支持批量加入
127.0.0.1:6379> geoadd china:city 106.50 29.53 chongqing 114.05 22.52 shenzhen
(integer) 2
# geopos key value 查看value的地理位置(返回经纬度)
127.0.0.1:6379> GEOPOS china:city beijing
1) 1) "116.39999896287918091"
2) "39.90000009167092543"
# geopos支持批量返回
127.0.0.1:6379> GEOPOS china:city beijing chongqing
1) 1) "116.39999896287918091"
2) "39.90000009167092543"
2) 1) "106.49999767541885376"
2) "29.52999957900659211"
# geodist key value1 value2 km 查看两个地理位置的直线距离,返回单位km
127.0.0.1:6379> GEODIST china:city beijing shanghai km
"1067.3788"
# geodist key value1 value2 m 查看两个地理位置的直线距离,返回单位m
127.0.0.1:6379> GEODIST china:city beijing shanghai m
"1067378.7564"
# georadius key longtitude latitude length 单位 返回距离(longtitude,latitude)length长度的结果
# 前提是要把所有的地理位置加入到key中(实现“附近的人”的功能)
# m 表示单位为米。
# km 表示单位为千米。
# mi 表示单位为英里。
# ft 表示单位为英尺。
127.0.0.1:6379> GEORADIUS china:city 110 30 1000 km
1) "chongqing"
2) "shenzhen"
# withcoord:将位置元素的经纬度返回
127.0.0.1:6379> GEORADIUS china:city 110 30 1000 km withcoord
1) 1) "chongqing"
2) 1) "106.49999767541885376"
2) "29.52999957900659211"
2) 1) "shenzhen"
2) 1) "114.04999762773513794"
2) "22.5200000879503861"
# withdist:将位置元素与中心位置的距离返回
127.0.0.1:6379> GEORADIUS china:city 110 30 1000 km withdist
1) 1) "chongqing"
2) "341.9374"
2) 1) "shenzhen"
2) "924.6408"
# count num:返回结果的个数
127.0.0.1:6379> GEORADIUS china:city 110 30 1000 km withdist withcoord count 1
1) 1) "chongqing"
2) "341.9374"
3) 1) "106.49999767541885376"
2) "29.52999957900659211"
127.0.0.1:6379> GEORADIUS china:city 110 30 1000 km withdist withcoord count 2
1) 1) "chongqing"
2) "341.9374"
3) 1) "106.49999767541885376"
2) "29.52999957900659211"
2) 1) "shenzhen"
2) "924.6408"
3) 1) "114.04999762773513794"
2) "22.5200000879503861"
# georadiusbymember key value length 单位:返回距离value1000长度范围内,距离最大的元素
127.0.0.1:6379> GEORADIUSBYMEMBER china:city beijing 1000 km
1) "beijing"
# geohash key value1 value2返回标准地理空间value的Geohash字符串
127.0.0.1:6379> GEOHASH china:city beijing chongqing
1) "wx4fbxxfke0"
2) "wm5xzrybty0"
GEO的底层实现是通过ZSet实现的,所以也支持zset命令
127.0.0.1:6379> zrange china:city 0 -1 1) "chongqing" 2) "shenzhen" 3) "shanghai" 4) "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) "shenzhen" 3) "shanghai" 127.0.0.1:6379>
基数:不重复的元素个数,可接受误差(0.81%)
Hyperloglog是一个基数统计的算法
- 优点:占用的内存固定,2^64不同的元素的基数只需要12KB内存
使用场景:网页的UV(一个人浏览网页多次,但还是算一个人)在一堆访问结果中查询访问人数(基数)
- 传统的方式:set保存用户的id,然后统计set中的元素数量作为标准判断
- 保存大量的用户id,比较麻烦。毕竟我们的目的是计数,而非保存用户id
# pfadd key value1 value2 value3... 批量插入元素
127.0.0.1:6379> PFADD mykey a b c d e f g h i j
(integer) 1
# pfcount key返回key对应值中的基数
127.0.0.1:6379> PFCOUNT mykey
(integer) 10
127.0.0.1:6379> PFADD mykey2 a b c d f j k p s
(integer) 1
127.0.0.1:6379> PFCOUNT mykey2
(integer) 9
# pfmerge key key1 key2 合并key1和key2取并集到key中
127.0.0.1:6379> PFMERGE mykey3 mykey mykey2
OK
127.0.0.1:6379> PFCOUNT mykey3
(integer) 13
允许误差,使用Herperloglog
不允许误差,使用set存储/其他数据结构
非0即1的数据类型都可使用bitmaps,例如登录与未登录;打卡与未打卡;活跃与不活跃
操作二进制位来记录,只有0和1两个状态
365天 = 365bits 1字节= 8bits 365天大概46个字节左右(占用少!)
# 从0开始标记星期一到星期日,星期一打卡,setbit key field value,value为1
127.0.0.1:6379> setbit sign 0 1
(integer) 0
# 星期二1未打卡,value为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
# getbit key field 获取某天的值
127.0.0.1:6379> getbit sign 6
(integer) 0
127.0.0.1:6379> getbit sign 4
(integer) 1
# bitcount key 获取key中为1的个数(打卡天数)
127.0.0.1:6379> bitcount sign
(integer) 3
使用场景:例如用户的打卡记录,可以每个用户开一个365天的打卡记录,打卡即使用setbit sign:userId 1 1,没打卡结果就是setbit sign:userId 1 1,可通过getbit获取用户某天是否打卡,bitcount获取用户当前打卡总天数
事务:一组命令的集合
一个事务中的所有命令都会被序列化。在事务执行过程中,会按照顺序执行
事务的特点:一次性执行、顺序性、排他性地去执行一系列命令
Redis的单条命令是原子性的,但是事务不保证原子性(运行时出错不影响事务中其他正确命令的执行)
Redis事务没有隔离级别的概念
Redis的事务:
# multi开启事务
127.0.0.1:6379> multi
OK
# 开启事务后输入的命令先入队列
127.0.0.1:6379> set k1 v1
QUEUED
127.0.0.1:6379> set k2 v2
QUEUED
127.0.0.1:6379> set k3 v3
QUEUED
# exec执行事务,依次执行队列中的命令
127.0.0.1:6379> exec
1) OK
2) OK
3) OK
127.0.0.1:6379> get k1
"v1"
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set k1 v1
QUEUED
127.0.0.1:6379> set k2 v2
QUEUED
# discard放弃事务
127.0.0.1:6379> DISCARD
OK
# 队列中的命令都不执行
127.0.0.1:6379> get k1
(nil)
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set k1 v1
QUEUED
# 语法错误,直接提示有误
127.0.0.1:6379> setget k2 v2
(error) ERR unknown command `setget`, with args beginning with: `k2`, `v2`,
127.0.0.1:6379> set k3 v3
QUEUED
# 队列中的命令有语法错误,exec执行事务会显示失败
127.0.0.1:6379> exec
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6379> get k1
(nil)
127.0.0.1:6379> set k1 "d"
OK
127.0.0.1:6379> get k1
"d"
127.0.0.1:6379> multi
OK
# 运行时错误,给一个字符串递增1
127.0.0.1:6379> incr k1
QUEUED
127.0.0.1:6379> get k1
QUEUED
127.0.0.1:6379> set k2 v2
QUEUED
# 执行事务,不影响其他正常语句执行
127.0.0.1:6379> exec
1) (error) ERR value is not an integer or out of range
2) "d"
3) OK
127.0.0.1:6379> get k2
"v2"
悲观锁:认为什么时候都有可能出问题,所以无论做什么都会加锁,影响性能
乐观锁:认为什么时候都不会有问题,所以不会加锁
更新数据的时候会去判断在此期间是否有人修改过这个数据,获取version,更新的时候比较version
127.0.0.1:6379> set money 100
OK
127.0.0.1:6379> set out 0
OK
# 使用watch监视这个key
127.0.0.1:6379> watch money
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> decrby money 20
QUEUED
127.0.0.1:6379> incrby out 20
QUEUED
# 事务正常结束,执行期间数据没有发生变动,所以执行成功
127.0.0.1:6379> exec
1) (integer) 80
2) (integer) 20
一旦事务结束后,对于键key的watch就会自动撤销
秒杀系统中使用乐观锁,确保数据操作的安全性。将数据改变防在事务中执行,并且使用watch对操作的key进行监视,如果事务执行过程中数据被修改,则事务直接执行失败。执行失败后unwatch然后再watch再次尝试multi-exec执行事务即可
Jedis:Redis官方推荐的java连接开发工具,使用Java操作Redis的操作中间件
导入Jedis依赖
<dependency>
<groupId>redis.clientsgroupId>
<artifactId>jedisartifactId>
<version>3.2.0version>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>fastjsonartifactId>
<version>1.2.58version>
dependency>
public class TestPing {
public static void main(String[] args) {
// 建立Jedis连接
Jedis jedis = new Jedis("127.0.0.1", 6379);
// Jedis的指令和Redis中的操作命令同
System.out.println(jedis.ping()); //输出PONG
}
}
public class TestTransaction {
public static void main(String[] args) {
Jedis jedis = new Jedis("127.0.0.1", 6379);
jedis.flushDB();
JSONObject jsonObject = new JSONObject();
jsonObject.put("hello","world");
jsonObject.put("name","ciery");
// 开启事务
Transaction transaction = jedis.multi();
String result = jsonObject.toJSONString();
try {
transaction.set("user1",result);
transaction.set("user2",result);
int i = 1/0; // 语法错误,事务执行失败
transaction.exec();
} catch (Exception e) {
transaction.discard();
e.printStackTrace();
} finally {
System.out.println(jedis.get("user1"));
System.out.println(jedis.get("user2"));
// 关闭Jedis连接
jedis.close();
}
}
}
// 输出
java.lang.ArithmeticException: / by zero
at TestTransaction.main(TestTransaction.java:29)
null
null
SpringBoot操作数据:spring-data jpa jdbc mongodb redis
在SpringBoot2.x之后,使用Jedis替换为lettuce
- jedis:采用的直连数据库,多个线程操作的话不安全。如果想要避免多线程操作,需要使用jedis pool连接池(BIO模式)
- lettuce:采用netty,实例可在多个线程中共享,不存在线程不安全的情况(NIO模式)
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
# springboot 所有的配置类都有一个自动配置类 RedisAutoConfiguration
# 自动配置类会绑定一个application.properties RedisProperties
# 配置redis
spring.redis.host=127.0.0.1
spring.redis.port=6379
spring.redis.database=0
# 配置文件中不能配置redis连接池,需要配置lettuce连接池(默认jedis相关的依赖都没有配置)
@SpringBootTest
class DemoApplicationTests {
@Autowired
RedisTemplate redisTemplate;
@Test
void contextLoads() {
redisTemplate.opsForValue().set("mykey", "myvalue");
System.out.println(redisTemplate.opsForValue().get("mykey"));
}
}
实际操作中多对对象进行序列化(对象类 implements Serializer然后存入到redis中
@Data
@AllArgsConstructor全参构造器
@NoArgsConstructor无参构造器
@Configuration
public class RedisConfig {
// 自定义的RedisTemplate
@Bean
@SuppressWarnings("all")
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
// 为开发方便,一般自定义
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
// Json序列化配置
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
// String的反序列化
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
// key采用String的序列化方式
template.setKeySerializer(stringRedisSerializer);
// hash的key采用String的序列化方式
template.setHashKeySerializer(stringRedisSerializer);
// value序列化方式采用jackson
template.setValueSerializer(jackson2JsonRedisSerializer);
// hash的value序列化采用jackson
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
}
使用@Qualifier指定自定义的redisTemplate
@SpringBootTest
class DemoApplicationTests {
@Autowired
@Qualifier("redisTemplate")
RedisTemplate redisTemplate;
@Test
void contextLoads() {
redisTemplate.opsForValue().set("mykey", "myvalue");
System.out.println(redisTemplate.opsForValue().get("mykey"));
}
}
Redis启动的时候会通过加载Redis.conf来启动
# 1k => 1000 bytes
# 1kb => 1024 bytes
# 1m => 1000000 bytes
# 1mb => 1024*1024 bytes
# 1g => 1000000000 bytes
# 1gb => 1024*1024*1024 bytes
# 包含(相当于java项目中的import)
# include .\path\to\local.conf
# include c:\path\to\other.conf
# 网络绑定
bind 127.0.0.1
# 保护模式
protected-mode yes
# 端口设置
port 6379
# 通用GENERAL
# 以守护进程方式运行。默认为no
daemonize yes
# 如果以后台方式运行,需要指定一个pid文件
pidfile /var/run/redis.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
# 日志文件输出位置,为""的话指直接输出
logfile ""
# 数据库
databases 16
# RDB持久化规则
# 900s内至少一个key进行修改,则进行持久化
save 900 1
save 300 10
save 60 10000
# 持久化出错后是否继续进行持久化操作
stop-writes-on-bgsave-error yes
# 是否压缩RDB文件,默认开启(压缩的话会有消耗CPU资源)
rdbcompression yes
# rdb持久化的时候是否进行错误检查
rdbchecksum yes
# rdb文件保存的位置
dir ./
# 设置redis密码
requirepass 密码
# 设置client连接数
maxclients 10000
# 内存达到上限后的处理策略(移除过期key;报错等)
maxmemory-policy noeviction
- noeviction:当内存使用达到阈值的时候,所有引起申请内存的命令会报错。
- allkeys-lru:在所有键中采用lru算法删除键,直到腾出足够内存为止。
- volatile-lru:在设置了过期时间的键中采用lru算法删除键,直到腾出足够内存为止。
- allkeys-random:在所有键中采用随机删除键,直到腾出足够内存为止。
- volatile-random:在设置了过期时间的键中随机删除键,直到腾出足够内存为止。
- volatile-ttl:在设置了过期时间的键空间中,具有更早过期时间的key优先移除。
# aof配置
# 默认不开启aof模式(以RDB模式运行)
appendonly no
# aof持久化文件的名字
appendfilename "appendonly.aof"
# 每次修改都sync刷写到磁盘上
# appendfsync always
appendfsync everysec
# 由操作系统自己刷写(从page cache到磁盘,速度最快)
# appendfsync no
Redis是一个内存数据库,如果不将内存中的数据库状态保存在磁盘,那么一旦服务器进程退出,服务器中的数据库状态就会消失,所以Redis提供了持久化功能
rdb文件中存储的是压缩后的二进制文件,aof文件中存储的是写操作命令
在执行时间间隔内,将内存中的数据集快照写入磁盘,即Snapshot快照,恢复时是将快照文件直接读到内存中
Redis会单独创建(fork)一个子进程来进行持久化,先将数据写入到一个临时文件(临时RDB文件)中,待持久化过程结束了,再用这个临时文件替换上次持久化好的文件。
整个过程中,主进程是不进行IO操作的,确保极高的性能。如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,RDB方式比AOF方式更高效
RDB的缺点是最后一次持久化后的数据可能会丢失
默认使用RDB进行持久化
RDB保存的文件:dump.rdb
1,save的规则满足
2,执行flushall命令
3,退出redis
只需要将rdb文件放在redis启动目录即可,redis启动的时候会自动检查dump.rdb并恢复其中的数据
查看需要存在的位置:
127.0.0.1:6379> config get dir 1) "dir" # 这个目录中存在dump.rdb文件,启动就会自动恢复其中的数据 2) "D:\\install\\redis"
1,适合大规模的数据恢复
2,对数据的完整性要求不高
1,需要一定的时间间隔进行操作,如果Redis意外宕机,则最后一次修改数据就会丢失(rdb还未完成持久化过程) 一般会将rdb文件进行备份
2,fork进程的时候,会占用一定的内存空间
将所有命令记录下来,恢复的时候将文件重新执行一遍即可
AOF保存的文件:appendonly.aof文件
默认是不开启的,需要修改配置文件开启:appendonly yes,然后重启生效
上述图包含持久化到旧的AOF文件+重写到临时AOF+缓存最后覆盖旧AOF完成重写
以日志的形式记录每个写操作,将Redis执行过的所有制令记录下来(读操作不记录),只许追加文件,不可改写文件。Redis启动之初会读取该文件重新构建数据
即redis重启会根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作
如果AOF文件出错,redis启动会出错。可通过redis-check-aof – fix appendonly.aof进行修复
重写机制:aof文件大于64mb
1,每一次修改都会同步,文件的完整性会更好
2,每秒同步一次的话,可能会丢失一秒的数据
3,从不同步的话,效率最高
1,相对于数据文件来讲,aof远大于rdb,修复速度也比rdb慢
2,AOF的运行效率慢于rdb,redis的默认持久化策略是RDB持久化
pub/sub是一种消息通信模式,发送者(pub)发送消息,订阅者(sub)接收消息
Redis客户端可订阅任意数量的频道
接收消息的内容:
- message表示消息
- testchannel表示发来消息的频道
- world表示在这个频道发的消息
原理:Redis是通过C实现的,可通过分析Redis源码中的pubsub.c文件了解发布和订阅机制的底层实现。
Redis通过PUBLISH、SUBSCRIBE和PSUBCRIBE等命令实现发布和订阅功能
通过SUBSCRIBE命令订阅某频道后,redis-server里维护了一个字典,字典的键就是channel,字典的值是一个链表,链表中保存了所有订阅这个channel的客户端。SUBSCRIBE命令的关键就是将客户端添加到给定channel的订阅链表中。
通过PUBLISH命令向订阅者发送消息,redis-server使用给定的频道作为键,在其维护的channel字典中查找记录了订阅这个频道的所有客户端的链表,遍历这个链表,将消息发布给所有订阅者。
Pub/Sub从字面上理解就是发布(Publish)与订阅(Subscribe)。在Redis中,可设定对某一个key值进行消息发布及消息订阅,当一个key值进行消息发布后,所有订阅它的客户端都会收到相应的消息。
使用场景:实时消息系统,例如普通即时聊天、群聊等功能(复杂场景使用Kafka、RabbitMQ)
主从复制:将一台Redis服务器的数据,复制到其他Redis服务器中,前者称为主节点(master/leader),后者称为从节点(slave/follower)
数据的复制是单向的, 只能从主节点到从节点。master负责写读,slave负责读
默认情况下,每台Redis服务器都是主节点,且一个主节点可有多个从节点(或没有从节点),但一个从节点只能有一个主节点
主从复制的作用:
1,数据冗余:主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式
2,故障恢复:当主节点出现问题时,可由从节点提供服务,实现快速的故障恢复。实际是一种服务的冗余
3,负载均衡:在主从复制的基础上,配合读写分离,可由主节点提供写服务,由从节点提供读服务(即写Redis数据时应用连接主节点,读Redis数据时应用连接从节点)分担服务器负载;写少读多场景下(80%场景都是读场景),通过多个从节点分担负载提高Redis服务器的并发量
4,高可用基石:哨兵和集群能够实施的基础
一般而言,将Redis运用于工程项目中使用一台Redis是不可的,原因如下:
1,结构上,单个Redis服务器会发生单点故障,并且一台服务器需要处理所有的请求负载,压力较大
2,容量上,单个Redis服务器内存容量有限,并且也不会将服务器内容用作Redis存储。(一般来讲单台Redis最大使用内存不应超过20G)
# 查看当前库的信息
info replication
# Replication
role:master
connected_slaves:0
master_replid:d196605e7967368f25237df22c4aab61fe86ee1e
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
# 一个服务器节点配置一主多从模式,复制配置文件,依次修改端口、pid名称、log文件名字、dump.rdb名字的信息,修改结束后重启redis,分别制定对应的三个配置文件即可
# 默认启动后三台都是主机,可通过replicaof 主ip 主port 将当前的节点改为从节点
主机断开连接,从机依旧是连接到主机的,可以执行读操作,依旧无法执行写操作(此时可使用
slaveof no one
让自己变为主机,其他节点再手动与这个节点建立“主-从”关系;原来的主机恢复连接后只能是个从节点)之后主机恢复连接后,主从机关系不变,主机负责写操作,从机负责读操作
从机断开连接后自动升级为主机,恢复主从关系后,从机再次从主机上拿到数据(全量复制+增量复制)
slave启动成功连接到master之后会发送一个sync同步命令
master接到命令,启动后台的RDB持久化进程并将RDB文件发送给slave,同时将所有接收到的写操作写入replication buffer中,等待slave同步完RDB文件后发送给slave
全量复制:master执行RDB持久化进程生成RDB文件并发送给slave
增量复制:主从恢复连接后,master将repl_backlog_buffer中的差量数据发送给replication_buffer,后发送给slave
因为主机生成RDB文件和传输RDB都消耗很大,所以为减轻多个从机下主机的压力,使用“主-从-从”模式
现公司基本都用Redis的哨兵模式
Redis2.8之后,提供Sentinel哨兵模式,解决之前人工干预的手动切换服务器为主节点的操作
过程:
Redis提供哨兵的命令,哨兵是一个独立进程可独立运行,其原理是哨兵通过发送命令,等待Redis服务器响应,从而监控运行的多个Redis实例,如果出现故障会根据投票数,自动将从库切换为主库
哨兵模式的作用:
- 通过发送命令,让Redis服务器返回监控其运行状态,包括主服务器和从服务器
- 当哨兵检测到master宕机,会自动将slave切换到master,然后通过订阅发布模式通知其他从服务器,修改配置文件,让其切换主机
但是只使用一个哨兵对Redis服务器进行监控,如果这个哨兵出现问题,就无法知道Redis服务器的运行状态,所以会使用多个哨兵进行监控,各个哨兵之间还会进行监控,形成多哨兵模式
主观下线:假设主服务器宕机,哨兵1先检测到这个结果,系统不会马上进行failover进程(仅仅是哨兵1主观认为主服务器不可用)
客观下线:当之后的哨兵也检测到主服务器不可用,且数量达到一定值的时候。哨兵间会进行一次投票,投票结果由一个哨兵发起(随机一个哨兵),进行failover(故障转移)操作。切换成功后,通过订阅发布模式,让各个哨兵把自己监控的从服务器实现切换主机
实现过程:
1,配置哨兵配置文件sentinel.conf,1表示主机挂了后slave投票,票数最多的成为主机
sentinel montitor redis 127.0.0.1 6379 1
2,启动哨兵
redis-sentinel ../config/sentinel.conf
3,手动让主机宕机,观测哨兵可以看到过一会选举一个从机为新的主机,在从机上使用info replication可见主机信息被改变
4,重新启动原主机,info replication查看这个节点的信息,发现其为从机(如果没有哨兵模式推举新的主机,原主机恢复后依旧为主机)
优点:
1,基于主从复制,具有数据冗余、故障转移、负载均衡、高可用基石等优点
2,主从可切换,故障可转移,系统可用性更好
3,主从模式的升级,手动切换到自动切换,更加健壮
缺点:
1,Redis集群容量达到一定上限后,不好在线扩容,
2,哨兵的集群模式配置复杂
Redis缓存极大提升应用程序的性能和效率,尤其在数据查询方面,但也带来数据一致性、缓存穿透、缓存雪崩和缓存击穿的问题
缓存穿透:用户想要查询一个数据,发现redis内存数据库没有数据,即缓存没有命中,于是向持久层数据库(例如MySQL、HBase等)查询,发现也没有,于是查询失败。如果多个用户缓存没有命中(例如秒杀环境),都去请求持久层数据库,就会给持久层数据库带来压力,相当于出现缓存穿透(查不到数据导致的)
解决方法:
- 布隆过滤器:一种数据结构,对所有可能查询的参数以hash形式存储,在控制层先进行校验,不符合则丢弃,从而避免对底层存储系统的查询压力
- 缓存空对象:当存储层不命中后,即使返回空对象也缓存起来。同时设置一个过期时间,之后再访问这个数据将会从缓存中获取,保护后端数据源
存在的问题:
- 如果空值可被缓存起来,这就意味着缓存需要更多的空间存储更多的键(可能有很多空值的键)
- 即使对空值设置了过期时间,还是会存在缓存层和存储层的数据会有一段时间窗口的不一致,对需要保持一致性的业务会有影响(缓存中有空值,存储层没有)
缓存击穿:一个key非常热点,大并发集中对这个key进行访问,当这个key失效的瞬间,持久的大并发就会穿破缓存,直接请求数据库,就像一个屏障上凿开一个洞(查询量太大导致的)
当某个key在过期瞬间,有大量并发请求访问,这类数据一般是热点数据。由于缓存过期,会同时访问数据库来查询最新数据,并回写缓存,会导致数据库瞬间压力过大(例如导致wb服务器宕机)
解决方案:
- 设置热点数据永不过期:从缓存层面看,只要不设置过期时间就不会出现热点key过期后产生的问题
- 加互斥锁:使用分布式锁,保证对于每个key同时只有一个线程去查询后端服务,其他线程在没有获得分布式锁的权限之前等待即可。
- 将高并发的压力转移到分布式锁,对分布式锁的考验大
缓存雪崩:指在某一个时间段,缓存集中过期失效/Redis宕机
场景:双十二为迎接抢购,会将商品集中放在缓存中,假设缓存一个小时。一个小时缓存过期,对这批商品的访问查询都会落到数据库上。对于数据库而言,就会产生周期性的压力波峰,于是所有的请求都会到达存储层,存储层的调用量就会暴增,造成存储层也挂掉(一般也有停掉一些服务,保证高可用)
解决方案:
- redis高可用:增设几台redis(搭建redis集群)
- 限流降级:在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待
- 数据预热:在正式部署前,先把可能的数据先访问以便,这样大量访问的数据就会加载到缓存中。在即将发生大并发访问前手动触发加载缓存不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀