# Redis 实战读书笔记

Posted 爱码代码的喵

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了# Redis 实战读书笔记相关的知识,希望对你有一定的参考价值。

Redis 实战读书笔记(一)

初始Redis

Redis是一个远程内存数据库,它不仅性能强劲而且还具有复制特性以及为解决问题而生的独一无二的数据模型。Redis提供了5中不同类型的数据库,初次之外通过复制持久化和客户端分片等特性用户可以很方便的将Redis扩展成一个能够包含数百GB数据每秒处理上百万次请求的系统。

Redis 和其它数据库的对比

名称类型数据存储选项查询类型附加功能
Redis内存存储的非关系型数据库字符串、列表、散列表、集合、有序集合每种数据类型都有自己的操作命令、批量操作、事务支持发布订阅、主从复制、持久化、脚本
memcached内存存储的键值缓存键值之间的映射创建读取更新、删除等其它命令为提升性能而设的多线程服务器
mysql关系型数据库表、视图、表空间、表的行DMLACID、主从复制、主主复制
postgresql关系型数据库表、视图、表空间、表的行DMLACID、主从复制
MongoDb使用硬盘存储的非关系型文档表、表包含多个schemabson文档创建读取更新、删除等其它命令支持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个命令可以让用户在不被打断的情况下对多个键执行操作, 它们分别是WATCHMULTIEXECUNWATCHDISCARD

  • 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可以正常接收命令。
  • 客户端向redisSAVE命令,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之后执行的所有写命令根据配置选项来决定是继续使用现有的数据(如果有的话)来处理客户端的命令请求,还是向发送请求的客户端返回错误。
3bgsave执行完毕,向从服务器发送快照文件,并在发送期间继续使用缓冲区记录被执行的写命令丢弃所有旧数据(如果有的话),开始载入主服务器发来的快照文件。
4快照文件发送完毕,开始向从服务器发送存储在缓冲区里面的写命令。完成对快照文件的解释操作,像往常一样开始接受命令请求。
5缓冲区存储的写命令发送完毕;从现在开始,每执行一个写命令,就向从服务器发送相应的写命令。执行主服务器发送的所有存储在缓冲区里面的写命令;并从现在开始,接受并执行主服务传来的每个写命令。
  • 从服务器在进行同步时,会清空自己的所有数据
  • Redis不支持主主复制

主从链

  • 当【读请求】的重要性明显高于【写请求】的重要性,并且读请求的数量远远超出一台Redis服务器可以处理的范围时,用户就需要添加新的从服务器来处理【读请求】。随着负载不断上升,主服务器可能会无法快速地更新所有从服务器,或者因为重新连接和重新同步从服务器而导致系统超载。为了缓解这个问题,用户可以创建一个由Redis主从节点组成的中间层来分担主服务器的复制工作。

一个Redis主从复制树示例,树的中层有3个帮助开展复制工作的服务器,底层与9个从服务器。

处理系统故障

  • redis提供了对应的命令redis-check-aofredis-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 被其他命令所改动,那么事务将被打断。

以上是关于# Redis 实战读书笔记的主要内容,如果未能解决你的问题,请参考以下文章

《Redis开发与运维》读书笔记

Redis设计与实现读书笔记 SDS

[redis读书笔记] 第二部分 单机数据库

[redis读书笔记] 第一部分 数据结构与对象 对象以及总结

《Redis设计与实现》读书笔记

[redis读书笔记] 第二部分 集群