Redis 实战读书笔记(一)
初始Redis
Redis
是一个远程内存数据库,它不仅性能强劲而且还具有复制特性以及为解决问题而生的独一无二的数据模型。Redis
提供了5
中不同类型的数据库,初次之外通过复制持久化和客户端分片等特性用户可以很方便的将Redis
扩展成一个能够包含数百GB
数据每秒处理上百万次请求的系统。
Redis 和其它数据库的对比
名称 | 类型 | 数据存储选项 | 查询类型 | 附加功能 |
---|---|---|---|---|
Redis | 内存存储的非关系型数据库 | 字符串、列表、散列表、集合、有序集合 | 每种数据类型都有自己的操作命令、批量操作、事务支持 | 发布订阅、主从复制、持久化、脚本 |
memcached | 内存存储的键值缓存 | 键值之间的映射 | 创建读取更新、删除等其它命令 | 为提升性能而设的多线程服务器 |
mysql | 关系型数据库 | 表、视图、表空间、表的行 | DML 等 | ACID 、主从复制、主主复制 |
postgresql | 关系型数据库 | 表、视图、表空间、表的行 | DML 等 | ACID 、主从复制 |
MongoDb | 使用硬盘存储的非关系型文档 | 表、表包含多个schema 的bson 文档 | 创建读取更新、删除等其它命令 | 支持map-reduce 、主从复制、分片、空间索引 |
Redis 数据结构简介
结构类型 | 结构存储的值 | 结构的读写能力 |
---|---|---|
Strin | 字符串、整数或者浮点数 | 浮点数自增或者自减 |
List | 链表链表上的每个节点包含了一个字符串 | 链表两端推入弹出、根据偏移量进行删除… |
Hash | 键值对的无序散列表 | 增查删 |
Set | 包含字符串的无序收集器,并且被包含的每个字符串都是唯一的 | 增删改查、计算交并差集… |
Zset | 字符串成员与浮点数分值之间的有序映射,元素的排列顺序由分值大小决定 | 增查删根据分值范围或者成员来获取元素 |
常用命令
字符串命令
命令 | 作用 |
---|---|
GET | 获取 |
SET | 设置 |
DEL | 删除 |
列表命令
命令 | 作用 |
---|---|
RPUSH | 列表右端插入 |
LRANGE | 获取列表在给定范围上的所有值 |
LINDEX | 获取列表在给定位置上的单个元素 |
LPOP | 从列表的左端弹出一个值 |
散列命令
命令 | 作用 |
---|---|
HGET | 获取 |
HSET | 设置 |
HDEL | 删除 |
HGETALL | 获取散列包含的所有键值对 |
集合命令
命令 | 作用 |
---|---|
SADD | 添加元素 |
SMEMBERS | 返回集合包含的所有元素 |
SISMEMBER | 检查给定元素是否存在于集合中 |
SREM | 如果给定的元素存在那么移除这个元素 |
有序集合命令
命令 | 作用 |
---|---|
ZADD | 将一个带给定分值的成员添加到有序集合里面 |
ZRANGE | 根据元素在有序排列中所处的位置从有序集合里面获取多个元素 |
ZRANGERBYSCORE | 获取有序集合在给定分值范围内的所有元素 |
ZREM | 如果给定的元素存在那么移除这个元素 |
使用Redis 构建Web应用
典型Web 服务器对请求进行相应的步骤
- 服务器对客户端发来的请求进行解析
- 请求被转发给一个预定义的处理器
- 处理器从数据库中拿到数据
- 处理器拿到的数据对模板进行渲染
- 处理器向客户端返回渲染的内容作为响应
签名cookie和令牌cookie
cookie类型 | 优点 | 缺点 |
---|---|---|
签名cookie | 验证cookie 所需的一切信息都存储在cookie 里面。cookie 可以包含额外的信息, 并且对这些信息进行签名也很容易 | 正确地处理签名很难。很容易忘记对数据进行签名,或者忘记验证数据的签名, 从而造成安全漏洞 |
令牌cookie | 添加信息非常容易。cookie 的体积非常小,因此移动终端和速度较慢的客户端可以更快地发送请求 | 需要在服务器中存储更多信息。如果使用的是关系数据库, 那么载入和存储cookie 的代价可能会很高 |
文章投票伪代码
/**
* 文章投票模拟
*/
@Override
public void articalVote() {
// 散列值存储文章信息
String articalKey = "artical:";
redisTemplate.opsForHash().put(articalKey + "1001", "title", "文章1001");
redisTemplate.opsForHash().put(articalKey + "1001", "votes", 528);
redisTemplate.opsForHash().put(articalKey + "1001", "time", "20230209");
redisTemplate.opsForHash().put(articalKey + "1002", "title", "文章1002");
redisTemplate.opsForHash().put(articalKey + "1002", "votes", 520);
redisTemplate.opsForHash().put(articalKey + "1002", "time", "20230209");
// 文章发布时间有序集合
redisTemplate.opsForZSet().add("time", "artical:1001", 20230209);
redisTemplate.opsForZSet().add("time", "artical:1002", 20230209);
redisTemplate.opsForZSet().add("time", "artical:1002", 20230208);
// 文章评分有序集合
redisTemplate.opsForZSet().add("time", "artical:1001", 528);
redisTemplate.opsForZSet().add("time", "artical:1002", 520);
redisTemplate.opsForZSet().add("time", "artical:1003", 100);
// 每篇文章记录已经投票的用户
redisTemplate.opsForSet().add("voted:1001", "user1001");
redisTemplate.opsForSet().add("voted:1001", "user1002");
redisTemplate.opsForSet().add("voted:1002", "user1003");
redisTemplate.opsForSet().add("voted:1002", "user1004");
}
本章小结
在为应用程序创建新构件时,不要害怕回过头去重构已有的构件,已有的构件有时候需要进行一些细微的修改才能真正满足你的需求。
Redis 命令
字符串
列表
- 列表的一个主要优点在于它可以包含多个字符串值,这使得用户可以将数据集中在同一个地方。
集合
- 用于组合和处理多个集合的
Redis
命令
散列
有序集合
发布与订阅
- 简单理解就是将消息发送给指定频道时,频道所有的订阅者都会收到消息。订阅者可以同时收听多个频道的发出的消息
Java
代码简单实现
@Component
public class RedisPublisher {
public static final String CHANNEL = "channel_01";
/**
* Redis订阅消息监听容器
*
* @param connectionFactory connection factory
* @param messageListenerAdapter message listener adapter
* @return RedisMessageListenerContainer
*/
@Bean
public RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory, MessageListenerAdapter messageListenerAdapter) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
// 可以添加多个MessageListener
container.addMessageListener(messageListenerAdapter, new PatternTopic(CHANNEL));
return container;
}
/**
* 配置消息处理适配器
*
* @param redisConsumer redis consumer
* @return MessageListenerAdapter
*/
@Bean
public MessageListenerAdapter listenerAdapter(RedisConsumer redisConsumer) {
// messageListenerAdapter 传入一个消息接受的处理器,利用反射的方式调用对应的处理方法
return new MessageListenerAdapter(redisConsumer, "onMessage");
}
}
@Configuration
public class RedisConsumer implements MessageListener {
private static final Logger logger = LoggerFactory.getLogger(RedisConsumer.class);
@Autowired
private RedisTemplate redisTemplate;
@Override
public void onMessage(Message message, byte[] pattern) {
RedisSerializer<String> serializer = redisTemplate.getStringSerializer();
String body = serializer.deserialize(message.getBody());
String chanel = serializer.deserialize(message.getChannel());
logger.info("接收的消息:{},使用的chanel:{}", body, chanel);
}
}
- 发布消息代码
@Override
public void publishMessage(Map<String, Object> message) {
// 发布者
redisTemplate.convertAndSend(CHANNEL, JSON.toJSONString(message));
}
其他命令
-
可以根据字符串、 列表、 集合、 有序集合、 散列这5种键里面存储着的数据, 对列表、 集合以及有序集合进行排序。
-
为了对相同或者不同类型的多个键执行操作,
Redis
有5个命令可以让用户在不被打断的情况下对多个键执行操作, 它们分别是WATCH
、MULTI
、EXEC
、UNWATCH
和DISCARD
。 -
当
Redis
从一个客户端那里接收到MULTI
命令时,Redis
会将这个客户端之后发送的所有命令都放入到一个队列里面, 直到这个客户端发送EXEC
命令为止, 然后Redis
就会在不被打断的情况下, 一个接一个地执行存储在队列里面的命令
数据性能与安全保障
持久化方式
- 配置项
#快照持久化选项
#多久执行一次自动快照操作
save 60 1000
#创建快照失败后是否仍然继续执行写命令
stop-writes-on-bgsave-error no
#是否对快照文件进行压缩
rdbcompression yes
#命名硬盘上的快照文件
dbfilename dump.rdb
#AOF持久化选项
appendonly no
appendfsync everysec
no-appendfsync-on-rewrite no
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
#共享选项,这个选项决定了快照文件和AOF文件的保存位置。
dir ./
快照(snapshotting)
- 如果系统发生崩溃,用户将丢失最近一次生成快照之后更改的所有数据
- 这种方式在内存中的数据过大的时候生成快照会耗费一定的时间,即使手动生成快照也会造成耗时。
触发redis创建快照
- 客户端向
redis
发送BGSAVE
命令,会建立子进程将快照写入存储,同时redis
可以正常接收命令。 - 客户端向
redis
发SAVE
命令,redis
在执行写入存储前不接收命令。 - 配置文件设置快照,例如
save 60 5000
,可配置多个。达到条件时触发BGSAVE
命令。 SHUTDOWN
时,会触发SAVE
命令。slave redis
发关SYNC
命令后,主启动BGSAVE
命令。
只追加文件(append-only file)
AOF
持久化会将被执行的命令写到AOF
文件的末尾,以此来记录数据发生的变化
appendfsync
appendfsync always
:总是写入aof
文件,并完成磁盘同步appendfsync everysec
:每一秒写入aof
文件,并完成磁盘同步appendfsync no
:写入aof
文件,不等待磁盘同步。
可见,从持久化角度讲,always
是最安全的。从效率上讲,no
是最快的。而redis
默认设置进行了折中,选择了everysec
,即使数据丢失也只会丢失一秒之内产生的数据
复制
- 从服务器连接主服务器时的步骤
步骤 | 主服务器操作 | 从服务器操作 |
---|---|---|
1 | (等待命令进入) | 连接(或者重连接)主服务,发送sync 命令。 |
2 | 开始执行bgsave ,并使用缓冲区记录bgsave 之后执行的所有写命令 | 根据配置选项来决定是继续使用现有的数据(如果有的话)来处理客户端的命令请求,还是向发送请求的客户端返回错误。 |
3 | bgsave 执行完毕,向从服务器发送快照文件,并在发送期间继续使用缓冲区记录被执行的写命令 | 丢弃所有旧数据(如果有的话),开始载入主服务器发来的快照文件。 |
4 | 快照文件发送完毕,开始向从服务器发送存储在缓冲区里面的写命令。 | 完成对快照文件的解释操作,像往常一样开始接受命令请求。 |
5 | 缓冲区存储的写命令发送完毕;从现在开始,每执行一个写命令,就向从服务器发送相应的写命令。 | 执行主服务器发送的所有存储在缓冲区里面的写命令;并从现在开始,接受并执行主服务传来的每个写命令。 |
- 从服务器在进行同步时,会清空自己的所有数据
Redis
不支持主主复制
主从链
- 当【读请求】的重要性明显高于【写请求】的重要性,并且读请求的数量远远超出一台
Redis
服务器可以处理的范围时,用户就需要添加新的从服务器来处理【读请求】。随着负载不断上升,主服务器可能会无法快速地更新所有从服务器,或者因为重新连接和重新同步从服务器而导致系统超载。为了缓解这个问题,用户可以创建一个由Redis
主从节点组成的中间层来分担主服务器的复制工作。
一个
Redis
主从复制树示例,树的中层有3
个帮助开展复制工作的服务器,底层与9
个从服务器。
处理系统故障
redis
提供了对应的命令redis-check-aof
和redis-check-rdb
验证aof
文件和快照文件,是否有效。- 可以对
aof
使用–fix
进行修复,但是快照目前无法修复。 - 修复只是删除出错得命令以及位于出错命令之后得所有命令。
Redis 事务
- 单个
Redis
命令的执行是原子性的,但Redis
没有在事务上增加任何维持原子性的机制,所以Redis
事务的执行并不是原子性的。 - 事务可以理解为一个打包的批量执行脚本,但批量指令并非原子化的操作,中间某条指令的失败不会导致前面已做指令的回滚,也不会造成后续的指令不做。
命令
DISCARD
取消事务,放弃执行事务块内的所有命令。EXEC
执行所有事务块内的命令。MULTI
标记一个事务块的开始。UNWATCH
取消WATCH
命令对所有key
的监视。WATCH
key
[key
…] 监视一个(或多个)key
,如果在事务执行之前这个(或这些)key
被其他命令所改动,那么事务将被打断。
何维持原子性的机制,所以 Redis
事务的执行并不是原子性的。
- 事务可以理解为一个打包的批量执行脚本,但批量指令并非原子化的操作,中间某条指令的失败不会导致前面已做指令的回滚,也不会造成后续的指令不做。
命令
DISCARD
取消事务,放弃执行事务块内的所有命令。EXEC
执行所有事务块内的命令。MULTI
标记一个事务块的开始。UNWATCH
取消WATCH
命令对所有key
的监视。WATCH
key
[key
…] 监视一个(或多个)key
,如果在事务执行之前这个(或这些)key
被其他命令所改动,那么事务将被打断。