分布式:一个业务分拆多个子业务,部署在不同的服务器上
集群:同一个业务,部署在多个服务器上
1.单机MYSQL
2.memcached(缓存) + MYSQL+ 垂直拆分(读写分离)
分库拆表 + 水平拆分 + MySQL集群
本质:数据库 读 写
NoSQL
NoSQL = Not Only SQL 不仅仅是SQL
关系型数据库:行 列 MySQL Oracle Sql server SQLlite
非关系型数据库:{key:value} Redis MongDB 对象存储
不仅仅是数据
没有固定的查询语言
键值对存储,列存储,文档存储,图形数据库(社交关系)
最终一致性
CAP定理 BASE
大数据的3V和3高
大数据时代的3V:主要是描述问题的
海量Volume
多样Variety
实时Velocity
大数据时代的3高:主要是对程序的要求
高并发
高可扩
高性能
KV键值对
文档型数据库(bson格式和json一样)
MongoDB是一个基于分布式文件存储的数据库,c++编写,主要用来处理大量的文档。
一个介于关系型数据库和非关系型数据库中间的产品
列存储数据库
图关系数据库
存的不是图形,放的关系,比如社交关系,广告推荐
Remote Dicitionary Server 远程字典服务
一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。
中文文档:https://www.redis.com.cn/documentation.html
简单来说 redis 就是一个数据库,不过与传统数据库不同的是 redis 的数据是存在内存中的,所以存写速度非常快,因此 redis 被广泛应用于缓存方向。另外,redis 也经常用来做分布式锁。redis 提供了多种数据类型来支持不同的业务场景。除此之外,redis 支持事务 、持久化、LUA脚本、LRU驱动事件、多种集群方案。
干什么?
1、内存存储、持久化,内存是断电即失,rdb、aof
2、效率高,用于高速缓存
3、发布订阅系统
4、地图信息分析
5、计时器、计数器(浏览器)
特性
1、多样的数据类型
2、持久化
3、集群
4、事务
主要从“高性能”和“高并发”这两点来看待这个问题。
高性能:
假如用户第一次访问数据库中的某些数据。这个过程会比较慢,因为是从硬盘上读取的。将该用户访问的数据存在数缓存中,这样下一次再访问这些数据的时候就可以直接从缓存中获取了。操作缓存就是直接操作内存,所以速度相当快。如果数据库中的对应数据改变的之后,同步改变缓存中相应的数据即可!
高并发:
直接操作缓存能够承受的请求是远远大于直接访问数据库的,所以我们可以考虑把数据库中的部分数据转移到缓存中去,这样用户的一部分请求会直接到缓存这里而不用经过数据库。
redis支持更丰富的数据类型(支持更复杂的应用场景):Redis不仅仅支持简单的k/v类型的数据,同时还提供 list,set,zset,hash等数据结构的存储。memcache支持简单的数据类型,String。
Redis支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用,而 Memecache把数据全部存在内存之中。
集群模式:memcached没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据;但是 redis 目前 是原生支持 cluster 模式的.
Memcached是多线程,非阻塞IO复用的网络模型;Redis使用单线程的多路 IO 复用模型。
需要的东西
1、官网:https://redis.io/
2、中文网:http://redis.cn/
推荐在Linux服务器搭建
redis的启动方式
1.直接启动
进入redis根目录,执行命令:
#加上‘&’号使redis以后台程序方式运行.``/redis-server` `&
2.通过指定配置文件启动
可以为redis服务启动指定配置文件,例如配置为/etc/redis/6379.conf
进入redis根目录,输入命令:.``/redis-server` `/etc/redis/6379``.conf
#如果更改了端口,使用
redis-cli
客户端连接时,也需要指定端口,例如:redis-cli -p 6380
执行:redis-cli -p 6379
关闭redis:shutdown
退出:exit
ps -ef|grep redis
#测试 100个并发连接 100000请求
redis-benchmark -h localhost -p 6369 -c 100 -n 100000
select n 打开第n个数据库 共16个 从0开始
dbsize 数据库大小
127.0.0.1:6379> select 3 #切换数据库
OK
127.0.0.1:6379[3]> select 0
OK
127.0.0.1:6379> dbsize
(integer) 0
127.0.0.1:6379> keys * #查看数据库所以的key
1) "name"
127.0.0.1:6379>
127.0.0.1:6379> flushdb #情况当前数据库
OK
127.0.0.1:6379> keys *
(empty array)
127.0.0.1:6379[1]> flushall #清空所有数据库
7929:M 10 Dec 2020 22:12:17.518 * DB saved on disk
OK
redis是单线程的!
它是很快的,Redis是基于内存操作,CPU不是Redis性能瓶颈,Redis的瓶颈是根据机器的内存和网络带宽
Redis是C语言写的,官方提供的数据是100000+的QPS,这个数据不比采用单进程多线程的同样基于内存的 KV 数据库 Memcached 差
1、高性能的服务器一定是多线程的?
2、多线程(CPU上下文会切换)一定比单线程效率高?
CPU>内存>硬盘
核心:Redis是将所有的数据全部放在内存中,绝大部分请求是纯粹的内存操作,非常快速.避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗;
Redis是一个开源(BSD许可),内存中的数据结构存储,用作数据库、缓存和消息代理。它支持数据结构,如字符串、哈希、列表、集合、带范围查询的排序集、位图、超日志、带有radius查询和流的地理空间索引。Redis内置了复制、Lua脚本、LRU逐出、事务和不同级别的磁盘持久性,并通过Redis Sentinel和Redis Cluster自动分区来提供高可用性
127.0.0.1:6379> set name hidisan
OK
127.0.0.1:6379> exists name #判断当前的key是否存在
(integer) 1
127.0.0.1:6379> move name 1 #移出当前key 1代表当前数据库
(integer) 1
127.0.0.1:6379> expire name 10 #十秒过期
(integer) 1
127.0.0.1:6379> ttl name
(integer) 2
127.0.0.1:6379> ttl name
(integer) -2
127.0.0.1:6379> get name
(nil)
127.0.0.1:6379> type name #查看当前key的类型
string
127.0.0.1:6379> type age
string
常用命令: set,get,decr,incr,mget 等。
127.0.0.1:6379> set key1 v1
OK
127.0.0.1:6379> exists key1 #判断是否存在
(integer) 1
127.0.0.1:6379> append key1 "hello" #追加字符串,如果不存在key 相当于set 一个key
(integer) 7
127.0.0.1:6379> get key1
"v1hello"
127.0.0.1:6379> strlen key1 #获取字符串的长度
(integer) 7
###########################################################################
127.0.0.1:6379> set views 0 #初始为0
OK
127.0.0.1:6379> get views
"0"
127.0.0.1:6379> incr views #自增1 i++
(integer) 1
127.0.0.1:6379> incr views #自减1 i--
(integer) 2
127.0.0.1:6379> get views
"2"
127.0.0.1:6379> decr views
(integer) 1
127.0.0.1:6379> decr views
(integer) 0
127.0.0.1:6379> get views
"0"
127.0.0.1:6379> decr views
(integer) -1
127.0.0.1:6379> decr views
(integer) -2
127.0.0.1:6379> incrby views 10 #设置步长 i+=10
(integer) 17
127.0.0.1:6379> decrby views 5
(integer) 12
############################################################################
#字符串范围 range 相当于substring
127.0.0.1:6379> set key1 "hell0 hidisan!" #设置key1的值
OK
127.0.0.1:6379> get key1
"hell0 hidisan!"
127.0.0.1:6379> getrange key1 0 3 #截取字符串 [0,3]
"hell"
127.0.0.1:6379> getrange key1 0 -1 #获取全部的字符串
"hell0 hidisan!"
###########################################################################
#替换 指定开始的字符串 相当于replace
127.0.0.1:6379> set key2 "abcdefghig"
OK
127.0.0.1:6379> get key2
"abcdefghig"
127.0.0.1:6379> setrange key2 1 xx
(integer) 10
127.0.0.1:6379> get key2
"axxdefghig"
###########################################################################
#setex (set with expire) 设置过期时间
#setnx(set if not exist) 不存在设置 在分布式锁中常常使用
127.0.0.1:6379> setex key3 30 "helllo" #设置key3的值 30秒过期
OK
127.0.0.1:6379> ttl key3
(integer) 25
127.0.0.1:6379> setnx mykey "redis" #如果mykey不存在 创建mykey
(integer) 1
127.0.0.1:6379> keys *
1) "key1"
2) "mykey"
3) "key2"
127.0.0.1:6379> ttl key3
(integer) -2
127.0.0.1:6379> setnx mykey "MongDB" #如果mykey存在,创建失败
(integer) 0
127.0.0.1:6379> get mykey
"redis"
###########################################################################
#mset
#mget
127.0.0.1:6379> mset key1 v1 key2 v2 key3 v3 #设置多个值
OK
127.0.0.1:6379> keys *
1) "key3"
2) "key1"
3) "key2"
127.0.0.1:6379> mget key1 key2 key3 #同时获取多个值
1) "v1"
2) "v2"
3) "v3"
127.0.0.1:6379> msetnx key1 v1 key4 v4 #是一个原子性的操作,要么一起成功,要么一起失败
(integer) 0
#对象
set user:1 {
name:zhangsan,age:3} #设置一个user:1 对象 值为json字符串 来保存
#这里的key是一个巧妙地设计: user:{id}:{filed},如此
127.0.0.1:6379> mset user:1:name zhangsan user:1:age 2
OK
127.0.0.1:6379> mget user:1:name user:1:age
1) "zhangsan"
2) "2"
###########################################################################
getset #先get然后set
127.0.0.1:6379> getset db redis #如果不存在就返回nil
(nil)
127.0.0.1:6379> get db
"redis"
127.0.0.1:6379> getset db mongdn #如果存在就返回原来的值,再设置新的值
"redis"
127.0.0.1:6379> get db
"mongdn"
String数据结构是简单的key-value类型,value其实不仅可以是String,也可以是数字。
String类似的使用场景:value除了是字符串还可以是数字
计数器
统计多单位的数量
粉丝数
对象缓存存储
常用命令: lpush,rpush,lpop,rpop,lrange等
可以把list完成栈、队列、阻塞队列
所以list命令都是以l 开头
######################################################################
127.0.0.1:6379> lpush list two #将一个值或多个值,插入列表头部,左边放进去
(integer) 1
127.0.0.1:6379> lpush list one
(integer) 2
127.0.0.1:6379> lpush list three
(integer) 3
127.0.0.1:6379> lrange list 0 -1 #获取list
1) "three"
2) "one"
3) "two"
127.0.0.1:6379> lrange list 0 1
1) "three"
2) "one"
127.0.0.1:6379> rpush list four #将一个值或多个值,插入列表尾部
(integer) 4
127.0.0.1:6379> lrange list 0 -1
1) "three"
2) "one"
3) "two"
4) "four"
127.0.0.1:6379>
######################################################################
pop
127.0.0.1:6379> lrange list 0 -1
1) "three"
2) "one"
3) "two"
4) "four"
127.0.0.1:6379> lpop list #移出列表第一个
"three"
127.0.0.1:6379> rpop list #移出列表最后一个
"four"
127.0.0.1:6379> lrange list 0 -1
1) "one"
2) "two"
######################################################################
lindex
127.0.0.1:6379> lrange list 0 -1 #通过下标获得list中某一个值
1) "one"
2) "two"
127.0.0.1:6379> lindex list 1
"two"
127.0.0.1:6379> lindex list 0
"one"
######################################################################
127.0.0.1:6379> flushdb
OK
127.0.0.1:6379> lpush list one
(integer) 1
127.0.0.1:6379> lpush list two
(integer) 2
127.0.0.1:6379> lpush list three
(integer) 3
127.0.0.1:6379> llen list #返回列表的长度
(integer) 3
######################################################################
移出指定的值
127.0.0.1:6379> lrange list 0 -1
1) "three"
2) "one"
3) "two"
4) "three"
5) "two"
6) "three"
127.0.0.1:6379> clear
127.0.0.1:6379> lrem list 2 three #指定个数的value,精确匹配
(integer) 2
127.0.0.1:6379> lrange list 0 -1
1) "one"
2) "two"
3) "two"
4) "three"
######################################################################
trim 修剪 list 剪裁
127.0.0.1:6379> rpush list "helllo"
(integer) 1
127.0.0.1:6379> rpush list "hello1"
(integer) 2
127.0.0.1:6379> rpush list "hello2"
(integer) 3
127.0.0.1:6379> rpush list "hello3"
(integer) 4
127.0.0.1:6379> ltrim list 1 2 #通过下标 start stop 剪裁指定的长度,但是list被改变
OK
127.0.0.1:6379> lrange list 0 -1
1) "hello1"
2) "hello2"
######################################################################
rpoplpush [source][destination] #移出列表最后一个元素,并且移动到新的列表中
127.0.0.1:6379> rpush list "hello"
(integer) 1
127.0.0.1:6379> rpush list "hello1"
(integer) 2
127.0.0.1:6379> rpush list "hello2"
(integer) 3
127.0.0.1:6379> rpoplpush list list1
"hello2"
127.0.0.1:6379> lrange list 0 -1 #查看原列表
1) "hello"
2) "hello1"
127.0.0.1:6379> lrange list1 0 -1 #查看目标列表
1) "hello2"
######################################################################
lset 将列表指定下标的值 替换为另外一个值 相当于更新操作
127.0.0.1:6379> lpush list value1
(integer) 1
127.0.0.1:6379> lrange list 0 -1
1) "value1"
127.0.0.1:6379> lset list 0 hidisan #
OK
127.0.0.1:6379> lrange list 0 -1
1) "hidisan"
######################################################################
linsert [before][after]
127.0.0.1:6379> linsert list before hidisan hello
(integer) 2
127.0.0.1:6379> lrange list 0 -1
1) "hello"
2) "hidisan"
实际是一个链表,before Node after ,left right都可以插入值
移出所有值,空链表,也代表不存在
在两边插入或改动,效率最高
list底层是快速链表,快速链表相对于普通链表使用的是连续地址空间块,当数据过多是,通过指针连接地址空间块
消息队列(lpush rpop) , 栈(lpush lpop)
常用命令: sadd,spop,smembers,sunion 等
set中的值不能重复 无序
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 hello #重复添加
(integer) 0
127.0.0.1:6379> smembers set #查看set所有值
1) "world"
2) "hello"
127.0.0.1:6379> sadd set hidisan
(integer) 1
127.0.0.1:6379> smembers set #无序
1) "world"
2) "hidisan"
3) "hello"
127.0.0.1:6379> sismember set hello #判断某一个值,是不是在set集合中
(integer) 1
#########################################################################
127.0.0.1:6379> scard set #获取set集合中的内容元素个数
(integer) 3
#########################################################################
127.0.0.1:6379> srem set hello #移出set中的指定元素
(integer) 1
127.0.0.1:6379> scard set
(integer) 2
127.0.0.1:6379> smembers set
1) "world"
2) "hidisan"
#########################################################################
set #无序 不重复
127.0.0.1:6379> srandmember set
"world"
127.0.0.1:6379> srandmember set #随机抽选出一个元素
"hidisan"
127.0.0.1:6379> srandmember set
"world"
127.0.0.1:6379> srandmember set
"world"
127.0.0.1:6379> srandmember set 2 #随机抽选出指定个数元素
1) "world"
2) "hidisan"
#########################################################################
删除指定的key,随机删除key
127.0.0.1:6379> SMEMBERS set
1) "hello3"
2) "hello1"
3) "hello2"
4) "hidisan"
5) "hello4"
127.0.0.1:6379> spop set #随机删除元素
"hello1"
127.0.0.1:6379> spop set
"hello3"
#########################################################################
将指定一个值,移动到另外一个set集合
127.0.0.1:6379> smove set set2 hidsian #移动到指定 [source] [destination] [member]
(integer) 1
127.0.0.1:6379> SMEMBERS set2
1) "set2"
2) "hidsian"
127.0.0.1:6379> SMEMBERS set
1) "world"
2) "hello"
#########################################################################
127.0.0.1:6379> sdiff set set1 #差集
1) "c"
2) "b"
127.0.0.1:6379> sinter set set1 #交集
1) "d"
2) "a"
127.0.0.1:6379> sunion set set1 #并集
1) "d"
2) "c"
3) "a"
4) "e"
5) "b"
6) "f"
常用命令: hget,hset,hgetall 等。
Map 集合 ,key-Map key-
本质和String类型没有太大区别,还是一个简单的key-value
127.0.0.1:6379> hset hash field1 hidisan #set一个具体的key-value
(integer) 1
127.0.0.1:6379> hget hash field1 #获取一个字段值
"hidisan"
127.0.0.1:6379> hmset hash field1 hello field2 world #set多个key-value
OK
127.0.0.1:6379> hmget hash field1 field2 #获取多个字段值
1) "hello"
2) "world"
127.0.0.1:6379> hgetall hash #获取全部的数据
1) "field1"
2) "hello"
3) "field2"
4) "world"
127.0.0.1:6379> hdel hash field1 #删除hash指定key字段,对应value也消失了
(integer) 1
127.0.0.1:6379> hgetall hash
1) "field2"
2) "world"
#######################################################################
127.0.0.1:6379> hgetall hash
1) "field2"
2) "world"
127.0.0.1:6379> hlen hash #获取hash表的字段数量
(integer) 1
127.0.0.1:6379> hexists hash field4 #判断hash指定字段是否存在
(integer) 1
#######################################################################
#只获得所以的field字段
#只获得所有的value
127.0.0.1:6379> hkeys hash #只获得所以的field字段
1) "field2"
2) "field1"
3) "field3"
4) "field4"
127.0.0.1:6379> hvals hash #只获得所有的value
1) "world"
2) "hello"
3) "hidisan"
4) "hidisan1"
#######################################################################
127.0.0.1:6379> hset hash field0 5
(integer) 1
127.0.0.1:6379> hincrby hash field0 1 #指定增量
(integer) 6
127.0.0.1:6379> hincrby hash field0 -2
(integer) 4
127.0.0.1:6379> hsetnx hash field4 hidis #如果不存在则可以设置,若存在不能设置
(integer) 0
hash变更的数据 user 的 name、 age,尤其是用户信息之类的保存,以及经常变动的信息,hash更适合于对象的存储,String更加适合字符串存储
127.0.0.1:6379> hset user:1 name hidisan age 20
(integer) 2
127.0.0.1:6379> hget user:1 name
“hidisan”
127.0.0.1:6379> hget user:1 age
“20”127.0.0.1:6379> hmget user:1 name age
- “hidisan”
- “20”
常用命令: zadd,zrange,zrem,zcard等
和set相比,sorted set增加了一个权重参数score,使得集合中的元素能够按score进行有序排列。
举例: 在直播系统中,实时排行信息包含直播间在线用户列表,各种礼物排行榜,弹幕消息(可以理解为按消息维度的消息排行榜)等信息,适合使用 Redis 中的 SortedSet 结构进行存储。
127.0.0.1:6379> zadd set 1 one #添加
(integer) 1
127.0.0.1:6379> zadd set 2 two
(integer) 1
127.0.0.1:6379> zadd set 3 three
(integer) 1
127.0.0.1:6379> zrange set 0 -1
1) "one"
2) "two"
3) "three"
###################################################################################
127.0.0.1:6379> zadd salary 2500 xiaohong 3000 zhangsan 1000 hidisan
(integer) 3
127.0.0.1:6379> zrange salary 0 -1
1) "hidisan"
2) "xiaohong"
3) "zhangsan"
127.0.0.1:6379> ZRANGEBYSCORE salary -inf +inf [mix][max]
1) "hidisan"
2) "xiaohong"
3) "zhangsan"
127.0.0.1:6379> ZRANGEBYSCORE salary +inf -inf
(empty array)
127.0.0.1:6379> ZRANGEBYSCORE salary -inf +inf withscores #正序 附带成绩
1) "hidisan"
2) "1000"
3) "xiaohong"
4) "2500"
5) "zhangsan"
6) "3000"
127.0.0.1:6379> ZREVRANGE salary 0 -1 #倒序
1) "zhangsan"
2) "xiaohong"
3) "hidisan"
#######################################################################
#移出
127.0.0.1:6379> zrange salary 0 -1
1) "hidisan"
2) "xiaohong"
3) "zhangsan"
127.0.0.1:6379> zrem salary xiaohong
(integer) 1
127.0.0.1:6379> zrange salary 0 -1
1) "hidisan"
2) "zhangsan"
127.0.0.1:6379> zcard salary #获取集合中的个数
(integer) 2
ZREVRANGEBYSCORE key max min [WITHSCORES] [LIMIT offset count]
ZCOUNT key min max
ZCOUNT 返回有序集 key 中, score 值在 min 和 max 之间(默认包括 score 值等于 min 或 max )的成员的数量。
排行榜
#geoadd 添加地理位置
#规则:南北极无法直接添加
#参数 key 值(经度、纬度、名称)
127.0.0.1:6379> geoadd china:key 116.40 39.90 beijing
(integer) 1
127.0.0.1:6379> geoadd china:key 121.470 31.23 shanghai
(integer) 1
127.0.0.1:6379> geoadd china:key 106.50 29.53 chongqing
(integer) 1
127.0.0.1:6379> geoadd china:key 114.05 22.32 shenzhen
(integer) 1
127.0.0.1:6379> geoadd china:key 120.16 30.24 hangzhou
(integer) 1
127.0.0.1:6379> geoadd china:key 108.94 34.33 xian
(integer) 1
#########################################################################
#geopos 获取指定的城市的经度、纬度
127.0.0.1:6379> GEOPOS china:key beijing
1) 1) "116.39999896287918091"
2) "39.90000009167092543"
127.0.0.1:6379> GEOPOS china:key beijing shanghai xian
1) 1) "116.39999896287918091"
2) "39.90000009167092543"
2) 1) "121.47000163793563843"
2) "31.22999903975783553"
3) 1) "108.93999785184860229"
2) "34.33000103844675976"
127.0.0.1:6379>
#########################################################################
#geodist
#两人的距离 单位:m 米 km 千米 mi英里 ft英尺
127.0.0.1:6379> GEODIST china:key beijing shanghai km #北京-上海的直线距离
"1067.3788"
#########################################################################
georadius:给定一个坐标点,查找附近指定距离范围内的元素,相当于附近的人
语法:georadius key longitude latitude radius m|km|ft|mi [withcoord] [withdist] [withhash] [COUNT count] [ASC|DESC] [STORE key] [STOREDIST key]
localhost:6379> georadius China:city 117.25 31.83 500 km # 查看方圆500km内的城市
杭州
上海
localhost:6379> georadius China:city 117.25 31.83 500 km withcoord # withcoord 结果带上坐标
杭州
120.15000075101852417
30.2800007575645509
上海
121.47000163793563843
31.22999903975783553
localhost:6379> georadius China:city 117.25 31.83 500 km withdist # withdist 带上距离
杭州
325.6740
上海
405.5792
localhost:6379> georadius China:city 117.25 31.83 500 km withdist count 1 asc # 由近到远取一个
杭州
325.6740
#########################################################################
georadiusbymember:与georadius功能相同,
不同的是georadiusbymember的中心点是geo中的成员而不是经纬度
localhost:6379> georadiusbymember China:city 上海 500 km withdist
杭州
164.5694
上海
0.0000
#########################################################################
zrem:应为geo的本质是zset,所以删除也是用zrem
localhost:6379> zrange China:city 0 -1
杭州
上海
北京
localhost:6379> zrem China:city 北京
1
localhost:6379> zrange China:city 0 -1
杭州
上海
#########################################################################
geohash:获取元素的 hash 值
geohash 可以获取元素的经纬度编码字符串,上面已经提到,它是 base32 编码。 你可
以使用这个编码值去 http://geohash.org/${hash}中进行直接定位,它是 geohash 的标准编码。该命令将返回11个字符的geohansh字符串
值.
语法:geohash key member [member ...]
localhost:6379> geohash China:city 上海
wtw3sj5zbj
geo底层的实现原理其实就是zset
移出就可以用zset的命令行:zrem
用来做基数统计的算法,网页的UV(一个人访问网站多次,但算一次),HyperLogLog 的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定 的、并且是很小的。
在 Redis 里面,每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 2^64 个不同元素的基 数。这和计算基数时,元素越多耗费内存就越多的集合形成鲜明对比。
比如数据集 {1, 3, 5, 7, 5, 7, 8}, 那么这个数据集的基数集为 {1, 3, 5 ,7, 8}, 基数(不重复元素)为5。 基数估计就是在误差可接受的范围内,快速计算基数。
PFADD key element [element ...] 添加指定元素到 HyperLogLog 中。
PFCOUNT key [key ...] 返回给定 HyperLogLog 的基数估算值。
PFMERGE destkey sourcekey [sourcekey ...] 将多个 HyperLogLog 合并为一个 HyperLogLog
127.0.0.1:6379> pfadd key a b c d
1
127.0.0.1:6379> pfcount key
4
127.0.0.1:6379> pfadd key a x y z i
1
127.0.0.1:6379> pfcount key
8
127.0.0.1:6379> pfadd key2 x y z h i j k l
1
127.0.0.1:6379> pfcount key2
8
127.0.0.1:6379> pfmerge mykey key key2
OK
127.0.0.1:6379> pfcount mykey
12
会有0.81%的错误率,如果允许容错,就用这个,如果不行就set或其他的
位存储
统计用户信息,活跃,不活跃。登录、未登录
BitMaps 位图,数据结构,操作二进制进行记录,只要0 1 两个状态。
使用bitmap来记录,周一到周日的打卡
127.0.0.1:6379> setbit sign 0 0
0
127.0.0.1:6379> setbit sign 1 0
0
127.0.0.1:6379> setbit sign 2 0
0
127.0.0.1:6379> setbit sign 3 1
0
127.0.0.1:6379> setbit sign 4 1
0
127.0.0.1:6379> setbit sign 5 0
0
127.0.0.1:6379> setbit sign 6 1
0
查看某一天是否打卡
127.0.0.1:6379> getbit sign 3
1
127.0.0.1:6379> getbit sign 5
0
统计操作,统计打卡的天数
bitcount key [start end]
127.0.0.1:6379> bitcount sign
3
Redis 事务的本质是一组命令的集合。事务支持一次执行多个命令,一个事务中所有命令都会被序列化。在事务执行过程,会按照顺序串行化执行队列中的命令,其他客户端提交的命令请求不会插入到事务执行命令序列中。
总结说:redis事务就是一次性、顺序性、排他性的执行一个队列中的一系列命令。
Redis事务没有隔离级别的概念:
批量操作在发送 EXEC 命令前被放入队列缓存,并不会被实际执行,也就不存在事务内的查询要看到事务里的更新,事务外查询不能看到。
Redis不保证原子性:
Redis中,单条命令是原子性执行的,但事务不保证原子性,且没有回滚。事务中任意命令执行失败,其余的命令仍会被执行。
Redis事务的三个阶段:
开始事务 (MULTI)
命令入队
执行事务 (EXEC)
取消事务 (DISCARD) 放弃事务块中的所有命令
命令:MULTI/EXEC/DISCARD
#执行事务
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> get k2
QUEUED
127.0.0.1:6379> set k3 v3
QUEUED
127.0.0.1:6379> exec
1) OK
2) OK
3) "v2"
4) OK
#放弃事务
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 k4 v4
QUEUED
127.0.0.1:6379> discard #取消事务
OK
127.0.0.1:6379> get k4 #事务队列都没有执行
(nil)
#若在事务队列中存在命令性错误(类似于java编译性错误),则执行EXEC命令时,所有命令都不会执行
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 k4 v4
QUEUED
127.0.0.1:6379> getset k3
(error) ERR wrong number of arguments for 'getset' command
127.0.0.1:6379> set k3 v3
QUEUED
127.0.0.1:6379> set k5 v5
QUEUED
127.0.0.1:6379> exec
(error) EXECABORT Transaction discarded because of previous errors.
#若在事务队列中存在语法性错误(类似于java的1/0的运行时异常),则执行EXEC命令时,其他正确命令会被执行,错误命令抛出异常。
127.0.0.1:6379> set k1 v1
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> incr k1 #会执行的时候失败
QUEUED
127.0.0.1:6379> set k2 v2
QUEUED
127.0.0.1:6379> set k3 v3
QUEUED
127.0.0.1:6379> get k3
QUEUED
127.0.0.1:6379> 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 k2
"v2"
监控 watch
悲观锁:
认为什么时候都出问题 无论做什么都加锁 会影响性能
乐观锁:
#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> decrby money 20
QUEUED
127.0.0.1:6379> incrby out 20
QUEUED
127.0.0.1:6379> exec #若是期间值被改动,则无法执行成功,exec会自动解锁
1) (integer) 80
2) (integer) 20
#如果修改后 还想让事务执行 可以再unwatch 再watch
测试多线程修改至,使用watch可以当做redis的乐观锁操作
Jedis是Redis官方推荐的Java连接开发工具,使用java操作Redis中间件
测试
1、导入依赖
<dependency>
<groupId>redis.clientsgroupId>
<artifactId>jedisartifactId>
<version>3.2.0version>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>fastjsonartifactId>
<version>1.2.70version>
dependency>
2、编码测试
redis 提供 6种数据淘汰策略:
volatile-lru:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰
volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰
volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰
allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key(这个是最常用的).
allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
no-eviction:禁止驱逐数据,也就是说当内存不足以容纳新写入数据时,新写入操作会报错。这个应该没人使用吧!
在指定的时间间隔内将内存中的数据集快照写入磁盘,也就是行话讲的Snapshot快照,它恢复时是将快照文件直接读到内存里
Redis会单独创建(fork)一个子进程来进行持久化,会先将数据写入到 一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。 整个过程中,主进程是不进行任何IO操作的,这就确保了极高的性能。如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那RDB方式要比AOF方式更加的高效。RDB的缺点是最后一次持久化后的数据可能丢失。
Fork的作用是复制一个与当前进程一样的进程。新进程的所有数据(变量、环境变量、程序计数器等) 数值都和原进程一致,但是是一个全新的进程,并作为原进程的子进程
如何触发RDB快照
配置文件中默认的快照配置
dbfilename dump.rdb
命令save或者是bgsave
执行flushall命令,也会产生dump.rdb文件,但里面是空的,无意义
退出redis,也会产生rdb文件
如何恢复
CONFIG GET dir
获取目录优势与劣势
RDB持久化配置
Redis会将数据集的快照dump到dump.rdb文件中。此外,我们也可以通过配置文件来修改Redis服务器dump快照的频率,在打开6379.conf文件之后,我们搜索save,可以看到下面的配置信息:
save 900 1 #在900秒(15分钟)之后,如果至少有1个key发生变化,则dump内存快照。
save 300 10 #在300秒(5分钟)之后,如果至少有10个key发生变化,则dump内存快照。
save 60 10000 #在60秒(1分钟)之后,如果至少有10000个key发生变化,则dump内存快照
概念
以日志的形式来记录每个写操作,将Redis执行过的所有写指令记录下来(读操作不记录), 只许追加文件但不可以改写文件,redis启动之初会读取该文件重新构建数据,换言之,redis 重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作
append
默认redis.conf 是不开启的,需要手动配置 config set appendonly “yes”
重启redis就可以 appendonly.aof 就生效了
AOF启动/修复/恢复
优势与劣势
如果aof文件大于64m,太大,就fork一个新的进程将文件进行重写
在同时使用了AOF和RDB方式的情况下,Redis重启后会优先使用AOF文件来重构原始数据集。
RDB 和 AOF ,我应该用哪一个?
一般来说,如果想达到足以媲美 PostgreSQL 的数据安全性, 你应该同时使用两种持久化功能。
如果你非常关心你的数据,但仍然可以承受数分钟以内的数据丢失, 那么你可以只使用 RDB 持久化。
有很多用户单独使用AOF,但是我们并不鼓励这样,因为时常进行RDB快照非常方便于数据库备份,启动速度也较之快,还避免了AOF引擎的bug。
进程间的一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接收消息。
下图展示了频道 channel1 , 以及订阅这个频道的三个客户端 —— client2 、 client5 和 client1 之间的关系:
当有新消息通过 PUBLISH 命令发送给频道 channel1 时, 这个消息就会被发送给订阅它的三个客户端:
命令 | 描述 |
---|---|
PSUBSCRIBE pattern [pattern …] | 订阅一个或多个符合给定模式的频道。 |
PUBSUB subcommand [argument [argument …]] | 查看订阅与发布系统状态。 |
PUBLISH channel message | 将信息发送到指定的频道。 |
PUNSUBSCRIBE [pattern [pattern …]] | 退订所有给定模式的频道。 |
SUBSCRIBE channel [channel …] | 订阅给定的一个或多个频道的信息。 |
UNSUBSCRIBE [channel [channel …]] | 指退订给定的频道。 |
127.0.0.1:6379> SUBSCRIBE chat
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "chat"
3) (integer) 1
1) "message"
2) "chat"
3) "hello,hidisan"
127.0.0.1:6379> PUBLISH chat hello,hidisan
(integer) 1
主从复制,是指将一台Redis服务器的数据,复制到其他的Redis服务器。前者称为主节点(master),后者称为从节点(slave);数据的复制是单向的,只能由主节点到从节点。master以写为主,slave以读为主。
默认情况下,每台Redis服务器都是主节点;且一个主节点可以有多个从节点(或没有从节点),但一个从节点只能有一个主节点。
主从复制的作用
主从复制的作用主要包括:
主从复制,读写分离,80都在读操作,可以减缓服务器的压力。
环境配置
127.0.0.1:6379> info replication #查看当前库的信息
# Replication
role:master #角色
connected_slaves:0 #没有从机
master_replid:dfb2fe86381f01f979d8b41e36364f26a3b1c42e
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
修改配置文件细节操作
一主二从
默认情况下,每台Redis服务器都是主节点
一般情况下,只用配置从机就行 一主(79)二从(80 81)
127.0.0.1:6380> SLAVEOF 127.0.0.1 6379 #slaveof host port 找老大
OK
127.0.0.1:6380> info replication
# Replication
role:slave #当前角色是从机
master_host:127.0.0.1
master_port:6379
master_link_status:up
master_last_io_seconds_ago:9
master_sync_in_progress:0
slave_repl_offset:0
slave_priority:100
slave_read_only:1
connected_slaves:0
master_replid:3786546e4af845dbd2bb431a1c422c9a20c88653
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:0
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:0
#在主机查看
127.0.0.1:6379> info replication
# Replication
role:master
connected_slaves:1 #多了从机配置
slave0:ip=127.0.0.1,port=6380,state=online,offset=56,lag=0
master_replid:3786546e4af845dbd2bb431a1c422c9a20c88653
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
主从问题演示
#### REPLICATION ####
)只要变为从机,就能立马获取值。
全量复制
全量同步
Redis全量复制一般发生在Slave初始化阶段,这时Slave需要将Master上的所有数据都复制一份。具体步骤如下:
- 从服务器连接主服务器,发送SYNC命令;
- 主服务器接收到SYNC命名后,开始执行BGSAVE命令生成RDB文件并使用缓冲区记录此后执行的所有写命令;
- 主服务器BGSAVE执行完后,向所有从服务器发送快照文件,并在发送期间继续记录被执行的写命令;
- 从服务器收到快照文件后丢弃所有旧数据,载入收到的快照;
- 主服务器快照发送完毕后开始向从服务器发送缓冲区中的写命令;
- 从服务器完成对快照的载入,开始接收命令请求,并执行来自主服务器缓冲区的写命令;
增量同步
Redis增量复制是指Slave初始化后开始正常工作时主服务器发生的写操作同步到从服务器的过程。
增量复制的过程主要是主服务器每执行一个写命令就会向从服务器发送相同的写命令,从服务器接收并执行收到的写命令。
只要重新连接主机Master,就会执行一次全量同步
主从切换技术的方法是:当主服务器宕机后,需要手动把一台从服务器切换为主服务器,这就需要人工干预,费事费力,还会造成一段时间内服务不可用。这不是一种推荐的方式,更多时候,我们优先考虑哨兵模式。
哨兵模式是一种特殊的模式,首先Redis提供了哨兵的命令,哨兵是一个独立的进程,作为进程,它会独立运行。其原理是哨兵通过发送命令,等待Redis服务器响应,从而监控运行的多个Redis实例。
假设主服务器宕机,哨兵1先检测到这个结果,系统并不会马上进行failover过程,仅仅是哨兵1主观的认为主服务器不可用,这个现象成为主观下线。当后面的哨兵也检测到主服务器不可用,并且数量达到一定值时,那么哨兵之间就会进行一次投票,投票的结果由一个哨兵发起,进行failover操作。切换成功后,就会通过发布订阅模式,让各个哨兵把自己监控的从服务器实现切换主机,这个过程称为客观下线。这样对于客户端而言,一切都是透明的。
## 配置监听的主服务器,这里sentinel monitor代表监控,myredis代表服务器的名称,可以自定义,127.0.0.1代表监控的主服务器,6379代表端口,2代表只有两个或两个以上的哨兵认为主服务器不可用的时候,才会进行failover操作。
sentinel monitor myredis 127.0.0.1 6379 2
sentinel monitor myredis 127.0.0.1 6379 1
#启动
./redis-sentinel kconfig/sentinel.conf
问题:如果之前挂了的master重启回来,会不会双master冲突?
答: 不会,原master,变成slave
哨兵模式
优点:
1、哨兵集群,基于主从复制,所有主从配置优点,都有
2、主从可以切换,故障可以转移,系统的可用性就会更好
3、哨兵模式就是主从模式的生机,手动到自动,更健壮
缺点:
1、Redis不好在线扩容,集群容量一旦到达上限,在线扩容就十分麻烦
2、实现哨兵模式的配置比较麻烦
#在sentinel.conf 配置文件中, 我们可以找到port 属性,这里是用来设置sentinel 的端口,一般情况下,至少会需要三个哨兵对redis 进行监控,我们可以通过修改端口启动多个sentinel 服务。
port 26379
概念
缓存穿透的概念很简单,用户想要查询一个数据,发现Redis内存数据库没有,也就是缓存没有命中,于是向持久层数据库查询。发现也没有,于是本次查询失败。当用户很多的时候,缓存都没有命中,于是都去请求了持久层数据库。这会给持久层数据库造成很大的压力,这时候就相当于出现了缓存穿透。
解决方案
1.布隆过滤器
布隆过滤器是一种数据结构,垃圾网站和正常网站加起来全世界据统计也有几十亿个。网警要过滤这些垃圾网站,总不能到数据库里面一个一个去比较吧,这就可以使用布隆过滤器。
布隆过滤器(Bloom Filter)的核心实现是一个超大的位数组和几个哈希函数。假设位数组的长度为m,哈希函数的个数为k
假设集合里面有3个元素{x, y, z},哈希函数的个数为3。首先将位数组进行初始化,将里面每个位都设置位0。对于集合里面的每一个元素,将元素依次通过3个哈希函数进行映射,每次映射都会产生一个哈希值,这个值对应位数组上面的一个点,然后将位数组对应的位置标记为1。查询W元素是否存在集合中的时候,同样的方法将W通过哈希映射到位数组上的3个点。如果3个点的其中有一个点不为1,则可以判断该元素一定不存在集合中。反之,如果3个点都为1,则该元素可能存在集合中。注意:此处不能判断该元素是否一定存在集合中,可能存在一定的误判率。可以从图中可以看到:假设某个元素通过映射对应下标为4,5,6这3个点。虽然这3个点都为1,但是很明显这3个点是不同元素经过哈希得到的位置,因此这种情况说明元素虽然不在集合中,也可能对应的都是1,这是误判率存在的原因。
布隆过滤器添加元素
布隆过滤器查询元素
2.缓存空对象
当存储层不命中后,即使返回的空对象也将其缓存起来,同时会设置一个过期时间(它的过期时间会很短,最长不超过五分钟),之后再访问这个数据将会从缓存中获取,保护了后端数据源;
缓存击穿:Redis中一个热点key在失效的同时,大量的请求过来,从而会全部到达数据库,压垮数据库。
当某个key在过期的瞬间,有大量请求并发访问,这类数据一般是热点数据,由于缓存过期,会同时访问数据库来查询最新数据,并且会写缓存,导致数据库瞬间压力过大
解决方法
①、设置热点数据永不过期
对于某个需要频繁获取的信息,缓存在Redis中,并设置其永不过期。当然这种方式比较粗暴,对于某些业务场景是不适合的。
②、定时更新
比如这个热点数据的过期时间是1h,那么每到59minutes时,通过定时任务去更新这个热点key,并重新设置其过期时间。
③**、互斥锁**
这是解决缓存击穿比较常用的方法。
互斥锁简单来说就是在Redis中根据key获得的value值为空时,先锁上,然后从数据库加载,加载完毕,释放锁。若其他线程也在请求该key时,发现获取锁失败,则睡眠一段时间(比如100ms)后重试。
**2、接口限流与熔断,降级。**重要的接口一定要做好限流策略,防止用户恶意刷接口,同时要降级准备,当接口中的某些 服务 不可用时候,进行熔断,失败快速返回机制。
缓存雪崩,是指在某一个时间段,缓存集中过期失效。
简单的理解为:由于原有缓存失效,新缓存未到期间(例如:我们设置缓存时采用了相同的过期时间,在同一时刻出现大面积的缓存过期),所有原本应该访问缓存的请求都去查询数据库了,而对数据库CPU和内存造成巨大压力,严重的会造成数据库宕机。从而形成一系列连锁反应,造成整个系统崩溃。
产生雪崩的原因之一,比如在写本文的时候,马上就要到双十二零点,很快就会迎来一波抢购,这波商品时间比较集中的放入了缓存,假设缓存一个小时。那么到了凌晨一点钟的时候,这批商品的缓存就都过期了。而对这批商品的访问查询,都落到了数据库上,对于数据库而言,就会产生周期性的压力波峰。
缓存正常从Redis中获取
缓存失效瞬间示意图
解决思路:
第一,大多数考虑用加锁或者队列的方式保证来保证不会有大量的线程对数据库一次性进行读写,避免缓存失效时对数据库造成太大的压力,虽然能够在一定的程度上缓解了数据库的压力但是与此同时又降低了系统的吞吐量。
第二,分析用户的行为,尽量让缓存失效的时间均匀分布。
第三,如果是因为某台缓存服务器宕机,可以考虑做主备,比如:redis主备,但是双缓存涉及到更新事务的问题,update可能读到脏数据,需要好好解决。
其实集中过期,倒不是非常致命,比较致命的缓存雪崩,是缓存服务器某个节点宕机或断网。因为自然形成的缓存雪崩,一定是在某个时间段集中创建缓存,那么那个时候数据库能顶住压力,这个时候,数据库也是可以顶住压力的。无非就是对数据库产生周期性的压力而已。但是缓存服务节点的宕机,对数据库服务器造成的压力是不可预知的,很有可能瞬间就把数据库压垮。
解决办法(中华石杉老师在他的视频中提到过,视频地址在最后一个问题中有提到):
事前:尽量保证整个 redis 集群的高可用性,发现机器宕机尽快补上。选择合适的内存淘汰策略。
事中:本地ehcache缓存 + hystrix限流&降级,避免MySQL崩掉
事后:利用 redis 持久化机制保存的数据尽快恢复缓存
(1)redis高可用
这个思想的含义是,既然redis有可能挂掉,那我多增设几台redis,这样一台挂掉之后其他的还可以继续工作,其实就是搭建的集群。
(2)限流降级
这个解决方案的思想是,在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。
(3)数据预热
数据加热的含义就是在正式部署之前,我先把可能的数据先预先访问一遍,这样部分可能大量访问的数据就会加载到缓存中。在即将发生大并发访问前手动触发加载缓存不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀。
所谓 Redis 的并发竞争 Key 的问题也就是多个系统同时对一个 key 进行操作,但是最后执行的顺序和我们期望的顺序不同,这样也就导致了结果的不同!
推荐一种方案:分布式锁(zookeeper 和 redis 都可以实现分布式锁)。(如果不存在 Redis 的并发竞争 Key 问
题,不要使用分布式锁,这样会影响性能)
基于zookeeper临时有序节点可以实现的分布式锁。大致思想为:每个客户端对某个方法加锁时,在zookeeper上的与该方法对应的指定节点的目录下,生成一个唯一的瞬时有序节点。 判断是否获取锁的方式很简单,只需要判断有序节点中序号最小的一个。 当释放锁的时候,只需将这个瞬时节点删除即可。同时,其可以避免服务宕机导致的锁无法释放,而产生的死锁问题。完成业务流程后,删除对应的子节点释放锁。
在实践中,当然是从以可靠性为主。所以首推Zookeeper。