测试性能
redis-benchmark是一个压力测试工具,官方自带的性能测试工具
redis-benchmark加命令参数
简单测试:
# 测试:100个并发连接 100000请求
redis-benchmark -h localhost -p 6379 -c 100 -n 100000
基础知识
redis默认有16个数据库,默认使用第0个数据库
keys *
:查看当前数据库中所有的key。
flushdb
:清空当前数据库中的键值对。
flushall
:清空所有数据库的键值对。
Redis为什么单线程还这么快
误区1: 高性能的服务器一定是多线程的
误区2: 多线程(CPU上下文会切换!)一定比单线程效率高
redis核心: redis是将所有的数据全部放在内存当中,所以说使用单线程去操作效率就是高的,多线程CPU上下文会切换,耗时!对于内存系统来说,如果没有上下文切换的话效率就是最高的,多次读写都是在一个CPU上的,在内存的情况下,这个就是最佳的方案,
五大数据类型
Redis-key :在redis中无论什么数据类型,数据库中都是以key-value的形式保存,通过进行对Redis-key操作来完成对数据库中的数据进行操作
exists key
:判断键是否存在del key
:删除键值对move key db
:将键值对移动到指定数据库expire key second
:设置键值对的过期时间type key
:查看value的数据类型
127.0.0.1:6379> keys * # 查看当前数据库所有key
(empty list or set)
127.0.0.1:6379> set name qinjiang # set key
OK
127.0.0.1:6379> set age 20
OK
127.0.0.1:6379> keys *
1) "age"
2) "name"
127.0.0.1:6379> move age 1 # 将键值对移动到指定数据库
(integer) 1
127.0.0.1:6379> EXISTS age # 判断键是否存在
(integer) 0 # 不存在
127.0.0.1:6379> EXISTS name
(integer) 1 # 存在
127.0.0.1:6379> SELECT 1
OK
127.0.0.1:6379[1]> keys *
1) "age"
127.0.0.1:6379[1]> del age # 删除键值对
(integer) 1 # 删除个数
127.0.0.1:6379> set age 20
OK
127.0.0.1:6379> EXPIRE age 15 # 设置键值对的过期时间
(integer) 1 # 设置成功 开始计数
127.0.0.1:6379> ttl age # 查看key的过期剩余时间
(integer) 13
127.0.0.1:6379> ttl age
(integer) 11
127.0.0.1:6379> ttl age
(integer) 9
127.0.0.1:6379> ttl age
(integer) -2 # -2 表示key过期,-1表示key未设置过期时间
127.0.0.1:6379> get age # 过期的key 会被自动delete
(nil)
127.0.0.1:6379> keys *
1) "name"
127.0.0.1:6379> type name # 查看value的数据类型
string
String(字符串)
String字符串使用场景:value可以使字符串也可以是数字
- 计数器
- 统计多单位数量
- 粉丝数量
- 对象存储缓存
List(列表)
Redis列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边)一个列表最多可以包含 232 - 1 个元素 (4294967295, 每个列表超过40亿个元素)。
正如图Redis中List是可以进行双端操作的,所以命令也就分为了LXXX和RLLL两类,有时候L也表示List例如LLEN
小结
- list实际上是一个链表 before Node after , left, right 都可以插入值
- 如果key不存在,就会创建新的链表
- 如果key存在就新增内容
- 如果移除了所有值,空链表,也就相当不存在
- 在两边插入或者改动值效率最高,修改中间的值效率最低
- 应用:消息队列,消息排队,栈
set集合
Redis的set集合是String类型的无序集合,集合成员是唯一的,这就意味集合中不能出现重复的数据!
Redis 中 集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是O(1)。
集合中最大的成员数为 232 - 1 (4294967295, 每个集合可存储40多亿个成员)。
Hash(哈希)
Redis的hash是一个String类型的field和value映射表 , hash特别适合存储对象
set就是一种简化的hash,只是变动key,而value使用默认值填充,可以将一个Hash表作为一个对象进行存储,表中存放对象的信息。
hset:将hash表的值设为value,重复的设置会覆盖原本的value并返回0
hmset:同事设置多个field-value
hmexists:查看hash表中是否存在指定的field(字段)
hget:获取hash表中指定的field的vlaue
hmget:获取多个hash表中指定的field和value
hgetall:获取hash表中所有的field跟value
hkeys:获取哈希表key中所有的字段
hlen:获取哈希表key中field字段的数量
hvals:获取哈希表key中所有的value
hdel:从哈希表key中移除指定的field和value字段和值
hincrby;对指定的field进行指定的数量增加 非整数字类型的字段不可用
hincrbyfloat:对指定的field进行指定小数点的增加
Hash变更的数据user name age,尤其是用户信息之类的,经常变动的信息,hash表更适合储存对象,string类型更适合储存字符串
Zset(有序集合)
Redis中的Zset有序集合与其他不同的是每个元素都会关联一个double类型的分数(score).redis正式通过分数来为集合中的成员进行从大到小的排序,score相同的话就按字典顺序排序
有序集合的成员是唯一的,但分数score却可以重复
应用案例:
- set排序 存储班级成绩表 工资表排序
- 普通消息,1.重要消息 2. 带权重进行判断
- 排序榜应用实现 取top N测试
Redis事务操作过程
- 开启事务(multi)
- 命令入队
- 执行事务(exec)
所以事务中命令加入时都是没有被执行的,直到提交时才会开始执行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> get k1
QUEUED
127.0.0.1:6379> set k3 v3
QUEUED
127.0.0.1:6379> keys *
QUEUED
127.0.0.1:6379> exec # 事务执行
1) OK
2) OK
3) "v1"
4) OK
5) 1) "k3"
2) "k2"
3) "k1"
取消事务(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> DISCARD # 放弃事务
OK
127.0.0.1:6379> EXEC
(error) ERR EXEC without MULTI # 当前未开启事务
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> set k2 v2
QUEUED
127.0.0.1:6379> error k1 # 这是一条语法错误命令
(error) ERR unknown command `error`, with args beginning with: `k1`, # 会报错但是不影响后续命令入队
127.0.0.1:6379> get k2
QUEUED
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> 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> INCR k1 # 这条命令逻辑错误(对字符串进行增量)
QUEUED
127.0.0.1:6379> get k2
QUEUED
127.0.0.1:6379> exec
1) OK
2) OK
3) (error) ERR value is not an integer or out of range # 运行时报错
4) "v2" # 其他命令正常执行
# 虽然中间有一条命令报错了,但是后面的指令依旧正常执行成功了。
# 所以说Redis单条指令保证原子性,但是Redis事务不能保证原子性。
监控:
- 悲观锁:
- 很悲观,认为什么时候都会出现问题,无论做什么都会加上锁
- 乐观锁
- 很乐观,认为什么时候都不会出现问题,所以不会上锁,更新数据的时候会去判断一下,在此期间是否有人修改过这个数据
- 获取version
- 更新的时候比较version
使用watch key监控指定数据,相当于乐观锁加锁
127.0.0.1:6379> set money 100 # 设置余额:100
OK
127.0.0.1:6379> set use 0 # 支出使用: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 use 20
QUEUED
127.0.0.1:6379> exec # 监视值没有被中途修改,事务正常执行
1) (integer) 80
2) (integer) 20
测试多线程修改值,使用watch key可以做redis的乐观锁操作,相当于getVersion
解锁unwatch获取最新值,然后再加锁进行事务
注意:每次提交exec都会自动释放锁,不管是否成功
Jedis
使用jedis操作Redis是官方推荐的使用java来操作redis的客户端
1.导入依赖
<!--导入jredis的包-->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.2.0</version>
</dependency>
<!--fastjson-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.70</version>
</dependency>
2.修改redis的配置文件
Redis与Springboot的整合
1.导入依赖
<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模式
RedisAutoConfiguratio只有两个简单到bean
1.RedisTemplate
2.StringRedisTemplate
当看到xxTemplate时可以对比RestTemplat、SqlSessionTemplate,通过使用这些Template来间接操作组件。那么这俩也不会例外。分别用于操作Redis和Redis中的String数据类型。
在RedisTemplate上也有一个条件注解,说明我们是可以对其进行定制化的
说完这些,我们需要知道如何编写配置文件然后连接Redis,就需要阅读RedisProperties
基本属性
2.编写配置文件
#redis的配置
spring.redis.host=localhost
spring.redis.port=6379
2.使用RedisTemplate
@SpringBootTest
class Redis02SpringbootApplicationTests {
@Autowired
private RedisTemplate redisTemplate;
@Test
void contextLoads() {
// redisTemplate 操作不同的数据类型,api和我们的指令是一样的
// opsForValue 操作字符串 类似String
// opsForList 操作List 类似List
// opsForHah
// 除了基本的操作,我们常用的方法都可以直接通过redisTemplate操作,比如事务和基本的CRUD
// 获取连接对象
//RedisConnection connection = redisTemplate.getConnectionFactory().getConnection();
//connection.flushDb();
//connection.flushAll();
redisTemplate.opsForValue().set("mykey","kuangshen");
System.out.println(redisTemplate.opsForValue().get("mykey"));
}
}
4.测试结果
此时我们回到Redis查看数据时候,惊奇发现全是乱码,可是程序中可以正常输出:
这时候就关系到存储对象的序列化问题,在网络中传输的对象也是一样需要序列化,否者就全是乱码。
我们转到看那个默认的RedisTemplate内部什么样子:
在最开始就能看到几个关于序列化的参数。
默认的序列化器是采用JDK序列化器
而默认的RedisTemplate中的所有序列化器都是使用这个序列化器:
RedisSerializer
提供了多种序列化方案:
-
直接调用RedisSerializer的静态方法来返回序列化器,然后set
-
5. 定制自己的RedisTemplate
@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;
}
}
这样一来,只要实体类进行了序列化,我们存什么都不会有乱码的担忧了。
Redis订阅与发布
Redis发布订阅(pub/sub)是一种消息通信模式:发布者(pub)发送消息,订阅者(sub)接收消息
下面展示频道channel1 .以及订阅这个频道的三个客户端的--client2 . client5和 client1之间的关系:
当有新的消息通过PUBLISH 命令发送给频道channel1时,这个消息就会被发送给订阅他的客户端:
命令
实例:
------------订阅端----------------------
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本身的速度变慢,甚至直接崩溃。
- 这和数据传输可靠性有关,如果在订阅方断线,那么他将会丢失所有在短线期间发布者发布的消息。
应用:
1.消息订阅,公众号订阅,微博关注等等(开始的时候是使用消息队列来进行实现的)
2.多人在线聊天室
稍微复杂的场景,我们就会使用消息中间件MQ处理
Redis主从复制
概念
主从复制,是件将一台Redis服务器的数据,复制到其他的Redis服务器.前者成为主节点的,后者称为从节点,数据的复制是单向的,只能由主节点复制到从节点,(主节点以写为主,从节点以读为主)
默认情况下,每台Redis服务器都是一个主节点,一个主节点可以有0个从节点,但每个从节点只能有一个主节点.
作用
- 数据冗余:主从复制实现了数据的热备份,是持久化之外的一种数据冗余的方式
- 故障恢复:当主节点故障时,从节点可以替代主节点提供服务,是一种服务冗余的方式
- 负载均衡:在主从复制的基础上,配合读写分离,由主节点进行写的操作,从节点进行读的操作,分担服务器的负载,尤其是在多读少写的场景下,通过多个从节点分担负载,提高并发量,
- 高可用基石:主从复制还是哨兵模式和集群能够实施的基础
为什么要使用集群
- 单台服务器难以负载大量的请求
- 单台服务器故障率高,系统崩坏概率大
- 单台服务器内存容量有限
环境配置
我们在讲配置文件的时候,注意到有一个replication的模块(见redis.conf的第八条)
查看当前库的信息:info replication
127.0.0.1:6379> info replication
# Replication
role:master #角色
connected_slaves:0 #从节点数量
master_failover_state:no-failover
master_replid:3286d4fe057fe4e29c5d7b6547189d8baea11c5b
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文件名
- 日志文件名
- rdb文件名
启动单机多服务集群
一主二从配置
==默认情况下,每台服务器都是主节点;我们一般情况下只需要配置从机就好了
认老大 一主(79) 二从(80,81)
使用SLAVEOF host port就可以为从节点配置的主机了
然后主机上也能看到从机的状态:
使用规则:
- 从机只能读不能写,主机能读能写,不过一般用于写
-
当主机断电宕机后,默认情况下从机的角色不会发生变化 ,集群中只是失去了写操作,当主机恢复以后,又会连接上从机恢复原状。
-
当从机断电宕机后,若不是使用配置文件配置的从机,再次启动后作为主机是无法获取之前主机的数据的,若此时重新配置称为从机,又可以获取到主机的所有数据。这里就要提到一个同步原理。
-
第二条中提到,默认情况下,主机故障后,不会出现新的主机,有两种方式可以产生新的主机:
1.从机手动执行命令slaveof no one,这样执行以后从机会独立出来成为一个主机
2.使用哨兵模式(自动选举)
我们这里是使用命令搭建的集群,只是暂时的,真实的开发当中应该在从机的配置文件的当中配置的主机的信息,这样的话是永久的
哨兵模式
**主从切换技术的方法是:当主服务器宕机后,需要手动把一台服务器切换为主服务器,这就需要人工干预,费时费力,还会造成一段时间内的服务不可用,这不是一种推荐的方法,更多时候,我们优先考虑哨兵模式.
哨兵模式的作用:
1.通过发送命令,让Redis服务器返回监控其运行状态,包括主服务器和从服务器
2.当主服务器master宕机,会自动将slave切换为master,然后通过发布订阅模式通知其他的从服务器,修改配置文件的,让他们切换为主服务器.
新建一个sentinel.conf的配置文件:
哨兵模式的核心配置
sentinel monitor 被监控的名称 host port 1
sentinel monitor mymsater 127.0.0.1 6379 1
这里的mymaster为监控的主机名称 ,这里可以自定义
数字1表示:当一个哨兵认为主机已经断开,就可以客观的认为主机故障,然后选举新的主机
测试
启动哨兵模式
redis-sentinel +sentinel的配置文件
这里是redis-sentinel myconfig/sentinel.conf
我们这里的sentinel配置文件配置的是监控端口为6379的主机
当我们关闭6379的主机的时候哨兵模式会自动监控得到然后自动选举一个从机作为主机
当我们关闭6379的主机的时候,可以看到哨兵模式自动帮我们配置端口为6381的从机选举为主机
哨兵模式的优缺点
优点:
1.哨兵集群,基于主从复制,所有的主从复制的优点他都有
2.主从可以切换,故障可以转移,系统的可用性好
3.哨兵模式是主从复制的升级版,手动到自动,更加健壮
缺点:
1.Redis在线扩容很麻烦,集群容量一旦达到上限,在线扩容就很麻烦
2.实现哨兵模式的配置很麻烦.里面有很多配置项
哨兵模式的全部配置
# 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