Redis
NoSql
原始瓶颈:1.数据太大,一个机器放不下 2.数据索引B+Tree 3.访问量(读写混合)
发展过程:优化数据结构和索引 –> 文件缓存(IO) –> Memcached
如今的互联网企业:
为什么要用NoSQL
用户的个人信息,社交网络,地理位置。用户自己产生的数据爆发式增长!
什么是NoSQL
NoSQL = Not Only SQL,泛指非关系型数据库,随着web2.0互联网的诞生!传统的关系型数据库很难对付web2.0时代!尤其是超大规模的高并发的社区!很多数据类型不需要一个固定格式和多的操作。
NoSQL特点
- 方便扩展(数据之间没有联系,好扩展!)
- 大数据量高性能
- 数据类型是多样的!(不需要实现设计数据库)
- 不仅仅是数据,没有固定的查询语言,键值对存储,最终一致性,高可用可扩展
阿里发展史
# 商品信息
- 一般存放在关系型数据库:Mysql,阿里巴巴使用的Mysql都是经过内部改动的。
# 商品描述、评论(文字居多)
- 文档型数据库:MongoDB
# 图片
- 分布式文件系统 FastDFS
- 淘宝:TFS
- Google: GFS
- Hadoop: HDFS
- 阿里云: oss
# 商品关键字 用于搜索
- 搜索引擎:solr,elasticsearch
- 阿里:Isearch 多隆
# 商品热门的波段信息
- 内存数据库:Redis,Memcache
# 商品交易,外部支付接口
- 第三方应用
Redis概述
Redis是什么?
Redis(Remote Dictionary Server ),即远程字典服务。是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。与memcached一样,为了保证效率,数据都是缓存在内存中。区别的是redis会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件,并且在此基础上实现了master-slave(主从)同步。
Redis能该干什么?
- 内存存储、持久化,内存是断电即失的,所以需要持久化(RDB、AOF)
- 高效率、用于高速缓冲
- 发布订阅系统
- 地图信息分析
- 计时器、计数器(eg:浏览量)
Redis基本知识
有16个数据库,默认使用第一个。Redis是基于内存操作,cpu不是redis瓶颈。
redis-cli进行交互默认为6379端口
shutdown关闭redis
redis-benchmark可以用于测试性能
dbsize #查看数据库大小
select [id] #选择第id个数据库
keys * #查看所有的key
flushdb #清空当前数据库
Redis为什么单线程还这么快
误区1:高性能的服务器一定是多线程的?
误区2:多线程(CPU上下文会切换!)一定比单线程效率高!
核心:Redis是将所有的数据放在内存中的,所以说使用单线程去操作效率就是最高的,多线程(CPU上下文会切换:耗时的操作!),对于内存系统来说,如果没有上下文切换效率就是最高的,多次读写都是在一个CPU上的,在内存存储数据情况下,单线程就是最佳的方案。
Redis-Key
Redis是一个开源(BSD许可),内存存储的数据结构服务器,可用作数据库,高速缓存和消息队列代理。它支持字符串、哈希表、列表、集合、有序集合,位图,hyperloglogs等数据类型。内置复制、Lua脚本、LRU收回、事务以及不同级别磁盘持久化功能,同时通过Redis Sentinel提供高可用,通过Redis Cluster提供自动分区。
set name xxx # set key
get name # get key
move name 1 # 将键值对移动到指定数据库
EXISTS name # 判断键是否存在
EXPIRE name [seconds] # 设置键值对的过期时间
ttl age # 查看key的过期剩余时间,-1为没有设置,-2为过期
String
常用场景:value除了字符也可以是数字,可以统计单位数量。
append key "string" # 在key后面追加
strlen key # 查看长度
加减:
incr key # 加1
incrby key num #加num
decr key # 减1
decrby key num #减num
字符串范围:
getrange key start end # 截取字符串
setrange key offset value # 从key1[offset]开始替换为value
set设置:
setex key seconds "string" # 设置时间
setnx key "string" # 设置如果key不存在
mset key1 value1 ... # 批量设置
mget key1 value1 ... # 批量获得
设置对象:
set user:1 {name:zhangsan,age:3} # 设置一个user:1对象,值用json实现
mset user:1:name test user:1:age 18 # 设置user:{id}:{filed}
List
list命令都是L开头的。
LPUSH key values # 向左推入值到list中
RPUSH key values # 向右推入值到list中
lrange key start end # 枚举元素
lpop list # 向左pop一个值
rpop list # 向右pop一个值
lindex list index # 获取list[index]
llen key # 获取list长度
lrem key count value # 移除count个value
ltrim mylist start end # 通过下标截取长度
lset mylist index value # 将指定下标替换为另一个值
Set
命令以s开头,应用场景如:微博共同关注,粉丝
sadd key values # 在key中添加元素
smembers key # 查看元素
sismember key value # 查看key中是否右value
scard key # 查看key大小
srem key value # 移除set集合中的值
srandmember key num # 随机抽取num个元素
sdiff key1 key2 # 查看差集
sunion key1 key2 # 查看并集
Hash
Map集合 ,key-map!这时候是一个map集合!命令H开头,类似于string;hash更适合存储对象
hset key key1 value1 # 设置键值对
hget key key1 # 获得值
hgetall key # 获得所有值
hdel key key1 # 删除key1键值对
hkeys key # 获取所有的key
hvals key # 获取所有vals
hincreby key num # 对key的val增加num
Zset
命令以z开头,是一个有序的列表,对应相应的key,应用场景:排行榜
zadd key score value # 添加一个值为score的value到key中
zrange key start end # 对key进行输出
ZRANGEBYSCORE key min max # 对key进行排序区间在min,max之间
zrem key value # 在key中删除value
zcrad key # 获取有序集合中的个数
zreverse key 0 -1 # 反转输出
zcount key start end # 获取指定区间的成员数量
geospatial
地理位置,朋友的定位,附近的人,打车距离计算
# geoadd key value(经纬纬度城市),添加地理位置,直接通过java导入
geoadd china:city 121.47 31.23 shanghai
# geopos key city(城市),获取地理位置
geopos key city
# geodist key city1 city2 [m,km],查看直线距离
geodist key city1 city2
# 获取某一半径内的城市总共num个,georadius key pos dist [with...] [count num]
GEORADIUS china:city 110 30 1000 km withdist count 3
# 通过元素查询附件城市
GEORADIUSBYMEMBER china:city beijin 1000 km
# 底层是zset,所以可以通过zrem移除,zrange查看等
Hyperloglog
网页的访问量(UV):一个用户多次访问,也只能算作一个人。
传统实现,存储用户的id,然后每次进行比较。当用户变多之后这种方式及其浪费空间,而我们的目的只是计数,Hyperloglog就能帮助我们利用最小的空间完成。
PFADD key element1 [elememt2..] # 添加指定元素到 HyperLogLog 中
PFCOUNT key [key] # 返回给定 HyperLogLog 的基数估算值。
PFMERGE destkey sourcekey [sourcekey..] # 将多个 HyperLogLog 合并为一个 HyperLogLog
Bitmap
使用位存储,信息状态只有 0 和 1,Bitmap是一串连续的2进制数字(0或1),每一位所在的位置为偏移(offset),在bitmap上可执行AND,OR,XOR,NOT以及其它位操作。
setbit key offset value #n为指定key的offset位设置值
getbit key offset # 获取offset位的值
bitcount key [start end] # 统计字符串被设置为1的bit数,也可以指定统计范围按字节
BITPOS key bit [start] [end] # 返回字符串里面第一个被设置为1或者0的bit位。start和end只能按字节,不能按位
Redis事物
redis事物本质:一组命令的集合!redis事物单条具有一致性,但是多条不具有。事务中每条命令都会被序列化,执行过程中按顺序执行,不允许其他命令进行干扰。
multi # 开启事务
set k2 v2
..... # 一系列事物入队
exec # 事务执行
DISCARD # 放弃事务
乐观锁
- redis乐观锁认为什么时候都不会出现问题,所以不会上锁!更新数据的时候去判断一下,在此期间是否有人修改过这个数据
- 获取version
- 更新的时候比较version
使用watch key
监控指定数据,相当于乐观锁加锁。
在watch key以后,会对key进行识别,如果其中有别的线程对key进行操作,那么事物会失败。
Jedis
导入java控制redis的依赖包
<dependencies>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.2.0</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>2.0.21</version>
</dependency>
</dependencies>
通过java控制redis
public static void main(String[] args) {
//创建对象
Jedis jedis = new Jedis("127.0.0.1",6379);
jedis.auth("foobared");
jedis.flushDB();
JSONObject jsonObject = new JSONObject();
jsonObject.put("hello","world");
jsonObject.put("name","dydong");
String str = jsonObject.toJSONString();
//开启事物
Transaction multi = jedis.multi();
try{
multi.set("user1",str);
multi.set("user2",str);
multi.exec();
} catch (Exception e) {
multi.discard();
} finally {
System.out.println(jedis.get("user1"));
System.out.println(jedis.get("user2"));
jedis.close();
}
}
Springboot整合
导入依赖包
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
springboot 2.x后 ,原来使用的 Jedis 被 lettuce 替换。
jedis:采用的直连,多个线程操作的话,是不安全的。如果要避免不安全,使用jedis pool连接池!更像BIO模式
lettuce:采用netty,实例可以在多个线程中共享,不存在线程不安全的情况!可以减少线程数据了,更像NIO模式
编写配置文件
# 配置redis
spring.redis.host=39.99.xxx.xx
spring.redis.port=6379
利用序列化的方式存储数据:
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
// 将template 泛型设置为 <String, Object>
RedisTemplate<String, Object> template = new RedisTemplate();
// 连接工厂,不必修改
template.setConnectionFactory(redisConnectionFactory);
/*
* 序列化设置
*/
// key、hash的key 采用 String序列化方式
template.setKeySerializer(RedisSerializer.string());
template.setHashKeySerializer(RedisSerializer.string());
// value、hash的value 采用 Jackson 序列化方式
template.setValueSerializer(RedisSerializer.json());
template.setHashValueSerializer(RedisSerializer.json());
template.afterPropertiesSet();
return template;
}
}
使用RedisTemplate需要频繁调用.opForxxx然后才能进行对应的操作,这样使用起来代码效率低下,工作中一般不会这样使用,而是将这些常用的公共API抽取出来封装成为一个工具类,然后直接使用工具类来间接操作Redis,不但效率高并且易用。工具类参考博客:
https://www.cnblogs.com/zeng1994/p/03303c805731afc9aa9c60dbbd32a323.html
https://www.cnblogs.com/zhzhlong/p/11434284.html
RDB(redis databse)
在指定时间间隔后,将内存中的数据集快照写入数据库 ;在恢复时候,直接读取快照文件,进行数据的恢复 ;默认情况下, Redis 将数据库快照保存在名字为 dump.rdb的二进制文件中。文件名可以在配置文件中进行自定义。
配置文件默认设置:
save 900 1 # 在900s内操作一次
save 300 10 # 在300s内操作10次
save 60 10000 # 在60s内操作10000次
触发机制:
- save的规则满足的情况下,会自动触发rdb原则
- 执行flushall命令,也会触发我们的rdb原则
- 退出redis,也会自动产生rdb文件
优缺点:
优点:
- 适合大规模的数据恢复
- 对数据的完整性要求不高
缺点:
- 需要一定的时间间隔进行操作,如果redis意外宕机了,这个最后一次修改的数据就没有了。
- fork进程的时候,会占用一定的内容空间。
AOF(Append Only File)
将我们所有的命令都记录下来,history,恢复的时候就把这个文件全部再执行一遍。
修改配置文件,默认是不开启的:
appendonly yes # 默认是不开启aof模式的,默认是使用rdb方式持久化的,在大部分的情况下,rdb完全够用
appendfilename "appendonly.aof"
# appendfsync always # 每次修改都会sync 消耗性能
appendfsync everysec # 每秒执行一次 sync 可能会丢失这一秒的数据
# appendfsync no # 不执行 sync ,这时候操作系统自己同步数据,速度最快
如果这个aof文件有错位,这时候redis是启动不起来的,我需要修改这个aof文件
redis给我们提供了一个工具redis-check-aof --fix
优点
- 每一次修改都会同步,文件的完整性会更加好
- 没秒同步一次,可能会丢失一秒的数据
- 从不同步,效率最高
缺点
- 相对于数据文件来说,aof远远大于rdb,修复速度比rdb慢!
- Aof运行效率也要比rdb慢,所以我们redis默认的配置就是rdb持久化
Redis订阅发布
Redis 发布订阅(pub/sub)是一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接收消息。下图展示了频道 channel1 , 以及订阅这个频道的三个客户端 —— client2 、 client5 和 client1 之间的关系:
------------订阅端----------------------
127.0.0.1:6379> SUBSCRIBE sakura # 订阅sakura频道
Reading messages... (press Ctrl-C to quit) # 等待接收消息
1) "subscribe" # 订阅成功的消息
2) "sakura"
3) (integer) 1
1) "message" # 接收到来自sakura频道的消息 "hello world"
2) "sakura"
3) "hello world"
1) "message" # 接收到来自sakura频道的消息 "hello i am sakura"
2) "sakura"
3) "hello i am sakura"
--------------消息发布端-------------------
127.0.0.1:6379> PUBLISH sakura "hello world" # 发布消息到sakura频道
(integer) 1
127.0.0.1:6379> PUBLISH sakura "hello i am sakura" # 发布消息
(integer) 1
-----------------查看活跃的频道------------
127.0.0.1:6379> PUBSUB channels
1) "sakura"
原理
每个 Redis 服务器进程都维持着一个表示服务器状态的 redis.h/redisServer 结构, 结构的 pubsub_channels 属性是一个字典, 这个字典就用于保存订阅频道的信息,其中,字典的键为正在被订阅的频道, 而字典的值则是一个链表, 链表中保存了所有订阅这个频道的客户端。
应用
- 消息订阅:公众号订阅,微博关注等等(起始更多是使用消息队列来进行实现)
- 多人在线聊天室。
主从复制
概念
主从复制,是指将一台Redis服务器的数据,复制到其他的Redis服务器。前者称为主节点(Master/Leader),后者称为从节点(Slave/Follower), 数据的复制是单向的!只能由主节点复制到从节点(主节点以写为主、从节点以读为主)。默认情况下,每台Redis服务器都是主节点,一个主节点可以有0个或者多个从节点,但每个从节点只能由一个主节点。
127.0.0.1:6379> info replication
# Replication
role:master # 当前默认为主机
connected_slaves:0 # 有0个从机
master_failover_state:no-failover
master_replid:57136c0291407cbf1d09bd9270dd5c1b5be4fb2d
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
配置文件
logfile "/www/server/redis/redis.log" # 修改redis的日志名
dbfilename dump.rdb # 修改rdb文件
port 6379 # 修改端口号
pidfile /www/server/redis/redis.pid # 改成端口对应的pid
一主二从
默认情况下,每台redis服务器都是主节点,我们一般情况下只用配置从机即可。也在配置文件中修改主从。
slaveof host port
info replication
使用规则
从机只能读,不能写,主机可读可写但是多用于写。当主机断电宕机后,默认情况下从机的角色不会发生变化 ,集群中只是失去了写操作,当主机恢复以后,又会连接上从机恢复原状。第二条中提到,默认情况下,主机故障后,不会出现新的主机,有两种方式可以产生新的主机:从机手动执行命令slaveof no one,这样执行以后从机会独立出来成为一个主机使用哨兵模式(自动选举)如果没有老大了,这个时候能不能选择出来一个老大呢?手动!如果主机断开了连接,我们可以使用SLAVEOF no one让自己变成主机!其他的节点就可以手动连接到最新的主节点(手动)!如果这个时候老大修复了,那么重新连接!
哨兵模式
主从切换技术的方法是:当主服务器宕机后,需要手动把一台从服务器切换为主服务器,这就需要人工干预,费事费力,还会造成一段时间内服务不可用。这不是一种推荐的方式,更多时候,我们优先考虑哨兵模式。
哨兵的作用:
- 通过发送命令,让Redis服务器返回监控其运行状态,包括主服务器和从服务器。
- 当哨兵监测到master宕机,会自动将slave切换成master,然后通过发布订阅模式通知其他的从服务器,修改配置文件,让它们切换主机。
测试
redis-sentinel xxx/sentinel.conf
# Example sentinel.conf
# 哨兵sentinel实例运行的端口 默认26379
port 26379
# 哨兵sentinel的工作目录
dir /tmp
# 哨兵sentinel监控的redis主节点的 ip port
# master-name 可以自己命名的主节点名字 只能由字母A-z、数字0-9 、这三个字符".-_"组成。
# quorum 当这些quorum个数sentinel哨兵认为master主节点失联 那么这时 客观上认为主节点失联了
# sentinel monitor <master-name> <ip> <redis-port> <quorum>
sentinel monitor mymaster 127.0.0.1 6379 1
# 当在Redis实例中开启了requirepass foobared 授权密码 这样所有连接Redis实例的客户端都要提供密码
# 设置哨兵sentinel 连接主从的密码 注意必须为主从设置一样的验证密码
# sentinel auth-pass <master-name> <password>
sentinel auth-pass mymaster MySUPER--secret-0123passw0rd
# 指定多少毫秒之后 主节点没有应答哨兵sentinel 此时 哨兵主观上认为主节点下线 默认30秒
# sentinel down-after-milliseconds <master-name> <milliseconds>
sentinel down-after-milliseconds mymaster 30000
# 这个配置项指定了在发生failover主备切换时最多可以有多少个slave同时对新的master进行 同步,
这个数字越小,完成failover所需的时间就越长,
但是如果这个数字越大,就意味着越 多的slave因为replication而不可用。
可以通过将这个值设为 1 来保证每次只有一个slave 处于不能处理命令请求的状态。
# sentinel parallel-syncs <master-name> <numslaves>
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 <master-name> <milliseconds>
sentinel failover-timeout mymaster 180000
# SCRIPTS EXECUTION
#配置当某一事件发生时所需要执行的脚本,可以通过脚本来通知管理员,例如当系统运行不正常时发邮件通知相关人员。
#对于脚本的运行结果有以下规则:
#若脚本执行后返回1,那么该脚本稍后将会被再次执行,重复次数目前默认为10
#若脚本执行后返回2,或者比2更高的一个返回值,脚本将不会重复执行。
#如果脚本在执行过程中由于收到系统中断信号被终止了,则同返回值为1时的行为相同。
#一个脚本的最大执行时间为60s,如果超过这个时间,脚本将会被一个SIGKILL信号终止,之后重新执行。
#通知型脚本:当sentinel有任何警告级别的事件发生时(比如说redis实例的主观失效和客观失效等等),将会去调用这个脚本,
#这时这个脚本应该通过邮件,SMS等方式去通知系统管理员关于系统不正常运行的信息。调用该脚本时,将传给脚本两个参数,
#一个是事件的类型,
#一个是事件的描述。
#如果sentinel.conf配置文件中配置了这个脚本路径,那么必须保证这个脚本存在于这个路径,并且是可执行的,否则sentinel无法正常启动成功。
#通知脚本
# sentinel notification-script <master-name> <script-path>
sentinel notification-script mymaster /var/redis/notify.sh
# 客户端重新配置主节点参数脚本
# 当一个master由于failover而发生改变时,这个脚本将会被调用,通知相关的客户端关于master地址已经发生改变的信息。
# 以下参数将会在调用脚本时传给脚本:
# <master-name> <role> <state> <from-ip> <from-port> <to-ip> <to-port>
# 目前<state>总是“failover”,
# <role>是“leader”或者“observer”中的一个。
# 参数 from-ip, from-port, to-ip, to-port是用来和旧的master和新的master(即旧的slave)通信的
# 这个脚本应该是通用的,能被多次调用,不是针对性的。
# sentinel client-reconfig-script <master-name> <script-path>
sentinel client-reconfig-script mymaster /var/redis/reconfig.sh
缓存穿透和雪崩
概念
在默认情况下,用户请求数据时,会先在缓存(Redis)中查找,若没找到即缓存未命中,再在数据库中进行查找,数量少可能问题不大,可是一旦大量的请求数据(例如秒杀场景)缓存都没有命中的话,就会全部转移到数据库上,造成数据库极大的压力,就有可能导致数据库崩溃。网络安全中也有人恶意使用这种手段进行攻击被称为洪水攻击。
解决方案
布隆过滤器:对所有可能查询的参数以Hash的形式存储,以便快速确定是否存在这个值,在控制层先进行拦截校验,校验不通过直接打回,减轻了存储系统的压力。
缓存击穿(量太大,缓存过期)
相较于缓存穿透,缓存击穿的目的性更强,一个存在的key,在缓存过期的一刻,同时有大量的请求,这些请求都会击穿到DB,造成瞬时DB请求量大、压力骤增。这就是缓存被击穿,只是针对其中某个key的缓存不可用而导致击穿,但是其他的key依然可以使用缓存响应。
解决方案
- 设置热点数据永不过期
- 加互斥锁(分布式锁)
缓存雪崩
大量的key设置了相同的过期时间,导致在缓存在同一时刻全部失效,造成瞬时DB请求量大、压力骤增,引起雪崩。
解决方案
1.redis高可用 2.限流降级 3.数据预热