[{"content":"Kafka Kafka: ZooKeeper协调的分布式消息系统\n基于Scala语言编写的高性能、多分区、多副本 Kafka高性能的原因：页缓存、顺序IO、零拷贝 具有以下特性：\n消息中间件: 系统解耦、冗余存储、流量消峰、异步通信等 存储系统: 通过消息持久化和多副本机制实现消息落盘 流处理: 为流式处理框架提供可靠的数据来源和库 Kafka组成: 若干个Producer、Consumer、Broker和ZooKeeper集群\nProducer(生产者): 生产并发送消息到Broker(推送) Consumer(消费者): 从Broker订阅并消费消息(拉取) Broker(服务代理节点): 将从Producer收到的消息进行落盘 ZooKeeper集群：管理Kafka集群的元数据 // Broker可看成单个独立的Kafka服务实例, 多个Broker组成个Kafka集群\n如: Kafka集群构成\n基础概念 主题(Topic): Kafka中消息归类单位\nTopic并不实际存在(仅逻辑上的概念) Topic可细分为多个Partition, 但Partition仅属于单个Topic 功能: Producer将消息发送到特定Topic, Consumer订阅Topic消费消息 分区(Partition): 组成Topic的单位(实际存储消息)\nPartition在存储层面可视为: 可被追加的日志文件 同一Topic下的不同Partition包含的消息是不同的 Partition可跨Broker(Topic可跨Broker) 偏移量(Offset): 消息追加到Partition时分配的标志位\nOffset是消息在Partition中的唯一标识(保证Partition内的有序性) Offset不支持跨Partition(Topic无序) 如: 消息追加写入Partition\n消息在发送到Broker之前, 都会先根据Partition规则分配到具体的Partition Topic的Partition应避免都属于单个文件(避免机器的I/O成为性能瓶颈) Partition中2个特殊的Offset:\nHW(High Watermark): Consumer能拉取到消息的最大Offset LEO(Log End Offset): Partition下条消息写入的Offset // ISR中最小的LEO为该Partition的HW(最慢的follower)\n如: Partition中的特殊Offset\n副本(Replica): Partition的冗余\n功能: Kafka通过多副本机制提高容灾能力 副本之间分为：leader(主副本)、follower(从副本) 副本间仅存在一主多从关系, 且可实现自动故障转移 Producer和Consumer只能和leader进行交互(follower仅进行消息同步) 如: Kafka的多副本交互\n副本相关名词:\nAR(Assigned Replicas): 所有副本(包括leader) ISR(In-Sync Replicas): 与leader保持同步的副本(包括leader) OSR(Out-of-Synce Replicas): 与leader同步滞后过多的副本(数据不同步) // 默认仅ISR中的副本才有资格选举为leader, 且负责动态管理ISR和OSR中的follower\n延迟任务 时间轮(TimeingWheel): 以固定时间粒度为单位管理和调度事件的数据结构\n时间跨度(tickMs): 时间轮构成的基本单位, 个数固定 表盘指针(currentTime): 指向当前所处的时间粒度 时间轮对于插入/删除操作的时间复杂度为O(1) 定时器(SystemTimer): Kafka中各类延迟操作的触发\n本质: 基于时间轮机制和数组构成的环形队列 定时任务项(TimerTaskEntry): 封装真正的定时/延迟任务(Task) 定时任务列表(TimerTaskList): 存放时间粒度下所有TimerTaskEntry的双向链表 如: 定时器构成结构\n当添加TimerTaskEntry时, 会根据过期时间和currentTime算出应插入的TimerTaskList 当计算结果超出总tickMs时, 会复用之前的TimerTaskList TimerTaskList中都有个哑元节点方便操作(不存储数据) 层级时间轮(Hierarchical TimeingWheel): 分层处理不同tickMs的多级时间轮的组合结构\n本质: 通过划分每个时间轮处理的时间范围, 以保证时间轮的高性能 升级: 当TimerTaskEntry的过期时间超出本层的时间范围时, 将交由上层时间轮 降级: 当TimerTaskEntry在高层时间轮中过期时, 会将其减少已过的时间并重新提交到层级时间轮 TimerTask仅能由最底层的时间轮负责执行处理, 高层的时间轮仅根据时间粒度负责其的编排和重新提交 // Kafka中通过DelayQueue和ExpiredOperationReaper线程实现时间的推进(避免空转造成的性能浪费)\n如: 层级时间轮\n层级时间轮创建时会以当前系统时间作为最底层时间轮的起始时间(startMs) 高层时间轮的起始时间都为创建时上一层时间轮的currentTime 每层时间轮的currentTIme都必须是tickMs的整数倍 Kafka仅持有最底层时间轮的引用 延迟操作管理器(DelayedOperationPurgatory, DOP): 管理/执行Kafka中各类延迟操作\n每个DOP都对应个定时器(超时管理)和监听池(监听Partition事件) 当进行延迟拉取时, 会读取两次日志文件并等待足够数量的消息才会返回 如: Producer的延迟操作\nProducer Producer(生产者): 生产并发送消息到Broker(推送)\nProducer是多线程安全的(建议通过池化以提高性能) Producer实例后可发送多条消息(可对应多个ProducerRecord) // 0.9之后的版本是基于Java实现(之前是Scala实现)\nProducer客户端发送消息大致逻辑:\n配置Producer客户端参数并创建该Producer实例 构建需发送的消息 发送构建的消息 关闭实例 构造Producer必填的3个参数:\n参数 说明 bootstrap.servers 引导程序的服务地址\n格式: 地址1:端口1,地址N:端口N\n(建议指定两个以上的Broker地址以保证稳定性, 且使用主机名形式) key.serializer 发送时对Key调用的序列化器\nBroker仅能接受字节数组形式的消息byte[] value.serializer 发送时对Value调用的序列化器\nBroker仅能接受字节数组形式的消息byte[] // 序列化器必须以全限定名方式指定, Java的ProducerConfig类中包含所有的配置参数\nProducerRecord ProducerRecord(构建消息): Producer每次发送的消息体\nProducerRecord由多个属性构成(Topic和消息是基础属性) ProducerRecord有多个构造方法(指定属性的个数) 可根据不同需求创建特定ProducerRecord ProducerRecord定义:\npublic class ProducerRecord\u0026lt;K, V\u0026gt; { private final String topic; // Topic(必填) private final Integer partition; // Partition // 消息头部(0.11版本引入) // 指定与应用相关信息(可忽略) private final Headers headers; // 键(附加信息) // 其会用于计算Partition(二次归类) private final K key; // 值(消息体, 必填) // 为空则代表: 墓碑消息 private final V value; // 消息时间戳 // 细分为CreateTime(消息创建时间)和LogAppendTime(追加日志时间) private final Long timestamp; ...... } Send\u0026amp;Close Send(发送消息): Producer构建ProducerRecord之后发送给Broker\n发送模式: 发后既忘(fire-and-forget)、同步(sync)、异步(async) 发送模式默认为异步(可通过获取返回值的方法以阻塞等待实现同步) 返回值通常为发送消息的元数据(Topic、Partition、偏移量和时间戳等) Send()方法的定义:\npublic Future\u0026lt;RecordMetadata\u0026gt; send(ProducerRecord\u0026lt;K, V\u0026gt; record); public Future\u0026lt;RecordMetadata\u0026gt; send(ProducerRecord\u0026lt;K, V\u0026gt; record, Callback callback); 可通过Future的get()方法阻塞实现同步(返回RecordMetadata对象) Send()方法需配合try/catch(发送成功或发生异常) 发送导致的异常分为: 重试异常、不可重试异常 // 不可重试异常发生时会直接抛出并结束\n常见的重试异常为：\n可重试异常 说明 NewworkException 网络异常 LeaderNotAvailableException 副本的leader不可用\n(可能正在选举leader) UnknownTopicOrPartitionException Topic或Partition异常 NotEnoughReplicasException 副本数量不足 NotCoordinatorException 协调器异常 Send()方法中的Callback定义:\npublic interface Callback { void onCompletion(RecordMetadata var1, Exception var2); } var1和var2参数互斥(两者必有个为null，后者代表异常) 若两个消息对相同Partition发送消息, 则按发送顺序调用Callback Close(结束发送):回收Producer实例\n发送结束后务必回收Producer实例(防止资源泄漏) Close默认会阻塞等待之前所有的发送请求完成之后再回收 可指定关闭的超时时间(超出该事件则强行回收, 不建议指定) Close()方法的定义：\npublic void close(); public void close(long timeout, TimeUnit timeUnit); 实现原理 Producer的发送消息由两个线程完成:\n主线程: 构建并处理消息后发送至RecordAccumulator Sender线程: 从RecordAccumulator获取消息, 并发送至Broker 如: Producer发送消息链路图\nRecordAccumulator: 双端队列缓存待发送ProducerBatch以减少网络影响 ProducerBatch: 包含任意多个待发送的ProducerRecord(消息批次) Request: Kafka支持的各种请求协议 InFlightRequests: 缓存已发送但未响应的Request // Interceptor和Partitioner可选择性处理, 但必须经Serializer处理\nProducer发送ProducerRecord的流程:\n主线程将ProducerRecord加工处理后发送至RecordAccumulator尾部 RecordAccumulator根据ProducerRecord分区选择对应的ProducerBatch RecordAccumulator根据内存复用原则和ProducerBatch大小决定是否新建 Sender线程从RecordAccumulator头部获取ProducerBatch 将\u0026lt;分区, \u0026lt;Deque\u0026lt;ProducerBatch\u0026gt;\u0026gt;形式变为\u0026lt;Node, List\u0026lt;ProducerBatch\u0026gt;\u0026gt; 再根据各种协议请求转换为\u0026lt;Node, Request\u0026gt;形式 发送前以Map\u0026lt;nodeId, Deque\u0026lt;Request\u0026gt;\u0026gt;缓存Request 返回发送后的响应并清理InFlightRequests和RecordAccumulator // 形式转换是为完成应用逻辑层到网络I/O层的转换\nRecordAccumulator内存复用原则:\nRecordAccumulator通过java.io.ByteBuffer和BufferPool实现内存复用 若内存申请不超过指定大小, 则申请指定大小并放置于BufferPool 若内存申请超过指定大小, 则申请该内存并再使用后直接释放 // BufferPool可避免频繁的申请和释放内存\nInFlightRequest中包含leastLoadedNode\nleastLoadedNode: 负载最小的Broker(未确认请求最少的) leastLoadedNode常用于元数据请求和Consumer组播协议的交互 leastLoadedNode由Sender线程根据指定过期时间维护(主线程也可访问) // 元数据: Broker、Topic、Partition、leader和follower副本所在的Broker等\n如: Sender线程维护leatLoadedNode信息\nSender线程检查元数据是否过期(默认5m) 超出则挑出leastLoadedNode, 向该Broker发送MetadataRequest请求 获取结果后将其结果存入InFlightRequests中, 并更新元数据的过期时间 ProducerInterceptor ProducerInterceptor(拦截器): 消息发送前/后的进行的操作\n不建议通过ProducerInterceptor修改topic、key和partition 可指定多个ProducerInterceptor(拦截链按配置时顺序执行) 可通过interceptor.classes参数指定Producer所使用的ProducerInterceptor ProducerInterceptor定义:\npublic interface ProducerInterceptor\u0026lt;K, V\u0026gt; extends Configurable { // 发送前进行的操作 public ProducerRecord\u0026lt;K, V\u0026gt; onSend(ProducerRecord\u0026lt;K, V\u0026gt; record); // 发送后被应答之后或失败进行的操作 // 优先于Send()方法中定义的Callback前执行 // 由于该方法运行于Producer的IO线程中, 应简洁 public void onAcknowledgement(RecordMetadata metadata, Exception exception); // 关闭拦截器 public void close(); } // 抛出的任何异常都会被记录到日志中, 并不再向上抛\nSerializer Serializer(序列化器): 将特定数据转换成字节数组(byte[])\nBroker仅能接受字节数组形式的数据(接收后会对其反序列化) Producer使用的Serializer需和Consumer使用的反序列化器需对应 Producer指定Serializer时, 需通过全限定名方式指定(类的完整路径) Serializer定义:\npublic interface Serializer\u0026lt;T\u0026gt; extends Closeable { // 配置序列化器 // 常用于指定编码类型(默认UTF-8) void configure(Map\u0026lt;String, ?\u0026gt; configs, boolean isKey); // 执行序列化 byte[] serialize(String topic, T data); // 关闭序列化器 // 需保证幂等性 void close(); } // 不建议使用自定义Serializer或DeSerializer, 会增加耦合度\nPartitioner Partitioner(分区器): ProducerRecord分区的默认规则\nProducerRecord中指定partition字段, 则略过Partitioner Partitioner的分区计算受Topic数量的影响(已分配的不受) 可通过partitioner.class参数指定Producer所使用的Partitioner Partitioner定义:\npublic interface Partitioner extends Configurable, Closeable { // 计算并返回分区号 public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster); // 关闭分区器 public void close(); } public interface Configurable { // 获取配置信息并初始化数据 void configure(Map\u0026lt;String, ?\u0026gt; configs); } 默认的Partitioner: org.apache.kafka.clients.producer.internals.DefaultPartitioner\n其close()方法默认为空 消息为null时, 则以轮询的方式分配可用的分区号 消息不为null时, 则进行Hash计算(MurmurHash2算法) // 消息相同的情况下会写入相同的分区(存在消息互相覆盖的情况)\n事务 事务(Transaction): Producer操作的最小原子单位(可跨Partition)\n开启事务时, 必须也需开启幂等性(enable.idempotence) 开启事务时必须指定事务ID(若事务ID重复, 将结束被覆盖的事务并抛出异常) 只能使事务处于以下两种状态(否则将抛出异常): COMMIT、ABORT 事务开启后需关闭自动位移提交, 也不能位移消费 Producer中常用的事务方法:\n// 初始化事务 void initTransactions(); // 开启事务 void beginTransaction(); // 事务内的位移提交 void sendOffsetsToTransaction(Map\u0026lt;TopicPartition, OffsetAndMetadata\u0026gt; offsets, String consumerGroupId) // 提交事务 void commitTransaction(); // 终止事务(回滚) void abortTransaction(); 事务协调器(TransactionCoordinator): 负责事务中的各类操作\n每个Producer都对应个事务协调器, 由其负责Producer中各类请求 事务协调器会将事务的信息都存储至内部Toipc的__transaction_state 如: 事务的执行流程\n查找事务协调器: 找到事务协调器所在的Broker并建立连接(同时查找Partition) 获取PID: 通过InitProducerIdRequest请求获取该事务ID 执行事务: 通过各类请求处理Record并将数据存储至内部Topic 结束事务: 发送各类请求结束事务, 同时将事务信息存储至内部Topic和日志文件 Consumer的事务受以下限制:\n采用日志压缩策略的Topic, 其Record可能被覆盖 Consumer在消费时可能没有分配到事务内的所有Partition Record可能分布在Partition的多个LogSegment, 存在部分被清除的可能 Consumer可通过位移提交/位移消费访问Record, 可能导致遗漏事务中的Record Consumer Consumer(消费者): 从Partition拉取并消费消息(非线程安全)\nTopic的Partition在每个消费者组中有且仅能由一个Consumer消费 若Consumer数量多于Partition, 则部分Consumer空闲(无对应Partition) 每个Consumer仅能消费从消费者组中分配到或单独订阅Partition所含消息 Partition分配策略: 定义Consumer对订阅Topic下的Partition的划分\n分配策略 说明 RangeAssignor\n(默认) Partition按跨度依次分配给Consumer\n(跨度 = Partition数量 / Consumer数量) RoundRobinAssignor 轮询方式依次将Partition分配给Consumer\n(轮询前会先按照字典序对Consumer和Partition进行排序)\n(分配给Consumer的Partition必须是订阅Topic下的Partition, 否则将略过) StickyAssignor 在RoundRobinAssignor的基础上尽可能保持黏性分配 // 以下均以RangeAssignor分配策略说明, 可通过partition.assignment.strategy参数更改\n消费者组(Consumer Group): 多个Consumer组成的消费群体\nTopic可被订阅的消费者组下任意个Consumer消费 Consumer通过group.id参数指定所属消费者组 每个Consumer有且仅有一个消费者组 消费者组之间无法感知(互不影响) 如：A消费者组和B消费者组订阅相同的Topic\n若Topic对应的所有Consumer都属于相同的消费者组, 则为点对点(P2P) 若Topic对应的所有Consumer属于不同的消费者组, 则为发布/订阅(Pub/Sub) Consumer客户端消费消息大致逻辑:\n配置Consumer客户端参数并创建该Consumer实例 订阅Topic, 拉取并消费消息(位移提交) 关闭实例 构建Consumer客户端必填的4个参数:\n参数 说明 bootstrap.servers 引导程序的服务地址\n格式: 地址1:端口1,地址N:端口N\n(建议指定两个以上的Broker地址以保证稳定性, 且使用主机名形式) group.id Consumer所属消费者组 key.derializer 消费时对Key调用的反序列化器\nBroker仅能接受字节数组形式的消息byte[] value.derializer 消费时对Value调用的反序列化器\nBroker仅能接受字节数组形式的消息byte[] // 序列化器必须以全限定名方式指定, Java的ConsumerConfig类中包含所有的配置参数\nclose()和wakeup()方法的定义:\n// 关闭Consumer // timeout参数指定关闭的超时时间(默认30s) public void close(); public void close(Duration timeout); // 唤醒Consumer // 该方法是唯一的线程安全方法 // 若唤醒阻塞的Consumer, 则抛出WakeupException public void wakeup(); 消费消息 消费消息: 订阅Topic使Consumer消费特定Partition\nTopic和Partition的定义:\n// Partition构成 public final class TopicPartition implements Serializable { private int hash = 0; // 每个TopicPartition的唯一标识 private final int partition; // 所属Topic private final String topic; // Partition编号 // 其他方法省略(构造函数和属性提取等) } // Topic元数据信息 // 该信息可通过Consumer的partitionsFor()方法获取(List集合形式返回) public class PartitionInfo { private final String topic; // Topic编号 private final int partition; // Partition编号 private final Node leader; // leader副本所在的Partition private final Node[] replicas; // AR private final Node[] inSyncReplicas; // ISR private final Node[] offlineReplicas; // OSR // 其他方法省略(构造函数和属性提取等) } 订阅/拉取 订阅(Subscribe): Consumer订阅个Topic/Partition以消费Partition\nConsumer可单独订阅Partition, 但其会脱离消费者组管理 单独订阅Partition还会导致Consumer的自动再均衡失效 Consumer可订阅多个Topic(可分配到多个Partition) 若Conuser进行多次订阅操作, 则以最后次为准 两种形式的订阅都可被取消 subscribe()和assign()方法的定义:\n// 订阅集合中所有的Topic // ConsumerRebalanceListener(再均衡监听器):监听特殊事件以触发再均衡 public void subscribe(Collection\u0026lt;String\u0026gt; topics); public void subscribe(Collection\u0026lt;String\u0026gt; topics, ConsumerRebalanceListener listener); // 订阅所有匹配正则表达式的Topic // 若后续新创建的Topic满足正则表达式, 则会自动订阅该Topic // ConsumerRebalanceListener(再均衡监听器):监听特殊事件以触发再均衡 public void subscribe(Pattern pattern); public void subscribe(Pattern pattern, ConsumerRebalanceListener listener); // 订阅指定集合中所有的Partition public void assign(Collection\u0026lt;TopicPartition\u0026gt; partitions); 订阅状态分为：AUTO_TOPICS、AUTO_PATTERN、USER_ASSIGNED Consumer的订阅状态只能为其一(未订阅则为NONE) 建议通过subscribe()方法订阅(具有再均衡的功能) unsubscribe()方法的定义:\n// 取消Consumer的所有订阅 // 效果等同于订阅空的集合/无匹配的正则表达式 public void unsubscribe(); 拉取(Pull): Consumer的消费是基于拉模式\n拉模式: 主动向服务端发起请求以获取消息消费 Consumer可暂停/恢复对指定Partition的消费(不再拉取) 拉取会自动根据拉取请求的session_id和epoc分为: 全量拉取、增量拉取 poll()方法的定义:\n// 拉取Consumer绑定的Partition的消息 // timeout参数用于指定获取消息前阻塞等待的时间(0则立刻返回) public ConsumerRecords\u0026lt;K, V\u0026gt; poll(final Duration timeout) { return poll(timeout.toMillis(), true); } // 拉取Consumer绑定的Partition的消息 // timeout参数用于指定获取消息前阻塞等待的时间(0则立刻返回) // includeMetadataInTimeout参数指定阻塞等待时是否考虑元数据超时 // // ConsumerRecords由多个ConsumerRecord组成的消息集(iterator()方法遍历) // 还可通过records（）方法获取消息集指定所属Topicd/Partition的消息 private ConsumerRecords\u0026lt;K, V\u0026gt; poll(final long timeoutMs, final boolean includeMetadataInTimeout) pause()和resume()方法的定义:\n// 暂停指定Partition的消费 // 该方法不会影响Consumer的订阅 // 可通过paused()获取所有被暂停的Partition public void pause(Collection\u0026lt;TopicPartition\u0026gt; partitions); // 恢复指定Partition的消费 // 若Partition未被暂停, 则直接返回 public void resume(Collection\u0026lt;TopicPartition\u0026gt; partitions); ConsumerRecord ConsumerRecord(消费消息): Consumer获取的消息体\nConsumerRecord由多个属性构成(Topic和消息算基础属性) ConsumerRecord有多个构造方法(指定属性的个数) ConsumerRecord与ProducerRecord相对应 ConsumerRecord定义:\npublic class ConsumerRecord\u0026lt;K, V\u0026gt; { private final String topic; // 所属Topic private final int partition; // Partition编号 private final long offset; // 所在Partition的偏移量 private final long timestamp; // 时间戳 // 时间戳类型 // CreateTime类型: 创建消息时间 // LogAppendTime类型: 追加到日志的时间 private final TimestampType timestampType; private final K key; // 键 private final V value; // 值 private final Headers headers; // 消息的头部内容 private final int serializedKeySize; // 键所对应的反序列化器 private final int serializedValueSize; // 值所对应的反序列化器 private volatile Long checksum; // CRC32校验值 // 其他方法省略 } 消费位移 消费位移: Consumer在Partition下个消费的ConsumerRecord位置\n偏移量(Offset): ProducerRecord在Partition中的位置 消费位移均存储于内部Topic的__consumer_offsets Consumer在每个分区中都有个消费位移 position()和committed()方法的定义:\n// 获取Consumer下条消费的ConsumerRecord在指定Partition中的位置 // timeout参数指定获取该信息的最大阻塞时间 public long position(TopicPartition partition); public long position(TopicPartition partition, final Duration timeout); // 获取Consumer最后次消费的ConsumerRecord在指定Partition中的位置 // timeout参数指定获取该信息的最大阻塞时间 public OffsetAndMetadata committed(TopicPartition partition); public OffsetAndMetadata committed(TopicPartition partition, final Duration timeout); 如: Consumer消费Partition后的位置信息\n// 从Broker拉取消息时, 会同时记录每条消息的具体位置\n位移提交 位移提交: 持久化消费位移信息\n位移提交并不总是与Position信息相同 位移提交策略分为：默认提交、手动提交 默认提交: 交由Kafka管理提交\nenable.auto.commit参数配置是否开启 默认5s提交次Partition中最大的消费位移, 其存在重复消费和消息丢失的风险 Consumer每次拉取之前也会检查次是否可提交, 满足则先提交再拉取 默认Consumer在消费完消息集后进行位移提交(延迟提交) 如: 消费过程中出现异常后恢复导致的重复消费\n// 若出现异常后未恢复, 且其他Consumer又进行位移提交则发送消息丢失\n手动提交: 由用户决定位移提交\n手动提交分为: 同步提交、异步提交 手动提交虽管理粒度更细, 但需消耗较多性能 commitSync()和commitAsync()方法的定义:\n// 同步提交 // timeout参数指定提交的超时时间 // offsets参数指定提交具体Partition的(默认提前所有Partition的Position) public void commitSync(); public void commitSync(Duration timeout); public void commitSync(final Map\u0026lt;TopicPartition, OffsetAndMetadata\u0026gt; offsets); public void commitSync(final Map\u0026lt;TopicPartition, OffsetAndMetadata\u0026gt; offsets, final Duration timeout); // 异步提交 // callback参数指定提交完成后(调用onComplete()方法之后)的回调方法 // offsets参数指定提交具体Partition的(默认提前所有Partition的Position) public void commitAsync(); public void commitAsync(OffsetCommitCallback callback); public void commitAsync(final Map\u0026lt;TopicPartition, OffsetAndMetadata\u0026gt; offsets, OffsetCommitCallback callback); 位移消费 位移消费(Seek): Consumer从指定位置处开始消费\nauto.offset.reset参数指定Consumer没有消费位移时如何消费 默认从Partition的末尾处开始(latest), 且位移越界也会触发该行为 seek()和其他相关方法的定义：\n// 设置/覆盖指定Partition下次拉取时的消费位移 // 若Consumer多次调用该方法, 则以最后次调用为准 // 必须在Poll()方法之后调用该方法(必须分配Partition后) public void seek(TopicPartition partition, long offset); // 返回Consumer分配到的所有Partition public Set\u0026lt;TopicPartition\u0026gt; assignment(); // 返回指定Partition集合的末尾消息位置(将要写入消息的位置) public Map\u0026lt;TopicPartition, Long\u0026gt; endOffsets(Collection\u0026lt;TopicPartition\u0026gt; partitions); public Map\u0026lt;TopicPartition, Long\u0026gt; endOffsets(Collection\u0026lt;TopicPartition\u0026gt; partitions, Duration timeout); // 返回指定Partition集合的起始处消息位置(还未清理的最早消息) public Map\u0026lt;TopicPartition, Long\u0026gt; beginningOffsets(Collection\u0026lt;TopicPartition\u0026gt; partitions); public Map\u0026lt;TopicPartition, Long\u0026gt; beginningOffsets(Collection\u0026lt;TopicPartition\u0026gt; partitions,Duration timeout); // 返回Partition集合中每个的消费位移, 其需大于等于设定的时间戳(最小的) public Map\u0026lt;TopicPartition, OffsetAndTimestamp\u0026gt; offsetsForTimes(Map\u0026lt;TopicPartition, Long\u0026gt; timestampsToSearch); public Map\u0026lt;TopicPartition, OffsetAndTimestamp\u0026gt; offsetsForTimes(Map\u0026lt;TopicPartition, Long\u0026gt; timestampsToSearch, Duration timeout); // seekToBegining()/seekToEnd()方法可直接设为起始处/末尾\n实现原理 Rebalance Rebalance(再均衡): 重新分配Partition所对应的Consumer\nRebalance期间消费者组内的Consumer不可拉取(消费者不可用) Partition被重新分配给新的Consumer时, 上个Consumer的状态会丢失 再均衡监听器(RebalanceListener): Rebalance发生前/后所就执行的操作 自动触发Rebalance的事件:\n消费者组增加/减少Consumer Topic的Partition数量发生变化 消费者组中的Consumer主动取消订阅 消费者组所对应的GroupCoordinator节点发生变化 再均衡监听器的定义:\npublic interface ConsumerRebalanceListener { // Rebalance之前和Consumer停止消费后调用 // partitions参数指定Rebalance前所分配到的Partition void onPartitionsRevoked(Collection\u0026lt;TopicPartition\u0026gt; partitions); // Rebalance之后和Consumer开始消费前调用 // partitions参数指定Rebalance后所分配到的Partition void onPartitionsAssigned(Collection\u0026lt;TopicPartition\u0026gt; partitions); } Rebalance的具体流程:\nFIND_COORDINATOR: 找到消费者组对应的GroupCoordinator所在的Broker, 并与之建立连接 JOIN_GROUP: 加入GroupCoordinator, 并配置相关信息(如: 心跳报文周期) SYNC_GROUP: GroupCoordinator同步由Consumer leader选举出的Partition分配策略 HEARTBEAT: Consumer确定offset并开始工作, 通过独立的线程周期性向GroupCoordinator发送心跳报文 组协调器(GroupCoordinator): 管理消费者组的组件(Kafka服务端)\n默认将首个加入消费者组的Consumer作为Consumer leader 根据Counsumer配置的Partition分配策略选举出消费者组的Partition分配策略(Consumer若不支持, 则抛出异常) // 消费者协调器(ConsumerCoordinator): 与GroupCoordinator交互的组件(Kafka客户端)\nConsumerInterceptor ConsumerInterceptor(拦截器): 拉取消息期间和位移提交前进行的操作\ninterceptor.classes参数指定Consumer使用的ConsumerInterceptor 可指定多个ConsumerInterceptor(拦截链按配置时顺序执行) ConsumerInterceptor的定义:\npublic interface ConsumerInterceptor\u0026lt;K, V\u0026gt; extends Configurable { // 拉取消息期间所进行的操作 // 若抛出异常, 则会被捕获并记录到日志中(不会向上传递) public ConsumerRecords\u0026lt;K, V\u0026gt; onConsume(ConsumerRecords\u0026lt;K, V\u0026gt; records); // 位移提交后所进行的操作(也可进行位移提交) public void onCommit(Map\u0026lt;TopicPartition, OffsetAndMetadata\u0026gt; offsets); // 关闭拦截器 public void close(); } DeSerializer DeSerializer(反序列化器): 将字节数组转换成特定数据结构\nConsumer使用的DeSerializer需和Producer使用的序列化器对应 Consumer指定DeSerializer时, 需通过全限定名方式指定(类的完整路径) DeSerializer的定义:\npublic interface Deserializer\u0026lt;T\u0026gt; extends Closeable { // 配置反序列化器 // 常用于指定编码类型(默认UTF-8) void configure(Map\u0026lt;String, ?\u0026gt; configs, boolean isKey); // 执行反序列化 // 若data参数为null, 则抛出异常 T deserialize(String topic, byte[] data); // 关闭序列化器 // 需保证幂等性 void close(); } // 不建议使用自定义Serializer或DeSerializer, 会增加耦合度\n多线程消费 Consumer默认是非线程安全\n通过acquire()和release()方法确保单线程(加锁和解锁) acquire()方法为轻量级锁实现(检查标记以检测是否发生并发操作) Consumer执行操作前都会调用acquire()方法(wakeup()方法例外) 消费线程 消费线程: 每个线程代表个Consumer\n消费线程可处理多个Partition(属于不同Topic) 若消费线程属于同一个消费者组, 则并发量受限于Partition数量 不建议让Partition对应多个消费线程, 需处理位移提交和顺序控制 如: 消费线程(不建议单独订阅Partition消费)\n处理线程 处理线程: Consumer对应多个线程处理线程进行消费\n相较于消费线程避免过多TCP连接的资源消耗和快速消费 该方式需解决消息的位移提交和顺序控制(可通过共享位移变量) 该方式还存在消息丢失的风险, 可通过滑动窗口解决(消费成功才移动) 如：处理线程\nTopic/Partition Topic/Partition: Kafka中消息管理的基础单位\nTopic和Partition并不实际存在(仅逻辑上的概念) 如: Topic和Partition关系\n// 每个日志文件可对应多个日志分段, 其还可分为索引、日志存储和快照等\nTopic Topic(主题): Kafka中消息归类单位\nTopic管理本质: 管理Topic对应的日志存储(文件) 日志存储随机分步于各个Broker以提搞Topic容灾性 日志存储数量 = Partition数量 * Replica数量 存储文件格式: Topic名-Partition名-序列号 // 可通过Kafka自带kafka-topics.sh脚本完成Topic相关管理\nTopic名称组成: 大小写字母、数字、点号、连接线、下划线\nTopic名称必须含有点号或下划线(metrics命名时会将前者替换为后者) 不建议使用双下划线作为前缀(其常为内部Topic格式) 创建Topic的本质(交由控制器异步完成) // ZooKeeper的/brokers/topics/和/config/topics/下创建子节点并写入Partition分配方案和配置信息\n管理Topic须知:\n创建Topic时Broker需统一是否配置机架信息, 否则会创建失败 Topic创建后仅能增加Partition数量(Partition不能被删除) Partition数量变化会影响Key的计算(影响消息顺序) Partition Partition(分区): 组成Topic的单位(实际存储消息)\nPartition可有多个副本(leader和follower), 每个副本对应个日志文件 leader提供读写服务, follower副本仅和leader进行数据同步 leader恢复后重新加入, 则只能为新的follower 优先副本: AR集合中首个副本\n理想情况下优先副本应是Partition的leader Kafka会确保所有Topic的优先副本在集群中均匀分布 Partition平衡: 通过选举策略使优先副本选举为leader副本 // 优先副本选举的元数据存储于ZooKeeper的/admin/preferred_replica_election\nPartition重分配: Partition重新进行合理的分配\n当Partition所处的Broker节点下线, Kafka不会自动进行故障转移 Kafka集群中增加新Broker节点时, 该节仅能分配到新创建的Partition 本质：部分Partition增加新副本, 并从剩余Partition的副本中拷贝数据 Partition重复配过程中需保证有足够的空间(完成后自动删除原有数据) // 建议分为多个小批次执行Partition重分配, 并重启预下线的Broker\nPartition数量与吞吐量关系:: 限定范围内增加Partition数量可增加吞吐量\n若无休止增加Partition数量, 超出限定范围后吞吐量反而下降 Partition数量有上限(过多会导致Kafka进程崩溃) Partition也是最小的并行操作单位 日志存储 日志(Log): Partition对应的物理存储\n日志以目录方式存储多个LogSegment 日志的目录命名格式: Topic名称-Partition名称 数据均以追加方式写入日志, 且以特定顺序进行追加 如: 日志存储关系\n// LogSegment还包含.deleted、.cleaned、.swap等后缀文件\nLogSegment(日志分段): 组成日志的基础单位\n每个LogSement必须有个日志文件和两个索引文件 日志的最后个LogSegment才可执行写入, 其他仅存储数据 BaseOffset(基准偏移量): 每个LogSegment中首个消息的偏移量 文件均以BaseOffset格式进行命名(固定为20位数字, 用0填充多余位) // BaseOffset是64位长整型数据, 其可得知前个LogSegment的数据量\n日志索引: 稀疏索引实现消息的快速检索\n稀疏索引达到指定大小后才建立索引(不保证Record均有对应的索引项) 稀疏索引通过MappedByteBuffer将索引文件映射到内层中 通过二分定位小于指定偏移量的最大偏移量 各索引均严格单调递增 存储格式 存储格式: 日志存储在硬盘的格式\n日志的存储格式决定其占用空间大小和检索速率 日志的存储格式演进为3个版本: v0(0.10.0)、v1(0.11.0)、v2 如: 日志存储格式\n// Varints(变长整型): 使用任意多个字节序列化记录整数(特定范围减少空间)\n消息压缩: 将RecordBatch压缩成单个Record\n压缩生成的消息记为外层消息(反者为内层消息) 外层消息的key为null, 而value为内层消息(偏移量查找) 内层消息的偏移量均从0开始(使用时Broker会进行转换计算) 如: 外层消息和内层消息的偏移量\n// 外层消息存储的是内层消息中最后条消息的绝对位移(相对于Partition而言)\n日志清理 日志清理: Kafka对日志的维护\n日志清理策略分为: 删除、压缩 日志清理的粒度最细可为Topic级别 可同时指定删除和压缩为日志清理的策略 删除 删除(Delete): 删除不符合特定条件的LogSegment\n删除依据分为: 时间、文件大小、日志的起始偏移量 Broker启动时会同时启动个线程周期性检测并删除特定LogSegment 删除线程会基于依据选择出可被删除的LogSegment(deletableSegment) 日志删除的大致流程：\n从日志对象中所维护的LogSegment跳跃表中移除待删除的LogSegment 将所有待删除的文件添加.deleted后缀(包括索引文件) 统一交由延迟删除线程处理(默认1m) 基于时间删除: 每个LogSegment拥有过期时间\n根据LogSegment的最大时间戳(最后条消息) 若最后条消息的时间戳字段小于0, 则根据最近修改时间 若所有LogSegment均满足删除条件, 则在删除前创建activeSegment 如: 基于时间的日志删除(只要最大时间戳未过期就不会被删除)\n基于文件大小: 每个LogSegment的限定大小\n基于文件大小又可分为：日志大小、LogSegment大小 若基于日志大小, 则超出限定时默认从头开始删除LogSegment 如：基于大小的日志删除\n基于日志的起始偏移量: 下个LogSegment的BaseOffset是否小/等于起始偏移量\n删除线程会逐个遍历LogSegment以判断BaseOffset是否满足 日志起始偏移量常为首个LogSegment的BaseOffset 如: 基于日志的起始偏移量(假设起始偏移量为25)\n压缩 压缩(Compact): 将具有相同Key的消息仅保留最后个版本的Value\n压缩后生成新的LogSegment, 消息的物理位置不会改变 压缩后的偏移量不再连续(不影响日志的检索) 压缩前后的消息可分为: clean和dirty activeLogSegment不参与压缩 如：日志压缩时其构成部分\n// 日志的cleaner-offset-checkpoint文件记录每个Partition的已清理偏移量\n日志压缩时大致流程:\n日志的污浊率触发压缩操作 压缩线程遍历两次日志(获取Key和判断) 对于压缩LogSegment的进行分组(防止过多小文件) 将LogSegment组中需保留消息存储于.clean后缀的临时文件 对日志进行压缩, 在压缩完成后将.clean临时文件后缀改为.swap 删除被压缩的LogSegment, 并将.swap后缀去除(变为可用LogSegment) // LogSegment组的大小不可超过LogSegment的限定大小\n如: 多次压缩的日志文件\n// ActiveSegment(活跃的日志分段): 可执行写入操作的LogSegment\n附录 配置参数 Broker 参数 默认值 说明 auto.create.topics.enable true Producer向不存在的Topic发送消息时, 是否自动创建该Topic\n(不建议开启, 其会增加Topic的管理和维护难度) auto.leader.rebalance.enabl true 是否启用自动Partition平衡\n通过定时任务轮询所有Broker, 并计算其Partition不平衡率\n判断不平衡率是否超出设定值, 超出则执行优先副本选举以Partition平衡\n(不建议开启，存在阻塞风险) background.threads 10 指定后台执行任务的线程数 broker.id 指定Broker运行时的唯一标识\n(多个配置文件中的该值不同时会抛出异常) broker.rack Broker部署所在的OS节点 compression.type producer 数据的压缩方式\n(可设为: gzip、snappy、lz4、uncompressed(不压缩数据)) delete.topic.enable true Topic是否可删除\n(内部Topic不可删除) default.replication.factor 1 自动创建Topic时的副本数 follower.replication.throttled.rate follower副本的消息同步速度 leader.imbalance.check.interval.seconds 300s 自动Partition平衡的定时任务轮询周期 leader.imbalance.per.broker.percentage 10% Broker节点中不平衡率界限 leader.replication.throttled.rate leader副本的消息传输速度 num.partitions 1 自动创建Topic时的Partition数量 Log相关的常用配置参数:\n参数 默认值 说明 log.cleaner.min.cleanable.ratio 0.5 日志清理策略为压缩时, 触发执行压缩的污浊率\n污浊率计算公式: dirty LogSegment / (clean LogSegment + dirty LogSegment) log.cleaner.min.compaction.log.ms 0 日志清理策略为压缩时, 消息的保留时间 log.cleaner.thread 1 日志清理策略为压缩时, 压缩线程数量 log.cleanup.policy delete 日志清理策略\n(可设为: compact、\u0026ldquo;delete,compact\u0026rdquo;) file.delete.delay.ms 60000 日志清理策略为删除时, 其延迟删除线程的等待时间 log.dir\nlog.dirs 日志存储目录 log.index.size.max.bytes 10485760 偏移量索引文件活时间戳索引文件的最大值\n超出该值则进行日志分段(生成新的LogSegment) log.message.timestamp.type CreateTime 消息的时间戳类型\n(可设为: LogAppendTime) log.rentention.bytes -1 日志清理策略为删除时, 日志文件的限定大小(所有LogSegment) log.retention.check.interval.ms 300000 日志清理策略为删除时, 删除线程的检测周期 log.retention.hours 日志清理策略为删除时, 日志的过期时间(时)\n优先级: log.retention.hours \u0026lt; log.retention.minutes \u0026lt; log.rentention.ms log.retention.minutes 日志清理策略为删除时, 日志的过期时间(分) log.rentention.ms 日志清理策略为删除时, 日志的过期时间(秒) log.roll.ms 168 LogSegment与当前系统时间戳的最大差值\n超出该值则进行日志分段(生成新的LogSegment)\n只有活跃的LogSegment才会为该参数的大小(其他的为实际占用大小)\n参数值必须是8的整数倍(满足索引文件是索引项的整数倍) log.segment.bytes 1073741824 日志文件切割为LogSegment的界限\n(生成新的LogSegment) Topic Topic相关的常用配置参数(在Broker层面都有对应的参数作为默认值):\n参数 Broker参数 默认值 说明 cleanup.policy log.cleanup.policy delete 日志压缩策略\n(可设为: compact、\u0026ldquo;delete,compact\u0026rdquo;) compression.type compression.type producer 消息的压缩类型\n(可设为: gzip、snappy、lz4、uncompressed(不压缩数据)) delete.retention.ms 86400000 标识为删除的数据保留时间 log.cleaner.delete.retention.ms file.delete.delay.ms log.segment.delete.delay.ms 60000 清理文件前的等待时间 flush.messages log.flush.interval.messages Long.MAX_VALUE 消息多少数据量可进行落盘\n(由OS决定, 不建议修改) flush.ms log.flush.interaval.ms Long.MAX_VALUE 消息落盘前等待时间\n(由OS决定, 不建议修改) follower.replication.throttled.replicas follower.replication.throttled.replicas 被限制速率的Topic所对应的follower副本列表 index.interval.bytes log.index.interval.bytes 4096 添加索引项的频率 leader.replication.throttled.replicas leader.replication.throttled.replicas 被限制速率的Topic所对应的leader副本列表 max.message.bytes message.max.bytes 1000012 消息的最大字节数 message.format.version log.message.format.version 2.0-IV1 消息格式的最大版本 message.timestamp.difference.max.ms Long.MAX_VALUE log.message.timestamp.difference.max.ms 消息与Broker之间时间戳相差的最大值\n(仅在timestamp.type参数为CreateTime时才可设定该参数) message.timestamp.type log.message.timestamp.type CreateTime 消息的时间戳类型 min.cleanable.dirty.ratio log.cleaner.min.cleanable.ratio 0.5 日志清理时的最小污浊率 min.compaction.lag.ms log.cleaner.min.compaction.log.ms 0 日志被清理前的最小保留时间 min.insync.replicas log.insync.replicas 1 Partition的ISR集合中最小副本数 preallocate log.preallocate false 创建日志分段是否预分配空间 retention.bytes log.retention.bytes -1 Partition所能保留的消息总量 retention.ms log.retention.ms 604800000 delete的清理策略的日志被清理后能够保留的时间 segment.bytes log.segment.bytes 1073741824 日志分段的最大值 segment.index.bytes log.index.size.max.bytes 10485760 日志分段索引的最大值 segment.jitter.ms log.roll.jitter.ms 0 滚动日志分段时在segment.ms基础上增加的随机数 segment.ms log.roll.ms 604800000 日志分段滚动周期 unclean.leader.election.enable unclean.leader.election.enable false 是否可从非ISR集合中选举leader副本 Producer 参数 默认值 说明 bootstrap.servers 引导程序的服务地址\n格式: 地址1:端口1,地址N:端口N\n(建议指定两个以上的Broker地址以保证稳定性, 且使用主机名形式) key.serializer 发送时对Key调用的序列化器\nBroker仅能接受字节数组形式的消息byte[] value.serializer 发送时对Value调用的序列化器\nBroker仅能接受字节数组形式的消息byte[] acks 1 Partition中须多少个副本接收到ProducerRecord才视为写入\n1: 仅leader副本接收成功即可\n0: 无需任何副本接收成功验证\n-1: ISR中所有副本都接收成功才可 batch.size RecordAccumulator中BufferPool复用缓存的最大的ByteBuffer\n(超出该限定的ByteBuffer在申请使用后直接释放) buffer.memory 33554432B Producer的RecordAccumulator的大小 client.id Producer的ID\n(未指定时随机生成个非空字符串) compression.type none ProducerRecord的压缩方式 connections.amx.idle.ms 540000 闲置连接的最大存活时间 enable.idempotence false 是否开启幂等\n(开启时, akcs参数必须为-1) interceptor.classes 发送ProducerRecord时使用的ProducerInterceptor\n可指定多个ProducerInterceptor形成拦截链(拦截链按配置时顺序执行) linger.ms 0 ProducerBatch发送之前的等待时间\n(若ProducerBatch达到指定时间前已被填满, 则也会直接发送) max.block.ms 60000 Producer的发送消息的最大阻塞时间 max.in.flight.requests.per.connection 5 发送请求的最大缓存数(发送后最多等待的数量) max.request.size 1048576B 限定发送ProducerRecord的最大值\n(不建议修改该参数, 可能导致未知的异常) metadata.max.age.ms 30000 获取的Broker元数据过期时间\n(超出该时间则向leastLoadedNode发送MetadataRequest请求) partitioner.class 发送ProducerRecord时使用的Partitioner request.timeout.ms 30000 Producer发送请求后等待的超时时间\n超出该事件, 则根据retries参数进行重试\n该参数值需大于Broker的replica.lag.time.max.ms参数值 retries 0 发送ProducerRecord失败时重试的次数\n仅在发生可重试异常时进行重试\n若超出指定重试次数后仍失败, 则放弃重试并返回异常\n若该参数非0且max.in.flight.requests.per.connection参数大于1, 会导致错序 retry.backoff.ms 100 每次重试发送ProducerRecord的时间间隔 receive.buffer.bytes 32768B Socket接收ProducerRecord的大小 send.buffer.bytes 131072B Socket发送ProducerRecord的大小 transactional.id 事务ID Consumer 参数 默认值 说明 bootstrap.servers 引导程序的服务地址\n格式: 地址1:端口1,地址N:端口N\n(建议指定两个以上的Broker地址以保证稳定性, 且使用主机名形式) group.id Consumer所属消费者组 key.derializer 消费时对Key调用的反序列化器\nBroker仅能接受字节数组形式的消息byte[] value.derializer 消费时对Value调用的反序列化器\nBroker仅能接受字节数组形式的消息byte[] auto.offset.reset latest Consumer没有指定消费位移时如何开始消费(位移越界也会触发)\nearliest: 从起始处开始\nnone: 直接抛出异常 client.id Consumer的ID\n(未指定时随机生成个非空字符串) connections.max.idle.ms 540000ms Consumer闲置多长时间后关闭 enable.auto.commit true 是否开启自动位移提交\n默认5s提交次Partition中最大的消费位移\n自动位移提交存在着重复消费和消息丢失的情景\n每次拉取之间也会检查次是否可提交, 满足则先提交再拉取 exclude.internal.topics true Consumer是否可访问内部Topic\n(内部Topic不可使用正则匹配方式订阅, 必须通过集合方式才可订阅) fetch.min.bytes 1B 每次拉取消息的最小数据量\n(可拉取的数据量不满足时, 则拉取动作将阻塞等待) fetch.max.bytes 52428800B 每次拉取消息的最大数据量(软限制) fetch.max.wait.ms 500ms 消息不满足最小数据量时等待的超时时间 heartbeat.interval.ms 3000 消费者组判断Consumer活跃的间隔\n(必须小于sessio.timeout.ms参数) interceptor.classes 使用的ConsumerInterceptor isolation.level read_uncommitted Consumer的事务隔离级别\nread_uncommitted: 未提交的事务可见(消费到HW)\nread_committed: 忽略未提交的事务(消费到LSO) max.partition.fetch.bytes 1048576B 从Partition中拉取消息的最大数据量(软限制) max.poll.records 500 每次拉取的最多消息条数 max.poll.interval.ms 300000 消费者组中Consumer的最大空闲时间 metadata.max.age.ms 30000ms 元数据的过期时间 partition.assignment.strategy Topic的Partition分配策略\n(可设为: RangeAssignor、RoundRobinAssignor、StickyAssignor) receive.buffer.bytes 65536B Socket接收缓冲区的大小 reconnect.backoff.ms 50ms Consumer连接Broker失败后的等待时间 request.timeout.ms 30000ms Consumer等待请求响应的最长时间 retry.backoff.ms 100ms 重新发送失败请求到Partition的等待时间 send.buffer.bytes 131072B Socket发送缓冲区大小 sessio.timeout.ms 10000 消费者组中Consumer判为离开的超时时间 ","permalink":"https://zhoujze.github.io/en/posts/tech/kafka/","summary":"Kafka Kafka: ZooKeeper协调的分布式消息系统 基于Scala语言编写的高性能、多分区、多副本 Kafka高性能的原因：页缓存、顺序IO、零拷贝 具有以下特性： 消息中间件: 系统解耦、冗余存储、流量消峰、异步通信等 存储系统: 通过消息持久化和多副本机制实现消息落盘 流处理: 为流式处理框架提供可靠","title":"Kafka"},{"content":"基础 OSI七层模型和TCP/IP协议 TCP/IP协议-应用层 应用层包括所有和应用程序协同工作，并利用基础网络进行业务数据交换的协议。该层协议所提供的服务能直接支持用户应用。\n协议包括：\nHTTP（万维网服务-超文本传输协议） FTP（文件传输） SMTP（电子邮件） SSH（安全远程登陆） DNS（域名解析） 等等\u0026hellip; TCP/IP协议-传输层 传输层协议，解决了端对端进程通讯问题，能确保数据可靠的到达目的地。\n主要功能：\n端对端连接提供传输服务 传输服务分为可靠（TCP）和不可靠（UDP） 为端对端连接，提供流量控制、差值控制等管理服务 TCP传输控制协议和UDP用户数据报协议\nTCP：面向连接的、可靠的传输协议，提供可靠的字节流，确保数据完整、无损并按顺序到达，TCP尽量连续不断的测试网络负载，并指发送数据的速度，以确保不会网络过载。TCP试图按照规定顺序发送。 UDP：无连接的数据报协议，是尽力传输和不可靠协议，不会对数据是否已经到达进行检查，也不保证数据报按顺序到达。 TCP传输效率低，可靠；UPD传输效率高，不可靠，适用于传输可靠性要求不高、体量小的数据（QQ聊天数据）\nTCP/IP协议-网络层 为要发送的数据找到一个合适的路径传输。还可实现拥堵控制，网际互连等。\n协议有：\nICMP IP IGMP 等等\u0026hellip; TCP/IP协议-链路层 也称数据链路层，用来处理连接网络的硬件部分。包括操作系统硬件的设备驱动、NIC（网卡）、光纤等物理可见部分，还包括连接器等一切传输媒介。\n在这一层，数据的传输单位为比特。\n主要协议：\nARP RARP 等\u0026hellip; HTTP报文传输 原理 利用TCP/IP进行网络通信时，数据包会按照分层顺序与对方进行通信。发送端从应用层往下走，接收端从链路层往上走。从客户端到服务器的数据，每一帧数据的传输的顺序都为：应用层-\u0026gt;传输层-\u0026gt;网络层-\u0026gt;链路层-\u0026gt;链路层-\u0026gt;网络层-\u0026gt;传输层-\u0026gt;应用层\n过程 数据的封装和分用 数据通过互联网传输，需要加上特定的标识，加标识的过程叫做数据的封装。使用数据的时候去掉标识，这个过程叫分用。\n在传输过程中，数据报文会在不同的物理网络之间传递。以一个HTTP请求的传输为例，请求在不同物理网络之间的传输过程，大致如下图所示：\n数据传输和交换的方式 数据传输方式 按数据传输的流向和时间关系分类 单工通信：数据沿一个方向传输，发送和接收方固定。类比广播。 半双工通信：可以双向传输，但是不能同时进行。类比对讲机。 全双工通信：可以双向传输，可以同时进行。类比打电话。 按数据传输的顺序分类 串行传输：以串行方式在一条信道传输，易于实现，缺于要解决同步问题。 并行传输：在多条信道上并行传输，一条信道一个字符，天然实现同步。缺点传输信道多，设备复杂，成本高，一般不采用。 按数据传输的同步方式分类 同步传输：数据没有被对方确认接收就一直等待，确认了才返回。 异步传输：发送数据后立马返回，接收时，有数据，则接收方会收到消息。 数据交换方式 数据交换指在多个数据终端设备之间，为任意两个终端设备建立数据通信临时互连通路的过程。\n电路交换 用户之间要传输数据时，交换中心在用户之间建立一条暂时的数据电路。电路接通后，用户双方便可传输数据，并一直占用到传输完毕拆除电路为止。\n各层的作用 网络层本身没有数据传输功能，数据实际委托给数据链路层传输，网络层起指导作用，指导数据链路层该怎么走\n物理层-集线器 无脑将电信号转发到所有出口，不做任何处理\nA发送数据包时，只需在数据头部拼接源MAC地址和目的MAC地址。\nB收到数据包后，根据头部MAC地址信息判断是否是发给自己的。是则留下，不是则丢弃。\n数据链路层-交换机 把集线器智能化，只发给目标MAC地址指向的电脑\n交换机利用内部维护的MAC地址表，判断连接在哪个端口。实现只发给目标MAC地址指向的电脑。\n通过这样传输方式组成的小范围网络，称以太网\nMAC地址在初始的时候是空的，通过网络中机器的不断通信，将MAC表建立完毕。\n网络层-路由器 作为一台拥有独立MAC地址的设备，并且可以帮我们把数据包做一次转发。\n路由器的每个端口，都有独立的MAC地址。\n通过IP地址和MAC地址来精准的发送给指定设备\n路由器需要知道收到的这个数据包，该从自己的哪个端口走，才能到目的地。——路由表\n数据包\n补充 上面说IP层需要IP地址，然后数据包发送需要数据链路层知道MAC地址。如果只知道IP怎么办？\n在每个电脑里都有一个arp缓存表，记录着ip地址与MAC地址的关系\n一开始表是空的，电脑A为了知道电脑B的MAC地址，将会广播一条arp请求，B收到请求，带上自己的MAC地址给A一个响应，A更新字的arp缓存表。\n通过不断的广播arp请求，完善arp缓存表。\n从各个节点视角看整个过程\n电脑视角：\n首先我要知道自己的IP和对方IP 通过子网掩码判断是否在同一子网 在则通过arp获取对方MAC地址，然后扔过去 不在则通过arp获取网关MAC地址，然后扔过去 交换机视角：\n我收到的数据包必须有目标MAC地址 通过MAC地址查映射关系 查到了就按映射关系从我指定端口扔出去 查不到就向所有端口扔出去 路由器视角：\n我收到的数据包必须有目标IP地址 通过路由表查映射关系 查到了就按映射关系从我指定的端口扔出去 查不到则返回一个路由不可达的数据包 数据链路层 什么是MAC地址 MAC地址称为链路地址、物理地址\n由网络设备制造商生产时烧录在网卡的 EPROM（一种闪存芯片，通常可以通过程序擦写）\n其中前 24 位（00-16-EA）代表网络硬件制造商的编号，后 24 位（AE-3C-40）是该厂家自己分配的，一般表示系列号。\n一台主机拥有多少个网络适配器（网卡）就有多少个 MAC 地址。\n网络层 IP协议 \u0026hellip;\nARP地址解析协议 什么是ARP ARP地址解析协议，实现IP地址到MAC地址的转换 ARP工作原理 工作原理：借助ARP请求和ARP响应两种类型的包确定MAC地址。\n每个主机都有一个ARP高速缓存，里面记录了本局域网下各主机和路由器的IP地址到MAC地址的映射表。\n举例：\n有主机A向局域网下主机B发送IP数据包，已知主机A和主机B的IP，他们互相不知道MAC地址。\n主机A查询自己的缓存里有没有主机B的MAC地址，有直接发送，无进入下一步 主机A把自己的IP地址MAC地址和目标的IP地址一起放在ARP请求包中，向局域网内所有主机广播 主机B收到ARP请求包，查看目标IP地址是自己，然后把自己的MAC地址放入ARP响应包，发送给主机A 主机A收到来自主机B的ARP响应包，向自己的ARP高速缓存中写入主机B的IP地址到MAC地址的映射 ICMP网际控制报文协议 什么是ICMP IP协议是一种不可靠数据交付的网络协议，分组的IP地址，在传输过程中可能发生丢失、重复、延迟、乱序等情况，但是IP协议对这些情况并没有有效的检测和补救。更不会通知接收双方。\n针对这种情况，ICMP网际控制报文协议，有两个功能。\n对IP数据报确认是否成功送达目标 如果某个IP数据报因为某种原因未正常到达，由ICMP通知接收双方具体的原因。 ICMP工作原理 ICMP协议通过发送ICMP报文来通知具体的出错原因。\nICMP报文是由路由器发送。\n举例：\n如图，主机A和B不在同一局域网下，主机B在休眠。此时主机A向主机B发送数据包。\n为了保持数据包的精准发送，我们要同时知道IP地址和MAC地址，路由器2广播ARP请求报文找主机B，多次广播后，无响应。\n路由器发送ICMP报文给主机A，告知主机A发送给主机B的数据包未能成功到达。\nICMP报文类型 ICMP报文类型由ICMP头部4位的字段类型决定，分为\n查询报文 差错报文 查询报文 ","permalink":"https://zhoujze.github.io/en/posts/tech/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E8%B7%AF/","summary":"基础 OSI七层模型和TCP/IP协议 TCP/IP协议-应用层 应用层包括所有和应用程序协同工作，并利用基础网络进行业务数据交换的协议。该层协议所提供的服务能直接支持用户应用。 协议包括： HTTP（万维网服务-超文本传输协议） FTP（文件传输） SMTP（电子邮件） SSH（安全远程登陆）","title":"计算机网络"},{"content":"Spring 面试题 什么是Spring spring是一个轻量级的Java开发框架，用于解决企业级应用开发的复杂性，简化Java开发。\nspring底层有两个核心特性：\nAOP面向切面编程 DI依赖注入 降低开发复杂度的四个策略：\n基于POJO的轻量级和最小侵入性编程 通过依赖注入和切面编程实现松散耦合 基于切面和惯例进行的声明式编程 通过切面和模板减少样式代码 Spring的设计目标、设计理念、核心 设计目标 为开发者提供一站式的轻量级应用开发平台\n设计理念 在JavaEE的开发中，支持POJO和JavaBean开发方式，是应用面向接口开发，充分支持面向对象设计方法。\nSpring通过IoC容器实现对象耦合关系的管理，并实现依赖反转，将对象之间的依赖关系交由IoC容器管理，实现解耦。\n核心 IoC容器和AOP模块。\nIoC容器用来管理对象之间的耦合关系，通过AOP以动态非侵入的方式增强服务。\nIoC容器让相互协作的组件实现松散耦合，AOP面向切面编程允许吧应用于各个层的功能分离出来复用。\nSpring的优缺点 优点 方便解耦，简化开发\nspring是个大工厂，可以把对象的创建和依赖关系的维护交给Spring进行管理。\nAOP切面编程\n方便对程序的权限拦截、运行监控等。\n声明式事务的支持\n通过配置就可以完成对事物的管理。\n方便测试\nSpring支持Junit4，可通过注解实现对Spring程序的测试。\n集成优秀框架\nSpring提供了对各种优秀框架的直接支持，如MyBatis等。\n提供JavaEE开发中难用的API的封装，降低JavaEE API使用难度\n如JDBC、远程调用等，\n缺点 Spring是轻量级框架，但是给人感觉大而全 Spring依赖反射，反射影响性能 使用门槛高，不易上手。 Spring的应用场景 JavaEE企业级应用开发，包括：SSH、SSM等。\nSpring的价值 Spring是非侵入式框架，使代码最低限度的依赖框架 Spring提供一致性编程模型，使应用直接使用POJO开发，与运行环境隔离 Spring推动面向对象和面向接口编程风格，提高代码复用率和可测性 Spring的组成 Core：提供框架基本支持，包括控制反转和依赖注入功能。 Beans：提供BeanFactory，是工厂模式的实现，Spring把管理对象称为Bean。 context；基于core构建的封装包，提供框架式的对象访问方法。 JDBC：提供JDBC的抽象类，简化jdbc。 AOP：提供面向切面的代码实现，可自定义拦截器、切点等。 Web：提供针对Web开发的集成特性。 Test：提供测试支持，支持Junit，进行单元测试和集成测试。 Spring使用到的5种设计模式 工厂模式：BeanFactory就是一种简单的工厂模式，用来创建实例对象。 单例模式：Bean默认就是单例模式。 代理模式：AOP用到了JDK的动态代理和CGLIB字节码生成技术。 模板方法：解决代码重复问题。 观察者模式：定义对象键的一种一对多的依赖管理，当对象发生改变，所有依赖于它的对象都要制动更新，如Spring中的Listener的实现ApplicationListener。 介绍下核心容器 ( spring context 应用上下文 ) 是最基本的Spring模块，提供Spring框架的基础功能。\nBeanFactory是任何以Spring为基础的核心，Spring框架建立在此模块之上，使Spring成为一个容器。\nBean工厂是工厂模式的一个实现，提供控制反转功能，把应用的配置和依赖从代码中分离开来，最常用的是XML文件中定义加载bean，该容器从xml文件中读取配置，并创建一个完全配置的系统或应用。\nSpring中的5种标准事件 上下文更新事件\n调用ConfigurableApplicationContext接口中的refresh()方法触发。\n上下文开始事件\n调用ConfigurableApplicationContext接口中的start()方法触发。\n上下文停止事件\n调用ConfigurableApplicationContext接口中的stop()方法触发。\n上下文关闭事件\nApplicationContext被关闭时触发。容器被关闭，所有Bean销毁。\n请求处理事件\nWeb应用中，当http请求结束时触发。\nSpring中的5大组件 接口 -\u0026gt; 定义功能 Bean类 -\u0026gt; 实体类，get、set方法 Bean配置文件 -\u0026gt; 包含类的信息和配置 AOP -\u0026gt; 提供面向切面编程功能 应用程序 -\u0026gt; 使用接口 使用Spring的四大方式 一个成熟的Spring Web应用程序 作为第三方Web框架 作为企业级Java Bean，包装现有的POJO 用于远程 Spring 控制反转 ( IoC Inversion Of Control) 什么是IoC容器 控制反转即使IoC，它把传统上由程序代码直接控制的对象的调用权交给容器管理，通过容器实现对象组件的装配和管理。\n控制反转，通俗说，就是对组件对象控制权的转移，从程序本身转移到外部容器。\nIOC负责创建对象，管理对象(通过依赖注入)，装配对象，配置对象，并且管理对象的生命周期。\n控制反转的作用 管理对象的创建和依赖关系的维护。 解耦，由容器去维护具体对象。 托管类的产生过程，在类的产生过程中做一些处理，可以直接托管处理，最直接例子，代理。 优点 IoC把应用的代码降到最低 使应用容易测试，单元测试不再需要单例 最小代价和最小侵入性使松散耦合得以实现 IoC容器支持加载服务时的饿汉式初始化和懒加载 IoC实现机制 实现原理：工厂模式+反射机制\ninterface Fruit { public abstract void eat(); } class Apple implements Fruit { public void eat(){ System.out.println(\u0026#34;Apple\u0026#34;); } } class Orange implements Fruit { public void eat(){ System.out.println(\u0026#34;Orange\u0026#34;); } } class Factory { public static Fruit getInstance(String ClassName) { Fruit f=null; try { f=(Fruit)Class.forName(ClassName).newInstance(); } catch (Exception e) { e.printStackTrace(); } return f; } } class Client { public static void main(String[] a) { Fruit f=Factory.getInstance(\u0026#34;io.github.dunwu.spring.Apple\u0026#34;); if(f!=null){ f.eat(); } } } IoC 支持 6种 功能 依赖注入 依赖检查 自动装配 支持集合 指定初始化方法和销毁方法 支持回调某些方法（但是需要实现Spring接口，略有侵入） BeanFactory 和 ApplicationContext 区别 BeanFactory 和 ApplicationContext 是Spring的两大核心，都可当成容器。\nApplicationContext 是 BeanFactory 的子接口。\n依赖关系 BeanFactory : 是Spring的最低层接口，包含了Bean的定义，读取Bean配置，管理Bean的加载、实例化，控制Bean的生命周期，维护Bean之间的依赖关系。 ApplicationContext ：是BeanFactory的派生，拥有它所有的功能外，还有更完整的框架功能： 继承MessageSource 因此支持国际化 统一的资源文件访问方式 提供在监听器中注册bean的事件 同时加载多个配置文件 载入多个（有继承关系）上下文，使每个上下文都专注于一个特定的层次，如应用的web层。 加载方式 BeanFactory : 采用延迟加载来注入Bean，当使用到的时候才对Bean进行加载实例化。这样如果Spring配置有问题，那么就不会在启动时发现，直到第一次调用getBean()才会抛出错误。 ApplicationContext ：容器启动，一次性加载所有的Bean，如果Spring配置错误，可以直接发现。因为ApplicationContext启动后预加载的所有的Bean，所以在需要用的时候直接调用就可以了不用等待。 相对于BeanFactory，ApplicationContext唯一不足：占用内存空间，因实例较多的Bean，启动较慢。 创建方式 BeanFactory : 编程式创建 ApplicationContext ：编程式和声明的方式创建(ContextLoader) 注册方式 都支持BeanPostProcessor、BeanFactoryPostProcessor\nBeanFactory : 手动注册 ApplicationContext ：自动注册 Spring如何设计容器，BeanFactory 和 ApplicationContext关系 Spring 作者用两个接口表示容器\nBeanFactory ApplicationContext BeanFactory简单粗暴，称为低级容器，可以理解为是一个HashMap，Key式BeanName，Value是Bean实例。通常只提供注册（put）和获取（get），两个功能。\nApplicationContext称为高级容器，因比BeanFactory多了更多的功能，继承了多个接口。如该接口定义了一个refresh方法，刷新整个容器，重新加载刷新Bean。\n低级容器实现Spring：\n加载配置文件，解析成BeanDefinition放在Map里 调用getBean，从BeanDefinition的Map里拿到Class对象实例化，同时如果有依赖关系，递归调用getBean方法，完成依赖注入 高级容器实现Spring：\n当执行refresh方法，刷新整个容器的Bean。\n它不仅仅是IoC，同时还支持不同信息源头，BeanFactory工具类，层级容器，支持访问文件资源，支持事物发布，接口回调。\nApplicationContext通常的实现 FileSystemXmlApplicationContext：从xml文件中加载bean，xml文件的全路径名必须提供构造函数。 ClassPathXmlApplicationContext：从xml文件中加载bean，正确设置classpath，容器会在classpath里找bean配置。 WebXmlApplicationContext：从xml文件中加载bean，此文件定义了Web应用所有的bean。 依赖注入 什么是依赖注入 IoC可以用不同的方式实现，主要由依赖注入和依赖查找。\n依赖注入就是组件之间的依赖关系由容器来管理，在系统运行期间，由容器将某种依赖关系的目标对象注入到组件中，组件不负责查询，只提供普通的Java方法让容器决定依赖关系。\n依赖注入原则 组件不负责查找资源或者依赖关系的对象，配置对象的工作交给IoC容器负责，查找逻辑从组件中分离到IoC容器负责。容器负责组件的装配，把符合依赖关系的对象通过属性或构造器传给需要的对象。\n依赖注入优势 查找定位操作与应用代码无关 不依赖容器API，可以在容器外使用 不需要特殊接口，绝大多数对象可以做到完全不依赖容器 依赖注入的不同类型 接口注入：因灵活性和易用性较差，Spring4被遗弃。\nSetter方法注入：通过容器触发一个类的构造器实现。\n构造器注入：容器调用无参构造器或者无参static工厂方法实例化bean后，调用bean的setter方法。\n构造器注入和Setter注入区别 构造器注入 setter注入 没有部分注入 有部分注入 不会覆盖setter属性 会覆盖setter属性 任意修改都会创建一个新实例 任意修改不会创建新实例 适用于设置很多属性 适用于设置少量属性 Spring 面向切面编程 ( AOP ) 什么是AOP OOP面向对象编程，允许开发者定义纵向关系，并且适用于定义横向关系，导致了大量代码重复，不利于各模块重用。\nAOP面向切面编程，可以认为是OOP的补充，把与业务无关，却对多个对象产生影响的公共行为和逻辑，抽取并封装成可重用的模块，模块被明明为切面(Aspect)，减少代码重复，降低耦合度，提高系统可维护性，可用于权限认证、日志、事物处理等。\nSpring AOP 和 AspectJ AOP 区别，AOP的实现方法 AOP的实现在于代理模式，主要分为动态代理和静态代理。\nAspectJ AOP：静态代理的增强，即AOP框架会在编译时生成AOP代理类，编译时增强，在运行时就时增强后的AOP对象 Spring AOP：使用动态代理，AOP框架不会在编译的时候生成文件，而是在每次运行的时候在内存中生成临时的AOP对象，其中包含了目标对象的全部方法，在特定切点做增强处理。 JDK动态代理和CGLIB动态代理区别 Spring有两种动态代理方式，JDK动态代理和CGLIB动态代理\nJDK动态代理：只提供接口实现类，不提供类的代理 CGLIB动态代理：提供类的代理，主要针对类的生成子类，覆盖其中的方法进行增强，因为采用继承，所以最好不要声明final，无法继承。 Spring如何选择使用哪种\nbean实现接口，使用JDK bean没有实现接口，使用CGLIB 可以通过注解强行使用CGLIB @EnableAspectJAutoProxy(proxyTargetClass = true)\nSpring术语解释 通知：定义什么时候，做了什么 连接点：程序执行过程中能够执行通知的所有点（方法！） 切点：定义在哪里切入，得到通知。切入点一定是连接点 切面：通知和切点结合。定义了是什么、什么时候做、在哪里 织入：把切面应用到目标对象上，并创建新的代理的过程。分为编译织入、类加载期织入、运行期织入 AOP 5类 通知 前置通知（Before）：在方法执行前通知 后置通知（After）：在方法执行完成后通知 返回通知（AfterReturning）：在方法执行完后，成功执行返回通知 异常通知（AfterThrowing）：在方法执行完成，方法抛出异常返回通知 环绕通知（Around）：在被通知的方法调用之前或者调用之后通知 AOP示例 定义连接点 @RestController public class AopController { @RequestMapping(\u0026#34;/hello\u0026#34;) public String sayHello(){ System.out.println(\u0026#34;hello\u0026#34;); return \u0026#34;hello\u0026#34;; } } 定义切面（切点+通知） 添加@Aspect注解 @Pointcut在一个切面内定义可重用的切点 @Aspect @Component public class AopAdvice { @Pointcut(\u0026#34;execution (* com.veal.aop.controller.*.*(..))\u0026#34;) public void test() { } @Before(\u0026#34;test()\u0026#34;) public void beforeAdvice() { System.out.println(\u0026#34;beforeAdvice...\u0026#34;); } @After(\u0026#34;test()\u0026#34;) public void afterAdvice() { System.out.println(\u0026#34;afterAdvice...\u0026#34;); } @Around(\u0026#34;test()\u0026#34;) public void aroundAdvice(ProceedingJoinPoint proceedingJoinPoint) { System.out.println(\u0026#34;before\u0026#34;); try { proceedingJoinPoint.proceed(); } catch (Throwable t) { t.printStackTrace(); } System.out.println(\u0026#34;after\u0026#34;); } } Spring Beans 什么时Spring Bean 交给Spring管理的对象，储存在Spring IoC容器中，由IoC容器初始化、装配和管理，通过配置中的元数据创建，如xml文件、注解、Java配置类等。\n一个Spring Bean的定义包含什么 创建bean 生命周期 依赖 如何提供配置元数据 xml配置文件 注解配置 Java配置类 创建 Spring Bean 4种方法 构造函数创建 @Configuration public class MyConfiguration { @Bean public MyBean myBean() { return new MyBean(); // 使用默认的无参构造函数创建 Bean 实例 } @Bean public AnotherBean anotherBean() { return new AnotherBean(myBean()); // 通过构造函数注入依赖的 Bean } } 静态工厂创建 @Configuration public class MyConfiguration { @Bean public MyBean myBean() { return MyBeanFactory.createMyBean(); // 调用工厂方法创建 Bean 实例 } } public class MyBeanFactory { public static MyBean createMyBean() { return new MyBean(); // 工厂方法创建 Bean 实例 } } 实例工厂创建 @Configuration public class MyConfiguration { @Bean public MyBeanFactory myBeanFactory() { return new MyBeanFactory(); // 实例工厂方法 } @Bean public MyBean myBean() { return myBeanFactory().createMyBean(); // 调用实例工厂方法创建 Bean 实例 } } public class MyBeanFactory { public MyBean createMyBean() { return new MyBean(); // 实例工厂方法创建 Bean 实例 } } 注解创建 使用@Component注解或者衍生@Service @Repository @Controller 标记类，让Spring自动扫描、创建bean实例。\nComponent：通用注解，可标记任意类为Spring的组件。如果不知道属于哪一层可以用。 @Service：服务类，针对复杂的逻辑，需要涉及Dao层。 @Repository：对Dao层，主要针对数据库相关操作。 @Controller：对应MVC控制层，用户接收请求调用Service层返回数据给前端。 @Component public class MyBean { // Bean 的实现 } @Component和@Bean区别 @Component作用于类，@Bean作用于方法\n@Component：通过类路径扫面自动装配到Spring容器 @Bean：在标有该注解的地方标记生成的Bean，告诉Spring这是某个类的实例，需要用到的时候还给我。 @Bean 比 @Component 自定义性更强。 示例： @Configuration public class AppConfig { @Bean public TransferService transferService() { return new TransferServiceImpl(); } } 相当于 \u0026lt;beans\u0026gt; \u0026lt;bean id=\u0026#34;transferService\u0026#34; class=\u0026#34;com.acme.TransferServiceImpl\u0026#34;/\u0026gt; \u0026lt;/beans\u0026gt; @Component无法实现 @Bean public OneService getService(status) { case (status) { when 1: return new serviceImpl1(); when 2: return new serviceImpl2(); when 3: return new serviceImpl3(); } } Spring基于XML注入bean的 4种 方式 Set方法注入 构造器注入 静态工厂注入 实例工厂注入 Spring Bean 的 五种 作用域 singleton：单例的，在同一Spring IoC容器中只有一个实例。 prototype：一个bean的定义有多个实例。 request：每次http请求都会创建一个新的bean，只在该http request内有效。 session：每次新的session的http请求都会创建新的bean，只在该session http 内有效。 global-session：全局的session的http请求都会创建新的bean，只在该 session http 内有效。 单例Bean的线程安全问题 单例Bean不是线程安全的 因spring没有对单例bean进行多线程封装处理。\n大部分时候Spring bean 无状态的（Dao类），但是如果有状态，那么就要自己去保证线程安全性。\n无状态：不会保存数据，无写数据操作 有状态：会保存数据，有写数据的操作 如果每个线程的全局变量、静态变量只有读操作，一般来说线程安全。 解决方案 改变bean的作用域为prototype，这样每次请求都是new Bean，保证线程安全。 使用ThreadLocal，存放在ThreadLocal中与当前线程绑定。 Bean的生命周期 （重要） 四个阶段 实例化 属性赋值 初始化 销毁 实例化 -\u0026gt; 属性赋值 -\u0026gt; 初始化 -\u0026gt; 销毁\n各个阶段的工作 实例化：创建Bean对象 属性赋值：调用Bean的set方法【设置属性值】 初始化： 如果实现了xxxAware接口，通过不同类型的Aware接口拿到Spring容器的资源。 如果实现了BeanPostProcessor接口，则会回调该接口的postProcessorBeforeInitialization 和 postProcessorAfterInitialization方法 如果配置了init-method方法，则会执行init-method配置的方法 销毁 容器关闭后，如果Bean实现了DisposableBean接口，则会回调该接口的destroy方法 如果配置了destroy-method方法，则会执行destroy-method配置的方法 影响多个Bean的接口 InstantiationAwareBeanPostProcessor BeanPostProcessor 影响单个Bean的接口 Aware类型的接口 生命周期接口 哪些是重要的生命周期方法 两个重要的生命周期方法：\nsetup：容器加载bean被调用 teardown：容器卸载类的时候被调用 什么是Spring内部bean 当一个bean仅被用作另一个bean的属性的时候，它可以被声明为内部类。\n内部bean可以用setter注入属性和构造方法注入构造参数，内部bean通常是匿名的，Scope为prototype。\n什么是Bean装配 知道bean的依赖关系，容器通过依赖关系把bean装配到一起。\n什么是Bean自动装配 在Spring框架中，用配置文件设置bean之间的依赖，Spring就可自动装配相互依赖的bean。容器无需配置就能通过bean工厂自动处理bean之间的协作。\nSpring自动装配的方式 在Spring框架xml配置文件有5种自动装配：\nno：默认不自动装配，手工设置ref属性来装配bean byName：通过bean的名称进行自动装配，与prototype的值对应 byType：通过参数的数值类型自动装配 constructor：利用构造函数进行进行装配 autodetect：自动探测 使用@Autowired自动装配过程 使用之前要在Spring配置文件进行配置 \u0026lt;context:annotation-config /\u0026gt; 。\n启动Spring IoC的时候，容器自动装配了一个后置处理器，当容器扫描到@Autowied、@Resource或@Inject的时候，容器自动查找需要的bean，并装配给对象的属性。\n如果查询结果唯一，bean装配给@Autowired指定的数据 如果查询结果多个，@Autowired会根据名称来查找 如果上述查找的结果为空，那么会抛出异常。解决方法时，使用required=false。 自动装配局限性 重写：需要重写自动装配 基本数据类型：不能自动装配简单的属性，如基本数据类型，String字符串和类 模糊特性：不如显示装配精准 FactoryBean 和 BeanFactory 的区别 BeanFactory：是Bean工厂，ApplicationContext的父类，IoC容器的核心，负责生产和管理bean。 FactoryBean：是一个Bean，通过实现FactoryBean接口定制实例化bean的逻辑。 循环依赖 有一个对象A内有属性对象B，对象B内有属性对象A。\n创建对象A -\u0026gt; 处理A的依赖 -\u0026gt; 创建B对象 -\u0026gt; 处理B的依赖 -\u0026gt; 创建A -\u0026gt; 处理A依赖 -\u0026gt; 创建B ······无限循环。\n自己依赖自己\n当我们注入对象A的时候，需要注入对象A标记了某些注解的属性，这些属性称为A的依赖，对象A中依赖初始化完成才算是A创建成功。\nSpring三级缓存 singletonObject 一级缓存：用于保存实例化、注入、初始化完成的bean实例 earlySingletonObject 二级缓存：用于保存实例化完成的bean实例 singletonFactories 三级缓存：用于保存bean工厂，以便后续扩展创建代理对象 循环依赖的主要场景 1、单例setter注入 @Service publicclass TestService1 { @Autowired private TestService2 testService2; public void test1() { } } @Service publicclass TestService2 { @Autowired private TestService1 testService1; public void test2() { } } 解决方法\n为什么要使用二级缓存：\n因为三级缓存实际是一个objectFactory对象，并不是实例对象。 三级缓存不能用实例对象，因为需要对对象进行增强。\n2、多例setter注入 在多线程场景下，经常出现\n@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) @Service publicclass TestService1 { @Autowired private TestService2 testService2; public void test1() { } } @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) @Service publicclass TestService2 { @Autowired private TestService1 testService1; public void test2() { } } 因为没有用到缓存，每次都会生成新的对象，所以循环无法解决。\n3、构造器注入 @Service publicclass TestService1 { public TestService1(TestService2 testService2) { } } @Service publicclass TestService2 { public TestService2(TestService1 testService1) { } } 执行过程\n也没有用到缓存，所以无法解决。\n4、单例的代理对象setter注入 @Service publicclass TestService1 { @Autowired private TestService2 testService2; @Async //使用AOP自动生成代理 public void test1() { } } @Service publicclass TestService2 { @Autowired private TestService1 testService1; public void test2() { } } 执行过程\nbean初始化完成后会把原始数据与二级缓存数据比较，不相等抛异常。\n解决方法：修改TestService1为TestService5\n因为bean的加载顺序是由文件完整路径递归查找，按照路径+文件名进行查找，2排在前先加载。\n执行过程\n因为6无二级缓存，所以不会报错。\n5、dependsOn循环依赖 一些特殊场景下，我们可以用注解@DependsOn注解要求先实例化谁。\n@DependsOn(value = \u0026#34;testService2\u0026#34;) @Service publicclass TestService1 { @Autowired private TestService2 testService2; public void test1() { } } @DependsOn(value = \u0026#34;testService1\u0026#34;) @Service publicclass TestService2 { @Autowired private TestService1 testService1; public void test2() { } } @DependsOn注解虽然可以指定先实例化谁，但是底层会判断时候存在循环依赖，如上代码，出现了循环依赖，那么就会抛异常。\n循环依赖的划分 如何解决循环依赖（总结） Spring可以解决的循环依赖有两个前提\n不全是构造器方式的循环依赖 必须是单例 通过三级缓存来解决循环依赖\n第一级缓存：保存实例化、初始化都完成的对象 第二级缓存：保存实例化但是并未初始化的对象 第三级缓存：保存实例化对象的所用的bean工厂 假设一个简单的循环依赖，A、B对象互相依赖\nA对象的创建过程：\n实例化A对象，把A的工厂存入第三级缓存 A注入属性，发现存在B对象，去创建B对象 实例化B对象，注入的时候发现存在A对象，依次从1-\u0026gt;3级缓存查找A，在第三级缓存拿到A，把A放进第二级缓存，同时删除第三级缓存里的A对象。B对象实例化、注入、初始化完成，把B放入第一级缓存。 继续创建A对象，从第一级缓存中拿到B对象，A对象创建完成，删除第二级缓存里的A对象，把A放入第一级缓存 一级缓存保存着A、B对象 因为实例化和初始化流程分开了，所以如果用构造器，没办法分离两个流程，就无法解决循环依赖。\n为什么要三级，二级不行吗 不可以，因为第三级缓存是构建了一个ObjectFactory对象，保证在生成代理对象的时候不会覆盖掉二级缓存中的普通bean。如果只有二级缓存就会覆盖掉，在多线程下很难保证数据一致。\nSpring 注解 什么是基于Java的Spring注解配置 基于Java的配置，使用少量的注解，代替大量的xml文件配置。\n举例：\n如@Configuration，标记一个bean，被Spring IoC容器使用。 @Bean，表示该方法要返回一个对象，作为bean用于Spring上下文。\n@Configuration public class StudentConfig { @Bean public StudentBean myStudent() { return new StudentBean(); } } 如何开启注解 注解装配默认不开启，需要在Spring配置里配置\u0026lt;context:annotation-config/\u0026gt;元素。\n@Component、@Controller、@Service、@Repository 区别 @Component：用于将java类标记为bean。 @Controller：用于标记为Spring Web MVC控制器 @Service：用于标记为服务层，更好知名意图。 @Repository：用于将DAO导入IoC。 @Required 注解有什么作用 明确bean的属性必须在配置的时候设置，如未设置则抛异常\npublic class Employee { private String name; @Required public void setName(String name){ this.name=name; } public string getName(){ return name; } } @Autowired 注解有什么作用 按照类型自动装配注入，要求依赖对象必须存在。\n可用于构造方法、成员变量、Setter方法\n@Autowired和@Resource之间的区别 @Autowired：按照类型装配注入，依赖对象必须存在 @Resource：按照名称装配注入，找不到名称再按类型。 @Qualifier 注解有什么作用 有多个相同的bean时，指定使用哪个bean。\n@RequestMapping 注解有什么用？ 将http请求映射到控制器指定的方法或者类\n类：映射请求的URL 方法：映射URL和Http请求方法 Spring 数据访问 对象/关系映射集成模块 Spring通过支持ORM模块，支持我们直接在jdbc上使用ORM映射工具。\nSpring如何高效使用JDBC 使用Spring JDBC框架，减小操作失败的代价。集成JDBC的模板类。\nspring DAO 有什么用 DAO被称为数据访问对象，使得JDBC等数据访问技术以一种统一的方式工作。\nJdbcTemplate是什么 JdbcTemplate类提供了解决数据库数据和基本数据类型与对象之间的转换，执行写好可调用的sql语句，提供自定义错误处理。\nSpring事务 事务的实现方法和原理 事务本质就是对数据库事务的支持，没有数据库事务支持，Spring无法提供事务功能。\n真正数据库层的事务提交和回滚通过bin log和redo log实现。\nSpring事务管理类型和实现方式 编程式事务管理：通过代码管理事务，灵活但是难管理。 声明式事务管理：业务层和事务管理分开，使用注解或xml配置管理事务。 什么是事务传播 事务传播是为了解决业务层方法相互调用的事务问题，当一个事务方法调用另外一个事务方法时，指定事务如何传播。\n举例：\n@Transactional public void methodA(){ jdbcTemplate.batchUpdate(updateSql, params); // A 事务调用 B 事务 methodB(); } // 不使用当前事务，创建一个新事务 @Transactional(propagation = Propagation.REQUIRES_NEW) public void methodB(){ jdbcTemplate.batchUpdate(updateSql, params); } 事务传播的7大值 propagation_required：如果当前没有事务，创建事务；如果当前有事务，加入事务。最常用。 propagation_supports：支持当前事务。如果当前有事务，加入；如果当前没事务，以非事务执行。 propagation_mandatory：支持当前事务。如果当前有事务，加入；如果当前没有事务，抛出异常。 propagation_requires_new：创建新事务，无论当前有无事务。 propagation_not_supports：以非事务方式执行。如果当前存在事务，挂起。 propagation_never：以非事务方式执行。如果当前存在事务，抛异常。 propagation_nested：如果当前有事务，嵌套在事务内执行；如果当前没有事务，以required属性执行。 事务的五大隔离级别 isolation_default：用底层数据库设置的隔离级别，数据库用什么我用什么。 isolation_read_uncommitted：未提交读，最低的隔离级别，事务在提交之前，都可以其他事务读取（会出现脏读、幻读、不可重复读） isolation_read_committed：提交读，事务只有提交之后，才能别其他事务读取（会出现幻读、不可重复读）SQL Server 默认级别。 isolation_repeatable_read：可重复读，保证多次读取同一值的时候，值都和事务开始时的内容一致，禁止读取到别的事务未提交的数据（会造成幻读）MySQL默认隔离级别。 isolation_serializable：序列化，代价最高最可靠，能防止脏读、幻读、不可重复读。 脏读：一个事务读取到另一个事务还未提交的数据。 不可重复读：同一事务内，多次读取同一数据。 幻读：同一事务内，多次查询结果都不一样。 事务的失效场景 1. 只支持public修饰的方法 2. 用final修饰不生效 事务底层使用AOP，通过jdk或者CGLIB动态代理，生成代理类，对代理类添加事务，如果使用final修饰，在代理类中就无法重写该方法，而添加事务。\n3. 同一个类的内部调用，不生效 @Servcie public class ServiceA { @Transactional public void save(User user) { ......; this.doSave(user); } @Transactional public void doSave(User user) { ......; } } 因为生成代理类后，直接调用了this对象的方法，不会生成事务。\n解决方法：\n新加一个类 @Servcie public class ServiceA { @Autowired prvate ServiceB serviceB; public void save(User user) { ...... serviceB.doSave(user); } } @Servcie public class ServiceB { @Transactional(rollbackFor=Exception.class) public void doSave(User user) { ...... } } 自己注入自己 @Servcie public class ServiceA { @Autowired prvate ServiceA serviceA; public void save(User user) { ...... serviceA.doSave(user); } @Transactional(rollbackFor=Exception.class) public void doSave(User user) { ...... updateData2(); } } 通过AopContent类 @Servcie public class ServiceA { public void save(User user) { ...... ((ServiceA)AopContext.currentProxy()).doSave(user); } @Transactional(rollbackFor=Exception.class) public void doSave(User user) { ...... updateData2(); } } 4. 未被Spring管理 5. 多线程调用 6. 数据库引擎不支持事务 Spring框架下的事务管理的优势 为不同的事务API（JDBC、JTA），提供不变的编程模式 为编程式事务管理提供一套简单的API 支持声明式事务管理 和Spring的各种数据访问抽象层很好的集成 你更倾向于那种事务管理 更倾向于声明式事务管理。\n声明式事务管理对应用代码影响最小，更符合无侵入的轻量级思想。虽然比编程式事务少了一点灵活，但是便于管理。唯一的缺点可能就是没办法像编程式事务管理一样做到对代码块进行事务管理。\nSpring MVC 专题 面试题 什么是MVC MVC是模型（model）、视图（View）、控制器（Controller）。他是一种将应用程序的业务逻辑和展示逻辑分开管理，实现松散耦合的框架。提供了良好的扩展性和可测试性，使得开发和维护Web应用变得简单高效。\n什么是Spring MVC 是一个基于Java的实现了MVC设计模式的请求驱动类型的轻量级Web框架，通过把模型-视图-控制器分离，实现松散耦合，把负责的Web应用分为逻辑清晰的几个部分，简化开发，减少出错。\nSpring MVC优点 可以支持各种视图技术，包括不仅仅jsp 与Spring框架集成 清晰的角色分配：前端控制器dispatcherServlet、请求到处理器映射handlerMapping、处理适配器HandlerAdapter、视图解析器ViewResolver 支持各种请求到资源的映射策略。 核心组件 前端控制器DispatcherServlet（不需要程序员开发） 作用：请求接收、相应结果，相当于转发器，减少其他组件的耦合度。\n处理器映射器HandleMapper（不需要程序员开发） 作用：根据请求的URL查找Handle\n处理适配器HandleAdapter（不需要程序员开发） 注意：编写Handle的时候要根据HandleAdapter的规则，这样才能正常执行。\n处理器Handle（程序员开发）\n视图解析器ViewResolver（不需要程序员开发）\n作用：进行视图解析，根据视图逻辑名解析成真正的视图。\n视图View（程序员开发jsp等） 是一个View接口，实现类支持不同的视图类型（jsp、free marker）\n什么是DispatcherServlet Spring MVC是围绕DispatcherServlet设计的，用来处理所有的Http请求和响应。\n什么是Spring MVC的控制器——Controller 提供访问应用程序的行为，控制器通过解析用户输入转换成视图呈现给用户的模型。Spring提供了抽象的控制层，允许创建多个控制器，多种用途。\nSpring MVC的控制器是不是单例模式 是单例模式，在多线程访问的时候会出现线程安全问题，不要用到同步影响性能。解决方法是不在控制器里写字段。\n工作原理 Spring MVC 工作流程 用户发送请求到前端控制器DispatcherServlet 前端控制器接收请求，调用处理器映射器HandlerMapping，请求获取Handler 处理器映射器根据请求的URL找到对应的处理器，生成处理器对象以及处理器拦截器，返回给前端控制器 前端控制器调用处理器适配器HandlerAdapter 处理器适配器调用对应的处理器-后端控制器Handler 后端控制器执行业务完成返回ModelAndView 处理器适配器把ModelAndView返回给前端控制器 前端控制器把ModelAndView传给视图解析器VierResolver 视图解析器解析返回具体的视图View 前端控制器对View进行视图渲染 响应给用户 MVC框架 什么是MVC框架 Model View Control 把模块-视图-控制器分开管理的一种设计模式。用于把前端页面显示和后端业务处理分离。\nMVC框架有什么好处 分层设计，实现了业务系统各层之间的解耦，有利于系统的维护和扩展 系统并发开发，提高开发效率 常用注解 注解是什么 注解的本质是继承了Annotation的特殊接口，具体实现类就是Java的动态代理类。我们通过反射获得注解。\nSpring MVC 常用注解 @Controller：用于定义控制器类 @RequestMapper：通常后面加括号，内填写路径，用于处理请求url映射的注解，可用在方法和类上，用在类上则表示所有响应请求的方法都已该地址为父路径。 @ResponseBody：把controller方法返回的对象转换成json响应给用户 @RestController：@Controller+@ResponseBody @RequestBody：将接收Http请求的json转换成java对象 @ControllerAdvice：统一异常处理 @ExceptionHandler：用在方法上，表示出现这类异常执行这个方法 其他 Spring怎么设置重定向和转发 转发：在返回值前加forward。如forward:user.do?name=method4 重定向：在返回值前加redirect。如redirect:http://www.baidu.com 如何解决Post请求中文乱码问题，Get如何做 POST请求：在web.xml中配置一个CharacterEncodingFilter过滤器，设置UTF-8 GET请求：重新编码或者修改tomcat配置文件 Spring MVC的异常处理 可以把异常抛给Spring框架，我们只需要配置简单的异常处理器，在异常处理器中添加视图页即可。\n怎么在方法里获得Request 或 Session 直接在方法形参中声明。\n怎么获得从前端传入的参数 直接在形参里声明参数，参数名保持一致\nSpring MVC返回值 String、ModelAndView，一般用String。\n用什么对象从后台向前端传数据 通过ModelAndView对象\n","permalink":"https://zhoujze.github.io/en/posts/tech/spring/","summary":"Spring 面试题 什么是Spring spring是一个轻量级的Java开发框架，用于解决企业级应用开发的复杂性，简化Java开发。 spring底层有两个核心特性： AOP面向切面编程 DI依赖注入 降低开发复杂度的四个策略： 基于POJO的轻量级和最小侵入性编程 通过依赖注入和切面编程实现松散耦合 基","title":"Spring基础总结"},{"content":"缓存穿透 什么是缓存穿透 查询一个不存在的数据，缓存层和数据层都不会命中\n缓存穿透导致每次查询都要请求到储存层，失去了保护储存层的意义 大量的空值查询，因储存层不具备高并发，可能出现宕机。 形成原因：\n自身业务代码问题 恶意攻击、爬虫 如何解决缓存穿透 缓存空对象 储存层未命中时，该对象返回给缓存层保留，再次访问直接访问缓存就能找到。保护了后端数据。 造成问题： 缓存里保留了大量了空值，造成缓存过大。解决方法：给保留的空值设置过期时间。 缓存和后端的数据不一致，需要消息队列或者其他方式清除空对象。 布隆过滤器 在访问缓存层和存储层之前，将存在的 key 用布隆过滤器提前保存起来（布隆过滤器的特点就是，如果布隆过滤器认为某个 key 不存在，那么这个 key 一定不存在，就不用再请求缓存和数据库了） 缓存雪崩 缓存击穿 缓存热点 BigKey问题 缓存和数据库一致问题 ","permalink":"https://zhoujze.github.io/en/posts/tech/redis-05-%E7%BC%93%E5%AD%98%E8%AE%BE%E8%AE%A1/","summary":"缓存穿透 什么是缓存穿透 查询一个不存在的数据，缓存层和数据层都不会命中 缓存穿透导致每次查询都要请求到储存层，失去了保护储存层的意义 大量的空值查询，因储存层不具备高并发，可能出现宕机。 形成原因： 自身业务代码问题 恶意攻击、爬虫 如何解决缓存穿透 缓存空对象 储存层未命中时，该对象返回给缓存层","title":"Redis 05-缓存设计"},{"content":"发布于订阅 频道订阅与退订 所有频道的订阅关系都保存在服务器的pubsub_channels字典中\n字典中的键是某个被订阅的频道 字典中的值是订阅频道的客户端，是链表 模式订阅与退订 所有模式订阅关系都存在服务器的pubsub_patterns链表中，链表中的节点结构pubsubPattern。\n示例:\n发送消息 发送给频道订阅者：在pubsub_channels字典中找到订阅者信息发送。 发送给模式订阅者：遍历pubsub_patterns链表，找到与客户端相同模式的客户端，发送。 Redis 事务 事务的实现 事务开始 命令入队 事务执行 1) 事务开始 redis\u0026gt; MULTI OK MULTI命令把客户端从非事务状态切换至事务状态——flags属性表示。\n2) 命令入队 当客户端处于非事务状态，命令会立即被服务器执行 当客户端处于事务状态，根据命令判断执行 如果客户端发送的命令为 EXEC 、 DISCARD 、 WATCH 、 MULTI 四个命令的其中一个，那么服务器立即执行这个命令。 除此之外，不立即执行，放入事务队列，向客户端返回QUEUED回复. 整体结构：\n事务状态：保存在服务器状态的mstate属性中 typedef struct redisClient { // ... // 事务状态 multiState mstate; /* MULTI/EXEC state */ // ... } redisClient; 事务状态 typedef struct multiState { // 事务队列，FIFO 顺序 multiCmd *commands; // 已入队命令计数 int count; } multiState; 事务队列 typedef struct multiCmd { // 参数 robj **argv; // 参数数量 int argc; // 命令指针 struct redisCommand *cmd; } multiCmd; 示例：\n执行命令 redis\u0026gt; MULTI OK redis\u0026gt; SET \u0026#34;name\u0026#34; \u0026#34;Practical Common Lisp\u0026#34; QUEUED redis\u0026gt; GET \u0026#34;name\u0026#34; QUEUED redis\u0026gt; SET \u0026#34;author\u0026#34; \u0026#34;Peter Seibel\u0026#34; QUEUED redis\u0026gt; GET \u0026#34;author\u0026#34; QUEUED 结构图\n3) 执行事务EXEC 服务器遍历事务队列，执行队列里的所有命令，最后把执行结果全部返回给客户端。\nwatch命令（乐观锁） watch命令是一种乐观锁，可以在exec命令执行前，检测任意数量的数据库键，在exec命令执行时，检测键是否已经被修改，如果修改则拒绝执行，返回执行失败的空回复。 底层实现：\nRedis底层里有个watched_keys字典属性用于监测 如果被修改则打开REDIS_DIRTY_CAS标识，最后根据表示判断。 事务的ACID性质 × 原子性 Redis不具备原子性，因为Redis不支持事务回滚，在事务队列里即使有命令出错，也会执行下去。\n√ 一致性 如何保证一致性：\n入队错误：如果在入队命令过程中，命令不存在，格式错误，Redis拒绝执行。 执行错误：事务执行过程中，命令出错，不会对数据库进行修改，不会对事务的一致性产生影响。 服务器停机： 无持久化模式下，重启依旧是空白，因此数据一致。 RDB模式下，事务中途停机不会导致不一致，因为可以恢复到一致状态。 AOF模式下，事务中途停机不会导致不一致，因为可以恢复到一致状态。 √ 隔离性 Redis采用单线程方式执行事务，并且服务器保证，运行期间事务不会中断，因此事务总是串行执行，具有隔离性。\n√/× 持久性 Redis在RDB和AOF模式下才会持久化。\n","permalink":"https://zhoujze.github.io/en/posts/tech/redis-04-%E7%8B%AC%E7%AB%8B%E5%8A%9F%E8%83%BD%E5%AE%9E%E7%8E%B0/","summary":"发布于订阅 频道订阅与退订 所有频道的订阅关系都保存在服务器的pubsub_channels字典中 字典中的键是某个被订阅的频道 字典中的值是订阅频道的客户端，是链表 模式订阅与退订 所有模式订阅关系都存在服务器的pubsub_patterns链表中，链表中的节点结构pubsubPatter","title":"Redis 04-独立功能实现"},{"content":"主从复制 Redis使用SLAVEOF命令实现主从复制\nRedis 2.8 之前的旧版复制功能 旧版复制的具体过程 两个过程：\n同步 命令转播 同步：将从服务器的数据库状态更新至主服务器当前的数据库状态，具体命令SYNC。\n从服务器发送SYNC命令给主服务器 主服务器收到SYNC命令，执行BGSAVE同时创建子进程生成RDB文件，并使用缓存区记录从当前时刻开始执行的命令 主服务器执行BGSAVE命令结束，由BGSAVE生成的RDB文件发送给从服务器，从服务器接收并载入RDB文件 主服务器将缓存区记录的命令发送给从服务器执行 命令传播：同步完成后，主服务器接收的命令，同时传播给从服务器写命令，使得主从服务器数据一致。\n旧版复制的缺陷 主从复制包含两种情况：\n初次复制：从服务器是一台新的服务器，或者之前的主服务器不是目前的主服务器。 断线后复制：处于命令传播的时候，如果出现网络波动问题，从服务器又重新连接了主服务器，那么就要重新执行SYNC复制完整的主服务器数据库状态。(缺陷) Redis 2.8 之后的新版复制功能 新版复制的具体过程 新版复制包含三个操作：\n同步 命令传播(同旧版命令传播) 心跳检测 同步 新版同步命令PSYNC，命令有两种模式\n完整重同步\n用于初次复制，执行步骤和旧版sync命令一样。 部分重同步（解决断线后复制问题）\n部分重同步不用重新执行完整SYNC命令，主服务器将从服务器断开期间的写命令，发送给从服务器，从服务器接收就可更新成相同状态。 具体实现关键点： 复制偏移量：主从服务器偏移量要一致，不一致则需要同步\n复制积压缓冲区：主服务器在发送写命令的同时，发送到复制积压缓冲区队列一份。当从服务器偏移量不一致的时候，主服务器通过偏移量来决定从复制积压缓冲区队列的哪里开始同步。如果偏移量大于队列里的数据量，则需要完整重同步。\n运行ID： 初次复制：主从服务器连接后，主服务器发送自己的ID给从服务器记录。 断线重连：从服务器连上主服务器之后，从服务器发送保存的主服务器ID，由主服务器判断： 与自己ID相等，主服务器继续执行部分重同步 与自己ID不相等，主服务器对其执行完整重同步 命令转播 命令传播和旧版相同，主服务器接收命令，把写命令发送给从服务器执行。同步主服务器的数据库状态。\n心跳检测 在命令传播阶段，从服务器以每秒一次的频率向主服务器发送命令，作用包括：\n检测主从服务器网络连接是否正常 防止主服务器在不安全的情况下执行写操作 检测命令丢失 复制的具体实现步骤 设置主服务器地址和端口 建立套接字连接\n发送ping命令\n身份验证\n发送端口信息\n同步 命令传播 哨兵(Sentinel) 什么是哨兵： 哨兵是Redis为高可用性提供的解决方案。 由一个或多个sentinel实例组成的Sentinel系统，可以监测任意多个主服务器、从服务器，并且在被监视的主服务器下线的时候，对该服务器进行故障转移，然后由新的主服务器代替下线的主服务器继续接收命令。\n哨兵的初始化步骤 初始化普通Redis服务器 将普通的Redis服务器使用的代码替换成Sentinel专用代码 初始化Sentinel状态 根据给定的配置文件，初始化Sentinel监视的主服务器列表 创建连接主服务器的网络连接 Sentinel创建两个对主服务器进行的异步网络连接（因Sentinel需要与多个主服务器实例创建多个连接，所以使用异步连接）\n命令连接：用于向主服务器发送和接收命令 订阅连接：用于订阅主服务器的频道，以便后续与其他Sentinel相认。 当Sentinel发现主服务有新的从服务器时，Sentinel也会与其建立连接\n哨兵获取服务器状态 Sentinel默认每十秒一次，通过命令连接向服务器发送INFO命令，并通过分析回复获取服务器状态。\n哨兵集群搭建 监视统一主从服务器的多个Sentinel可以通过与主从服务器建立的订阅连接互相发现对方。原理就时Sentinel向订阅频道里发送信息，同时订阅了相同频道里的Sentinel会发现对方。\n哨兵如何判断服务器下线 Sentinel通过每秒一次的向建立了命令连接的所有实例（主从服务器、其他Sentinel）发送PING命令，并通过PING命令的返回判断实例是否在线。\n如果在规定事件没有PING响应，则标记为主观下线 因存在网络波动，短时间没有响应，所以为了减少误判，设计了客观下线（客观下线只针对主服务器） 客观下线规则：需要3台以上的已经与该主服务器建立连接的Sentinel集群一起判断。 Sentinel1将主服务器标记主观下线，同时通过该主服务器的频道询问其他Sentinel，看他们是否标记主观下线 当Sentinel1接收到足够多的已下线判断后（数量由Sentinel中的quorum属性，设置为Sentinel个数/2+1，如3个Sentinel设置为2），Sentinel1判断该主服务器客观下线，并对该主服务器进行故障转移。 选举领头哨兵（由领头哨兵进行故障转移） 从监视该主服务器的Sentinel中协商选举领头哨兵进行故障转移。\n选择规则：\n每次进行领头哨兵选举时，所有的哨兵配置纪元（内置的计数器）的值都自增1。 在一个配置纪元里，所有的哨兵都有一次设置某个哨兵为局部领头的机会，一旦设置成功，在该设置纪元无法更改。 每个发现主服务器客观下线的哨兵都会要求其他哨兵把自己设置为局部领头 哨兵局部领头的设置规则为先到先得，最先要求设置的设置成功，后面再要求的一律失败。 如果某个哨兵被半数以上（n/2+1）的哨兵设置为局部领头，那么该哨兵称为领头。 如果在规定时间里没有选举出，那么将在一段时间后继续选举。 该算法是基于Raft算法的领头选举方法\n为什么哨兵集群最少3个 因为在进行领头选举的时候，如果只有2个哨兵。要获得半数以上的票，就要2/2+1=2票，而不是1票。\n如果哨兵集群中有哨兵下线，那么只有1票，永远无法选出领头哨兵，无法进行主从节点切换。\n所以通常配置3个哨兵以上。\n故障转移 步骤：\n选出新的主服务器 通知从服务器修改复制目标 将旧主服务器降级成从服务器 集群(Cluster) 什么是集群 Redis提供的分布式数据库方案，通过分片进行数据共享，提供复制和故障转移功能。\n集群里的每个Redis服务器表示为一个节点\n每个节点存在两个结构：\nclusterNode：保存节点当前的状态 clusterState ：保存当前节点视角下，集群的状态，如集群的状态、配置纪元、节点数等 节点初始为单个节点，通过CLUSTER MEET命令将节点与节点连接，组成集群。\n命令格式：\nCLUSTER MEET \u0026lt;ip\u0026gt; \u0026lt;port\u0026gt; 示例：现有三个节点，127.0.0.1:7000 127.0.0.1:7001 127.0.0.1:7002。\n使用客户端连接7000发送cluster node命令查看7000节点当前端口 $ redis-cli -c -p 7000 127.0.0.1:7000\u0026gt; CLUSTER NODES 51549e625cfda318ad27423a31e7476fe3cd2939 :0 myself,master - 0 0 0 connected 发送命令将7001与7002加入7000所在集群 127.0.0.1:7000\u0026gt; CLUSTER MEET 127.0.0.1 7001 OK 127.0.0.1:7000\u0026gt; CLUSTER NODES 68eef66df23420a5862208ef5b1a7005b806f2ff 127.0.0.1:7001 master - 0 1388204746210 0 connected 51549e625cfda318ad27423a31e7476fe3cd2939 :0 myself,master - 0 0 0 connected 127.0.0.1:7000\u0026gt; CLUSTER MEET 127.0.0.1 7002 OK 127.0.0.1:7000\u0026gt; CLUSTER NODES 68eef66df23420a5862208ef5b1a7005b806f2ff 127.0.0.1:7001 master - 0 1388204848376 0 connected 9dfb4c4e016e627d9769e4c9bb0d4fa208e65c26 127.0.0.1:7002 master - 0 1388204847977 0 connected 51549e625cfda318ad27423a31e7476fe3cd2939 :0 myself,master - 0 0 0 connected 图片理解，节点之间握手连接：\n槽指派 什么是槽 集群通过分片来保存数据库中的键值对。\n集群的整个数据库被分为2^14个槽，数据库中的每个键都属于一个槽，每个节点可以处理若干个槽。\n集群使用哈希槽，方便添加和删除节点，只要把槽挪到新节点即可。\n集群状态判断：\n上线状态：所有的槽都有节点在处理 下线状态：只要有槽未被分配到节点处理 记录节点的槽指派信息 槽指派就是通过节点clusterNode的slot numslot属性指派该节点负责处理的槽。\n传播节点的槽指派信息 节点会把自己的slot数组通过消息发送给集群里的其他节点，告诉他们自己负责处理的槽。\n节点7001 7002接收到7000的slot数组后，对自己的clusterState.slot数组进行更新。\nclusterState.slot记录的集群中所有的槽指派信息。\n槽的数量为什么是2^14 slot数组是一个二进制数组，长度为2^14/8=2048字节（2KB）\n从业务层面，Redis不太可能扩展1000+主节点，节点越多，携带数据也越多，网络就会越拥堵，Redis不建议扩展超过1000节点。\n在集群中执行命令(moved错误) 在客户端连接的当前节点执行命令时，如果键所在槽没有在指派在当前节点就会报moved错误。\n引导客户端转向正确的节点，并发送命令。\n重新分片 什么是重新分片 把已经交给节点的槽，重新指派到另一个节点。并且槽所属的键也一同过去。\n集群可以在线进行重新分片，源节点和目标节点都可以继续接收命令操作。\n重新分片原理 由Redis集群管理软件redis-trib执行。\n步骤：\n目标节点准备好从源节点导入键值对 源节点准备好把键值对迁移 获得若干个属于槽的键值对的键名 根据键名，将键原子性的迁移到目标节点 重复3、4步骤，直到2准备的键全部迁移。 槽指派给目标节点 ASK错误 在进行3、4步骤进行槽迁移的时候，一部分键值对在源节点，一部分键值对在目标节点。这时对源节点发送命令，操作的键值对就是正在迁移的键：\n源节点找到键 -\u0026gt; 直接执行命令 源节点未找到键 -\u0026gt; 可能被迁至目标节点了，返回ASK错误，并引导客户端转向目标节点，再次发送命令。 引导转向并发送ASKING 命令 ，然后再执行命令。 注意：如果没有发送ASKING，直接执行命令，因为槽指派还是源节点，那么就会返回moved错误。\nmoved错误和ASK错误区别\nmoved代表槽指派已经更变，在第一次报出moved错误转向目标节点后，以后所有都会直接把命令发送到目标节点。 ask错误只是在迁移发生的时候使用的临时措施。不会因报出一次ASK错误下次就会改变发送对象。 集群中的主从复制 主节点处理槽，从节点复制主节点。如果主节点下线，代替成为主节点继续接收命令。\n集群中如何判断节点是否下线 集群里每个节点定期向其他节点发送PING，检测对方是否在线。\n如果PING无返回，或者返回超时。那么发送PING的节点就会把接收PING的节点标记疑似下线。 当A节点通过消息得知B节点标记了C节点为疑似下线。就会在自己的clusterState.nodes里找到C节点的结构，并在结构里的fail_reports链表里添加下线报告。\n如果半数以上的操作槽的主节点都标记为疑似下线，那么就会被标记为已下线。\n将主节点标记为已下线的节点会向集群广播节点下线。所有收到广播的节点都把节点标记为已下线。\n故障转移 从节点发现主节点下线，对主节点进行故障转移。\n步骤：\n选出新的主服务器成为主节点 基于Raft算法的领头选举\n开始故障转移，节点配置纪元+1 每个纪元里，负责处理槽的主节点都有一次投票 从节点发现主节点下线，广播让有投票权的主节点为自己投票 投票规则：先到先得。 如果有半数以上的票，则成为主节点。 如果该纪元选举失败，那么集群进入新纪元，重新选举，直到选出新主节点。 新主节点撤销所有旧主节点的槽指派并把槽指派指向自己 新主节点向集群广播自己成为主节点操作处理槽 新主节点接收命令处理操作负责的槽 一致性哈希 Redis中引用的一致性哈希思想，但是没有直接引用，而是引入了哈希槽的概念。\n简单哈希 问题在于，集群扩容的时候，length发生变化，大部分数据重新hash导致被分配到其他机器上，这样操作导致服务器在一定时间不可用，每次扩容都会存在这个问题。导致缓存雪崩。\n一致性哈希 一致性hash算法主要应用于分布式存储系统。\n一致性hash算法构建了一个虚拟的哈希环。\n按顺时针方向从0开始到2^32-1。\n将各个服务器进行哈希，具体使用服务器主机名，作为key进行哈希，确定在哈希环上的位置。\n插入key-value数据的时候，对key进行哈希，然后放入对应位置，顺时针找到第一个服务器存储。\n容错性和可扩展性 假设node2宕机，只有key-b受到影响，会接着顺时针找到第一个服务器node3进行存储。\n假设新增一台node4服务器，受影响的只有node1和node4之间的key-b。 总结：在一致性哈希中，新增或者下机一台服务器，受影响的只有该服务器和前一服务器之间的数据。具有较好的容错性和可扩展性。\n数据倾斜 一致性哈希在服务器数很少的时候，容易因为节点分布不均导致有的服务器数据量很大，而有的服务器仅仅只有少量数据。\n示例：node2数据量很大，而node1很小。\n为了解决，一致性哈希引入了虚拟节点机制：为每个服务器计算多个哈希，每个计算的位置都放一个虚拟节点。可用node#1来表示。如图：\n这样只是多了一个对实际节点的映射。\nRedis为什么不用一致性哈希（缓存雪崩） 无法很好的手动设置数据分布，哈希槽则可以灵活的配置每个节占用的哈希槽数量。 缓存雪崩：一致性哈希环里如果有节点承受不住数据量，就会宕机，数据传给下一个节点，下一节点也承受不住宕机，数据就滚雪球越来越大，最终集群雪崩。 一致性哈希比哈希槽更复杂。 如何解决集群数据丢失问题 Redis数据丢失的两种情况 异步复制导致丢失 集群脑裂导致丢失 主节点发生了假故障，与哨兵失联，但是客户端正常。这时客户端发送命令，因网络问题无法同步给从服务器。 哨兵选举新主服务器，集群有两个主服务器——集群脑裂 网络好了，旧主服务器降级，清空自己的数据，复制新主服务器的数据，但是，在一开始客户端写入的命令就会丢失，导致数据丢失。 解决方法 通过参数设置，限制主服务器在假故障的时候接收客户端命令。\n主从复制、哨兵、集群的区别 主从复制为了数据备份\n哨兵为了高可用\n集群为了在单实例环境下，分散压力\n主从模式：备份数据，负载均衡，读写分离，主服务器可以有多个从服务器。 哨兵模式：监控，故障转移，哨兵发现主服务器挂了，就会在从服务器中重新选举主服务器。 集群模式：解决单机Redis容量有限问题，将数据分给多个服务器。 ","permalink":"https://zhoujze.github.io/en/posts/tech/redis-03-%E5%A4%9A%E6%9C%BA%E6%95%B0%E6%8D%AE%E5%BA%93%E7%9A%84%E5%AE%9E%E7%8E%B0/","summary":"主从复制 Redis使用SLAVEOF命令实现主从复制 Redis 2.8 之前的旧版复制功能 旧版复制的具体过程 两个过程： 同步 命令转播 同步：将从服务器的数据库状态更新至主服务器当前的数据库状态，具体命令SYNC。 从服务器发送SYNC命令给主服务器 主服务器收到SYNC命令，执行BGSAVE同时创建子进","title":"Redis 03-多机数据库的实现"},{"content":"数据库 数据库键空间 Redis是一个key-value形式的数据服务器，服务器中的每个数据库都是一个redis.h/redisDb结构表示。redisDb中的dict字典属性记录了数据库中的所有键值对，这个字典称为键空间。 typedef struct redisDb { // ... // 数据库键空间，保存着数据库中的所有键值对 dict *dict; // ... } redisDb; 键空间中的键就是数据库的键，每个键都是一个字符串对象 键空间中的值就是数据库的值，每个值都可以是Redis五大基本对象中的一种。 举例 在空白数据库中执行命令 redis\u0026gt; SET message \u0026#34;hello world\u0026#34; OK redis\u0026gt; RPUSH alphabet \u0026#34;a\u0026#34; \u0026#34;b\u0026#34; \u0026#34;c\u0026#34; (integer) 3 redis\u0026gt; HSET book name \u0026#34;Redis in Action\u0026#34; (integer) 1 redis\u0026gt; HSET book author \u0026#34;Josiah L. Carlson\u0026#34; (integer) 1 redis\u0026gt; HSET book publisher \u0026#34;Manning\u0026#34; (integer) 1 执行完成后数据库的键空间结构如下\n因数据库的键空间是一个字典结构，所有针对数据库增删改查操作都是对其中的字典进行操作实现。 添加新键 添加新的键值对到数据库中，本质就是对数据库的字典进行新增操作，其中键为字符串对象，值则为Redis五大对象之一。 示例 执行命令 redis\u0026gt; SET date \u0026#34;2013.12.1\u0026#34; OK 执行完成后数据库键空间结构图\n删除键 本质就是删除数据库键空间里的键值对对象 示例 执行命令 redis\u0026gt; DEL book (integer) 1 执行完成后数据库键空间结构图\n更新键 本质就是对数据库键空间的键值对对象进行更新，值对象不同，更新的具体方法也不同 示例 执行命令(值为字符串对象) redis\u0026gt; SET message \u0026#34;blah blah\u0026#34; OK 执行完成后数据库键空间结构图\n执行命令(值为哈希对象) redis\u0026gt; HSET book page 320 (integer) 1 执行完成后数据库键空间结构图\n对键查值 根据数据库键空间的键取出所对应的值对象，根据值不同，具体取值方法也不同。 示例 执行命令(字符串对象) redis\u0026gt; GET message \u0026#34;hello world\u0026#34; 执行完成后数据库键空间结构图\n执行命令(列表对象) redis\u0026gt; LRANGE alphabet 0 -1 1) \u0026#34;a\u0026#34; 2) \u0026#34;b\u0026#34; 3) \u0026#34;c\u0026#34; 执行完成后数据库键空间结构图\n其他键空间操作 除了以上的增删改查操作，还有一些基于Redis数据库本身的操作。 FLUSHDB ：清空数据库，通过删除键空间中所有键值对实现。 RANDOMKEY ：随机返回数据库中某个键，通过在键空间中随机返回一个键实现。 DBSIZE ：返回数据库键数，通过返回键空间中包含键值对的数量来实现。 EXISTS 、 RENAME 、 KEYS等。 读写键空间时的维护操作 当使用Redis命令对数据库进行读写操作的时候，服务器还会执行一些额外的维护操作：\n在读取一个键之后（读操作和写操作都要对键进行读取），服务器会根据键是否存在，以此来更新服务器的键空间命中（hit）次数或键空间不命中（miss）次数，这两个值可以在 INFO stats 命令的 keyspace_hits 属性和 keyspace_misses 属性中查看。 在读取一个键之后，服务器会更新键的 LRU （最后一次使用）时间，这个值可以用于计算键的闲置时间，使用命令 OBJECT idletime 命令可以查看键 key 的闲置时间。 如果服务器在读取一个键时，发现该键已经过期，那么服务器会先删除这个过期键，然后才执行余下的其他操作，本章稍后对过期键的讨论会详细说明这一点。 如果有客户端使用 WATCH 命令监视了某个键，那么服务器在对被监视的键进行修改之后，会将这个键标记为脏（dirty），从而让事务程序注意到这个键已经被修改过，《事务》一章会详细说明这一点。 服务器每次修改一个键之后，都会对脏（dirty）键计数器的值增一，这个计数器会触发服务器的持久化以及复制操作执行，《RDB 持久化》、《AOF 持久化》和《复制》这三章都会说到这一点。 如果服务器开启了数据库通知功能，那么在对键进行修改之后，服务器将按配置发送相应的数据库通知，本章稍后讨论数据库通知功能的实现时会详细说明这一点。 重点回顾 Redis服务器的所有数据库都保存在redisServer.db数组中，数据库的数量由redisServer.dbnum保存。 客户端通过修改目标数据库指针，让他指向redisServer.db数组中不同的元素实现切换数据库。 数据库主要由dict和expires两个字典构成，dict用于保存键空间里的键值对，expires保存键的过期时间。 数据库由字典构成，所有对数据库的操作，实际都是在对字典进行操作。 数据库的键总是一个字符串对象，值可以是Redis五大对象之一。 expires字典键指向数据库中的键，值则为键的过期时间，以毫秒为单位的时间戳。 Redis使用惰性删除和定期删除策略来删除过期键：惰性删除碰到过期键才删除，定期删除则每隔一定时间，主动查找过期键删除。 执行SAVE和BGSAVE命令，产生的新的RDB文件不会存在过期键。 执行BGREWRITEAOF命令，产生的重新AOF文件不存在过期键。 当一个过期键被删除之后，服务器会追加一条DEL命令到现有的AOF文件末尾，显示的删除过期键。 从服务器即使发现过期键，也不会自作主张地删除它，而是等待主节点发来 DEL 命令，这种统一、中心化的过期键删除策略可以保证主从服务器数据的一致性。 当 Redis 命令对数据库进行修改之后，服务器会根据配置，向客户端发送数据库通知。 持久化机制 Redis持久化有两种策略 RDB 快照 AOF 日志 RDB快照 RDB持久化就是将Redis在内存中的数据库状态保存到磁盘里，以RDB持久化生成的RDB二进制文件。通过该文件可以还原生成RDB文件时候的数据库状态。\n创建RDB文件 执行SAVE、BGSAVE 命令： SAVE会堵塞Redis服务器进程，一直到RDB文件生成完毕，期间服务器无法进行任何操作 BGSAVE：创建了一个子进程负责生成RDB文件，主进程继续处理命令 加载RDB文件 RDB文件在服务器启动的时候自动加载，在加载的时候Redis服务器会被堵塞。如果服务器开启了AOF持久化，那么服务器在启动的时候优先使用AOF还原数据库状态。 AOF日志 AOF通过保存Redis服务器执行的写命令来记录服务器状态。 AOF持久化功能的实现可以分为命令追加、文件写入、文件同步三个步骤 创建AOF日志 当AOF持久化功能打开的时候，服务器在执行完写命令之后，会以协议格式将被执行的写命令追加到服务器状态的aof_buf缓冲区末尾。 struct redisServer { // ... // AOF 缓冲区 sds aof_buf; // ... }; 示例 执行写命令 redis\u0026gt; SET KEY VALUE OK 服务器在执行命令之后,将以下协议内容追加到 aof_buf 缓冲区的末尾： *3\\r\\n$3\\r\\nSET\\r\\n$3\\r\\nKEY\\r\\n$5\\r\\nVALUE\\r\\n AOF文件的写入与同步 文件写入：为了提高文件的写入效率，在实际操作中，当用户调用write函数，将数据写入文件的时候，系统会暂时保存在内存缓存区里， 文件同步：等缓存区被填满或者由系统主动操作的时候才会真的将缓存区里的数据写入磁盘。\n注意：这种做法虽然高效，但是对写入的数据存在一定安全隐患，因为如果计算机出现停机，那么在缓存区中的数据将会丢失。 服务器每次结束一个事件循环之前，都会调用flushAppendOnlyFile函数，考虑是否要把aof_buf缓存区里的内容写入到AOF日志里。 def eventLoop(): while True: # 处理文件事件，接收命令请求以及发送命令回复 # 处理命令请求时可能会有新内容被追加到 aof_buf 缓冲区中 processFileEvents() # 处理时间事件 processTimeEvents() # 考虑是否要将 aof_buf 中的内容写入和保存到 AOF 文件里面 flushAppendOnlyFile() flushAppendOnlyFile值由服务器配置的appendfsync选项的值来决定，不同的值产生不同的行为\nappendfsync选项值 flushAppendOnlyFile函数行为 always 将aof_buf缓冲区中的所有内容写入并同步到AOF文件。 everysec 将aof_buf缓冲区中的所有内容写入到AOF文件，如果上次同步AOF文件的时间距离现在超过一秒钟，再次同步AOF文件，由专门的线程执行。 no 将aof_buf缓冲区中的所有内容写入到AOF文件，但不对AOF文件进行同步，同步时机由操作系统决定。 appendfsync默认为everysec\n系统提供fsync 和 fdatasync 两个同步函数，可以强制将缓存区中的数据写入到磁盘，确保数据安全。\n示例：\n执行命令 SADD databases \u0026#34;Redis\u0026#34; \u0026#34;MongoDB\u0026#34; \u0026#34;MariaDB\u0026#34; SET date \u0026#34;2013-9-5\u0026#34; INCR click_counter 10086 命令转换成协议追加到aof_buf缓存区 *5\\r\\n$4\\r\\nSADD\\r\\n$9\\r\\ndatabases\\r\\n$5\\r\\nRedis\\r\\n$7\\r\\nMongoDB\\r\\n$7\\r\\nMariaDB\\r\\n *3\\r\\n$3\\r\\nSET\\r\\n$4\\r\\ndate\\r\\n$8\\r\\n2013-9-5\\r\\n *3\\r\\n$4\\r\\nINCR\\r\\n$13\\r\\nclick_counter\\r\\n$5\\r\\n10086\\r\\n 执行flushAppendOnlyFile函数，查看appendfsync属性，若为everysec，则判断距离上次同步AOF文件是否超过1秒。 如超过则把aof_buf中的内容写如到AOF文件中，再对AOF文件进行同步。 安全性分析：\nappendfsync值为always：服务器每次事件循环都要将aof_buf缓存区的数据写入AOF文件，同步AOF文件。效率最低，但是安全性最高，即使服务器停机，也只损失循环中的一点点命令数据。 appendfsync值为everysec：服务器也是每次事件循环都要将aof_buf缓存区中的数据写入AOF文件，每隔一秒事件就要在子线程中对AOF文件进行一次同步。效率相对还行，安全性也还行，即使服务器停机，也只损失一秒的命令数据。 appendfsync值为no：服务器也是每次事件循环都要将aof_buf缓存区中的数据写入AOF文件，但是同步交由系统控制。效率最高，但是安全性很低，如果长时间没有执行AOF同步，服务器停机，损失的数据量就会很大。 AOF重写与后台重写 什么是AOF重写 因为AOF持久化是通过保存被执行的写命令来记录数据库状态，随着服务器的运行，AOF文件内容会越来越多，文件体积会越来越大。\n体积过大的AOF文件会对Redis服务器造成影响 使用AOF进行还原数据所用的事件越多。 Redis服务器会选择一个事件节点 创建一个新的AOF文件来代替旧的AOF文件，AOF文件的所保存的数据库状态相同，但是不会有任何冗余的命令，所以新AOF文件要比旧AOF文件小很多。\n如何实现AOF重写（aof_rewrite 函数） 仅需从数据库中读取键现在的值，然后用一条命令去记录键值对，代替之前的多条命令记录键值对。无需读取现有的AOF文件。\n什么是AOF后台重写 AOF重写会进行大量写入操作，调用该函数的线程很可能会出现堵塞，为了避免堵塞，Redis决定分配一个子进程给AOF重写。这样可以达到两个目的：\n主服务器还可以继续处理命令请求 子进程拥有服务器进程的数据副本，使用进程而不是线程，很好的避免了在使用锁的情况下，保证数据的安全性，因为线程会共享父进程的资源。 使用子进程带来的问题： 子进程在进行AOF重写期间，服务器还在处理命令请求，新的命令可能会对现有的数据库进行修改，那么子进程所重写的AOF文件就有可能和数据库状态不一样。 解决方案： 设置一个AOF重写缓存区，在服务器进行命令操作后，同时把命令发送给AOF缓存区和AOF重写缓存区。\n接着再重写完成的时候，再把AOF重写缓存区中的内容写入到新的AOF文件中。 最后原子性的对AOF文件进行重命名，覆盖掉现有的AOF文件。 RDB和AOF的使用场景 RDB优劣势 优势：Redis只包含一个快照文件，方便备份，比如一天归档一次 劣势：在服务器故障时，会丢失大量数据 AOF优劣势 优势：Redis持久性会比较好，写操作由协议组成，非常容易解析。 劣势：AOF文件体积较大。 fork函数耗时问题 在主进程创建子进程的时候会调用fork函数。\nfork执行时，主进程要拷贝自己的页表给子进程，如果数据量很大，那就要占用大量的资源，在完成拷贝之前，整个Redis会被堵塞，无法进行任何操作。\n优化方式：\n控制Redis整体内存 合理的进行持久化操作 不要部署在虚拟机上 事件 Redis是一个事件驱动程序有一下两种事件：\n文件事件 时间事件 Redis对事件的处理都是同步、有序、原子的，不会出现中断事件、事件抢占\n文件事件 什么是文件事件：服务器与客户端产生的通信，服务器通过监听和处理这些事件实现网络通信。\nRedis基于Reactor模式开发了自己的文件事件处理器。是单线程的运行的。\n单线程如何监听大量客户端 文件处理器采用I/O多路复用程序来监听客户端的大量连接\nRedis 6.0 以后为什么又引入了多线程 需要提高网络IO的读写性能，但是在执行命令的时候依然是单线程顺序执行。\n时间事件 什么是时间事件：Redis的一些操作需要在指定的时间点执行。\n分为两类：\n定时事件 周期性事件 ","permalink":"https://zhoujze.github.io/en/posts/tech/redis-02-%E5%8D%95%E6%9C%BA%E6%95%B0%E6%8D%AE%E5%BA%93%E7%9A%84%E5%AE%9E%E7%8E%B0/","summary":"数据库 数据库键空间 Redis是一个key-value形式的数据服务器，服务器中的每个数据库都是一个redis.h/redisDb结构表示。redisDb中的dict字典属性记录了数据库中的所有键值对，这个字典称为键空间。 typedef struct redisDb { // ... // 数据库键空间，保存着数据库中的所有键值对 dict *dict; //","title":"Redis 02-单机数据库的实现"},{"content":"数据结构 1. 简单动态字符串 Redis只会使用C语言字符串作为字面量，而经常使用的是自己构建的简单字符串SDS(simple dynamic string)抽象类型。并把SDS作为默认的字符串表示。 SDS的结构 struct sdshdr { // 记录 buf 数组中已使用字节的数量 // 等于 SDS 所保存字符串的长度 int len; // 记录 buf 数组中未使用字节的数量 int free; // 字节数组，用于保存字符串 char buf[]; }; 例子\nfree：没有分配未使用的字节 len：字节长度为5的字符串 buf：char类型的字节数组遵循C语言字符串定义以空字符串结尾,但是不算在len的长度里 SDS与C语言字符串的区别 常数复杂度获取数组长度 在C语言中获取字符串长度需要遍历整个字符串，时间复杂度为O(n) 在SDS中有内置的len属性记录了字符串的长度，时间复杂度为O(1) 杜绝缓存区溢出 在C语言中字符串是不记录自身长度的。 我需要在该字符串后拼接新的字符串，用到\u0026lt;string.h\u0026gt;/strcat函数进行拼接，src拼接到dest后。 char *strcat(char *dest, const char *src); 因为C字符串不记录自身长度，所以在C语言里默认你的字符数组是可以容纳src中的所有内容。一旦数组无法容纳，就会出现缓冲区泄露问题。 实例： 内存中有两个紧贴的字符串s1和s2，s1=\u0026quot;Redis\u0026quot; s2=\u0026quot;MongoDB\u0026quot;\n执行Redis命令 strcat(s1, \u0026#34; Cluster\u0026#34;); 因未对s1分配足够的空间，出现数据溢出，覆盖s2 在SDS中有用于执行字符串拼接的函数sdscat 执行命令 sdscat(s, \u0026#34; Cluster\u0026#34;); 在s后拼接Cluster，sdscat在拼接前会检查s长度是否足够，如不足则会扩展长度，然后再拼接。 sdscat命令执行后不仅执行了拼接操作还分配了13字节的未使用空间。len和free相等，涉及到空间分配策略。 减少修改字符串长度时所涉及的内存重分配次数 C语言字符串如果要修改长度 拼接字符串(append)：内存重分配来扩展底层数组的长度，如果忘记就会出现缓冲区溢出 截断字符串(trim)：内存重分配释放多余的字符串空间 如果忘记就会出现内存泄漏 SDS修改字符串长度 拼接字符串(空间预分配)：SDS执行命令sdscat拼接字符串 free未分配空间足够拼接新的字符串——直接拼接 free未分配空间不够拼接新的字符串——对SDS进行空间扩展 如果扩展后的len\u0026lt;1MB，给free分配len大小的空间作为未分配空间。\n例如：修改后len变为13字节，那么程序也会分13字节的未使用空间，SDS的实际长度就为13byt+13byt+1byt=27byt，1字节为末尾空字符。 如果扩展后的len\u0026gt;1MB，给free分配1MB空间作为未分配空间。\n例如：修改后的len变为10MB，那么程序就会分1MB的未使用空间，SDS的实际长度就是10MB+1MB+1byt 缩短字符串(惰性空间释放)：SDS的API执行缩短字符串时，程序不会立即回收多余空间，而是把多余的空间用free记录，等待使用。 举例：删除SDS里所有的XY\n执行命令 sdstrim(s, \u0026#34;XY\u0026#34;); // 移除 SDS 字符串中的所有 \u0026#39;X\u0026#39; 和 \u0026#39;Y\u0026#39; SDS修改流程图 通过惰性空间释放，SDS避免了缩短字符串内存重分配问题，为将来增加字符串做了优化。 SDS也提供了API，真正的释放未使用空间。不用担心内存浪费问题。 二进制安全 C语言字符串在读入的时候会把空字符串作为结尾，这样就会出现如果读入\u0026quot; hello\u0026quot;空字符在最前、\u0026quot;hello world\u0026quot;空字符在中间的情况，读入不正常。——这样的限制会出现C字符串只能保存一些文本数据，而不能保存音频，图片、视频等二进制数据。 SDS则没有对读入的数据做任何限制——SDS里的len属性值是用空字符串来判断结束的。你写入数据什么样子，读取就是什么样子。\n所有SDS的API都是用二进制的方式处理SDS中的buf里的数据，所以是二进制安全的。\n这也是我们将SDS的buf属性称为字节数组的原因——Redis 不是用这个数组来保存字符，而是用它来保存一系列二进制数据。 兼容部分C语言字符串函数 通过遵循C语言字符串的以空字符结尾，SDS可以在有需要的时候重用\u0026lt;string.h\u0026gt;里的函数库，避免代码重复。 SDS API 函数 作用 时间复杂度 sdsnew 创建一个包含给定 C 字符串的 SDS 。 O(N) ， N 为给定 C 字符串的长度。 sdsempty 创建一个不包含任何内容的空 SDS 。 O(1) sdsfree 释放给定的 SDS 。 O(1) sdslen 返回 SDS 的已使用空间字节数。 这个值可以通过读取 SDS 的 len 属性来直接获得，复杂度为 O(1) 。 sdsavail 返回 SDS 的未使用空间字节数。 这个值可以通过读取 SDS 的 free 属性来直接获得，复杂度为 O(1) 。 sdsdup 创建一个给定 SDS 的副本（copy）。 O(N) ， N 为给定 SDS 的长度。 sdsclear 清空 SDS 保存的字符串内容。 因为惰性空间释放策略，复杂度为 O(1) 。 sdscat 将给定 C 字符串拼接到 SDS字符串的末尾。 O(N) ， N 为被拼接 C 字符串的长度。 sdscatsds 将给定 SDS 字符串拼接到另一个 SDS字符串的末尾。 O(N) ， N 为被拼接 SDS 字符串的长度。 sdscpy 将给定的 C 字符串复制到 SDS 里面，覆盖 SDS 原有的字符串。 O(N) ， N 为被复制 C 字符串的长度。 sdsgrowzero 用空字符将 SDS 扩展至给定长度。 O(N) ， N 为扩展新增的字节数。 sdsrange 保留 SDS 给定区间内的数据，不在区间内的数据会被覆盖或清除。 O(N) ， N 为被保留数据的字节数。 sdstrim 接受一个 SDS 和一个 C 字符串作为参数，从 SDS 左右两端分别移除所有在 C字符串中出现过的字符。 O(M*N) ， M 为 SDS 的长度，N 为给定 C 字符串的长度。 sdscmp 对比两个 SDS 字符串是否相同。 O(N) ， N 为两个 SDS 中较短的那个 SDS的长度。 2. 链表 链表有高效的节点重排能力，对增加、删除比较灵活。\n链表作为一种常用的数据结构，内置在很多编程语言中，Redis所用的C语言并没有内置链表数据结构，所有构建了自己的链表结构。\n当一个列表键包含的数据比较多的时候，或者列表里字符串都比较长的时候，Redis就会使用链表作为底层实现。\n链表和链表节点 链表节点 typedef struct listNode { // 前置节点 struct listNode *prev; // 后置节点 struct listNode *next; // 节点的值 void *value; } listNode; 多个链表节点通过prev和next组成双端链表。 使用list来持有链表 typedef struct list { // 表头节点 listNode *head; // 表尾节点 listNode *tail; // 链表所包含的节点数量 unsigned long len; // 节点值复制函数 void *(*dup)(void *ptr); // 节点值释放函数 void (*free)(void *ptr); // 节点值对比函数 int (*match)(void *ptr, void *key); } list; list结构提供了表头节点head、表尾节点tail、链表长度计数器len、实现多态链表所需的类型特定函数： * dup：用于复制链表节点所保存的值 * free：用于释放链表节点所保存的值 * match：用于对比链表节点所保存的值是否和输入的值相等 Redis链表实现的特性 双端：链表节点有前置节点prev、后置节点next 无头：表头的prev节点和表尾的next节点都是NULL，对链表的访问都是以NULL结尾 有链表节点计数器：list结构提供了链表节点计数器，查找链表节点数的时间复杂度为O(1) 带表头指针和表尾指针：list结构提供了表头节点head、表尾节点tail，查找表头和表尾节点的时间复杂度为O(1) 多态：通过list结构的dup free match 设置链表不同的类型特定函数，来实现链表保存各种不同类型的值。 链表API 函数 作用 时间复杂度 listSetDupMethod 将给定的函数设置为链表的节点值复制函数。 O(1)。 listGetDupMethod 返回链表当前正在使用的节点值复制函数。 O(1)。 listSetFreeMethod 将给定的函数设置为链表的节点值释放函数。 O(1)。 listGetFree 返回链表当前正在使用的节点值释放函数。 O(1)。 listSetMatchMethod 将给定的函数设置为链表的节点值对比函数。 O(1)。 listGetMatchMethod 返回链表当前正在使用的节点值对比函数。 O(1)。 listLength 返回链表的长度（包含了多少个节点）。 O(1)。 listFirst 返回链表的表头节点。 O(1)。 listLast 返回链表的表尾节点。 O(1)。 listPrevNode 返回给定节点的前置节点。 O(1)。 listNextNode 返回给定节点的后置节点。 O(1)。 listNodeValue 返回给定节点目前正在保存的值。 O(1)。 listCreate 创建一个不包含任何节点的新链表。 O(1)。 listAddNodeHead 将一个包含给定值的新节点添加到表头。 O(1)。 listAddNodeTail 将一个包含给定值的新节点添加到表尾。 O(1)。 listInsertNode 将一个包含给定值的新节点添加到节点前后。 O(1)。 listSearchKey 查找并返回链表中包含给定值的节点。 O(N)， N为链表长度。 listIndex 返回链表在给定索引上的节点。 O(N)， N为链表长度。 listDelNode 从链表中删除给定节点。 O(1)。 listRotate 将链表的表尾节点弹出并移到表头。 O(1)。 listDup 复制一个给定链表的副本。 O(N)， N为链表长度。 listRelease 释放给定链表以及链表中的所有节点。 O(N)， N为链表长度。 3. 字典（dict） 定义：字典又称为符号表、关联数组、映射。是一种以键值对形式保存的抽象数据结构。因为Redis所用的C语言底层没有这种数据结构，所有Redis自己构建了字典。 字典的实现 Redis的字典使用哈希表（数组＋链表）作为底层实现，一个哈希表里有多个哈希节点，每个节点里保存了字典中的一个键值对。 哈希表：(dictht) typedef struct dictht { // 哈希表数组 dictEntry **table; // 哈希表大小 unsigned long size; // 哈希表大小掩码，用于计算索引值 // 总是等于 size - 1 unsigned long sizemask; // 该哈希表已有节点的数量 unsigned long used; } dictht; table：数组，里面每个元素都指向dictEntry哈希节点。\nsize：哈希表的大小，即table数组的长度。\nsizemask：与哈希值一起决定一个键应该放在数组的哪个索引上，值总是等于size-1。\nused：哈希表已经有多少个哈希节点。\n一个有四个空节点的空哈希表结构图。 哈希节点：(dictEntry) typedef struct dictEntry { // 键 void *key; // 值 union { void *val; uint64_t u64; int64_t s64; } v; // 指向下个哈希表节点，形成链表 struct dictEntry *next; } dictEntry; key：表示键\nval：表示值 next：指针指向下一个哈希节点，next指针可以把哈希值相同的键值对连接在一起，解决键冲突问题\n如图，k0和k1哈希值相同，通过next指针连接在一起。 字典：(dict) typedef struct dict { // 类型特定函数 dictType *type; // 私有数据 void *privdata; // 哈希表 dictht ht[2]; // rehash 索引 // 当 rehash 不在进行时，值为 -1 int rehashidx; /* rehashing not in progress if rehashidx == -1 */ } dict; type，privdata：针对不同类型的键值对，实现多种形态的字典。\ntype：是一个指向dictType的指针。\nprivdata：保存特定类型键值对函数的可选参数。 dictType：是一簇操作特定类型键值对的函数。 typedef struct dictType { // 计算哈希值的函数 unsigned int (*hashFunction)(const void *key); // 复制键的函数 void *(*keyDup)(void *privdata, const void *key); // 复制值的函数 void *(*valDup)(void *privdata, const void *obj); // 对比键的函数 int (*keyCompare)(void *privdata, const void *key1, const void *key2); // 销毁键的函数 void (*keyDestructor)(void *privdata, void *key); // 销毁值的函数 void (*valDestructor)(void *privdata, void *obj); } dictType; ht：是一个包含两项的数组，每一项都是一个dictht哈希表。\nht[0]：默认只使用的哈希表。\nht[1]：在执行rehash重新散列时使用。 还有一个和rehash相关的属性rehashidx，记录了rehash的进度，如果没有在进行rehash值为-1。 * 字典整体结构图 哈希算法 当我们需要向字典里添加键值对的时候就要用到哈希算法。 流程：\n根据键计算哈希值、索引值 -\u0026gt; 根据索引值把包含这个价值对的哈希节点放到哈希表的数组的指定索引上 计算方法： # 使用字典设置的哈希函数，计算键 key 的哈希值 hash = dict-\u0026gt;type-\u0026gt;hashFunction(key); # 使用哈希表的 sizemask 属性和哈希值，计算出索引值 # 根据情况不同， ht[x] 可以是 ht[0] 或者 ht[1] index = hash \u0026amp; dict-\u0026gt;ht[x].sizemask; 解决键冲突 定义：哈希值相同，索引相同的键被分配到数组的同一个索引上。 解决方法：链地址法\n通过哈希节点里的next指针把索引相同的哈希节点连接在一起形成单向链表。 举例：k1和k2的索引值都为2。 添加k2 因哈希节点没有指向链表表尾的指针，所以每次添加键冲突的键值对都要放在头部。复杂度为O(1)。 重新散列(rehash) 定义：随着哈希表内保存的键值对的不断增多，为了让负载因子维持在一个合理的范围内，要对哈希表进行扩展或收缩。 负载因子：哈希表已保存节点数量 / 哈希表大小。\nload_factor = ht[0].used / ht[0].size 触发条件： 服务器没有执行bgsave bgrewriteaof 命令，负载因子\u0026gt;=1 服务器在执行bgsave bgrewriteaof 命令，负载因子\u0026gt;=5 负载因子\u0026lt;0.1 流程： 为ht[1]分配空间，大小规则如下： 扩展时，ht[0].used * 2\u0026lt;= 2^n，符合条件第一个2^n即空间大小。 缩小时，ht[0].used \u0026lt;= 2^n，符合条件第一个2^n即空间大小。 把ht[0]上的所有键值对rehash到ht[1]。\nrehash：重新计算哈希值和索引值，并放置到ht[1]。 释放ht[0]，把ht[1]设置为ht[0]，创建新的空白哈希表ht[1]。 举例：条件ht[0].used = 4 分配空间\nht[0].used * 2 = 4 * 2 = 8，比4大等的一个2^n为2^3=8。所以分配空间ht[1].size = 8。 转移键值对\n重新计算键值对的哈希值和索引值，然后放置到ht[1]。 标准化\n释放ht[0]，把ht[1]设置成ht[0]，重新创建新的空哈希表ht[1]。 渐进式rehash 定义：在哈希表数据比较庞大的时候，如果要一次性rehash，计算量太大，可能导致服务器在一段时间内停止工作。因此引入了渐进式rehash的方式，渐进式rehash时分多次，渐进式的rehash。 步骤： 为ht[1]分配空间。字典同时持有ht[0] ht[1]两个哈希表 在字典中为rehashidx赋值为0,表示开始工作 在rehash期间，每次对字典执行增删改查操作的同时，对索引为rehashidx的哈希节点进行rehash，并且让rehashidx + 1。 随着对字典的不断操作，rehash操作完成，rehashidx = -1。 采用分治思想，把rehash操作分摊到每次增删改查操作上，避免了集中rehash带来大庞大计算量。\n对于渐进式rehash，每次删改查操作都要对两张哈希表进行。并且新增的键值对都要存在ht[1]中。 字典常用API 函数 作用 时间复杂度 dictCreate 创建一个新的字典。 O(1)。 dictAdd 将给定的键值对添加到字典里面。 O(1)。 dictReplace 将给定的键值对添加到字典里面，如果键已经存在于字典，那么用新值取代原有的值。 O(1)。 dictFetchValue 返回给定键的值。 O(1)。 dictGetRandomKey 从字典中随机返回一个键值对。 O(1)。 dictDelete 从字典中删除给定键所对应的键值对。 O(1)。 dictRelease 释放给定字典，以及字典中包含的所有键值对。 O(N)，N为字典包含的键值对数量。 重点回顾 字典广泛应用于Redis实现各种功能，包括数据库和哈希键等。 Redis中的字典用自己构建的哈希表底层实现，每个字典中带有两个哈希表，一个作为日常使用，另一个作为rehash的时候使用。 当字典被用作数据库、哈希键的底层实现时，Redis用MurmurHash2算法来计算键的哈希值。 哈希表使用链地址法来解决哈希冲突，根本是哈希节点内置了next属性，当键值对的哈希值和索引值相同的时候，会在同一个索引上形成单向链表。 在哈希表进行rehash操作的时候，采用分治的思想，分多次、渐进式的对每个键值对执行rehash。 4. 跳跃表（skiplist） 定义： 跳表就是支持二分查找的有序链表 跳跃表的实现 跳表的整体结构 跳表由zskiplistNode跳表节点和zskiplist保存跳表信息 构成。 zskiplist属性： header：指向跳表头部 tail：指向跳表尾部 level：记录除表头外，最大的节点的层数 length：记录出表头外，总的节点个数 zskiplistNode属性： 层(level)：节点中用L1、L2、L3\u0026hellip;表示，每层有两个属性：\n前进指针：访问位于表尾的其他节点\n跨度：前进指针所指节点和当前节点的距离 后退(backward)：BW标记，位于当前节点的前一节点，在从表尾往表头遍历时使用 分值(score)：节点按分值从小到大排列 成员对象(obj)：o1、o2、o3，为节点所保存的成员对象 跳跃表API 函数 作用 时间复杂度 zslCreate 创建一个新的跳跃表。 O(1)。 zslFree 释放给定跳跃表，以及表中包含的所有节点。 O(N)，N为跳跃表的长度。 zslInsert 将包含给定成员和分值的新节点添加到跳跃表中。 平均 O(log N)，最坏 O(N)，N为跳跃表长度。 zslDelete 删除跳跃表中包含给定成员和分值的节点。 平均 O(log N)，最坏 O(N)，N为跳跃表长度。 zslGetRank 返回包含给定成员和分值的节点在跳跃表中的排位。 平均 O(log N)，最坏 O(N)，N为跳跃表长度。 zslGetElementByRank 返回跳跃表在给定排位上的节点。 平均 O(log N)，最坏 O(N)，N为跳跃表长度。 zslIsInRange 给定一个分值范围，检测是否在跳跃表的分值范围内。 O(1)。 zslFirstInRange 给定一个分值范围，返回第一个符合范围的节点。 平均 O(log N)，最坏 O(N)，N为跳跃表长度。 zslLastInRange 给定一个分值范围，返回最后一个符合范围的节点。 平均 O(log N)，最坏 O(N)，N为跳跃表长度。 zslDeleteRangeByScore 给定一个分值范围，删除范围内的所有节点。 O(N)，N为被删除节点数量。 zslDeleteRangeByRank 给定一个排位范围，删除范围内的所有节点。 O(N)，N为被删除节点数量。 重点回顾 跳表是有序集合的底层实现，除此之外没有应用 跳表是可以实现二分查找的有序链表 最底层包含所用元素 每个索引节点包含两个指针，向右和向下 跳表查询、插入、删除的时间复杂度为 O(logn)，与平衡二叉树接近 5. 整数集合(intset) 定义： 整数集合是Redis用于保存整数数值的集合抽象数据结构，且不会出现重复元素。 整数集合是集合键的底层实现之一。 当一个集合只包含整数，且数据量不大的时候，就会使用整数集合作为底层集合键的底层实现。 整数集合的实现 结构 typedef struct intset { // 编码方式 uint32_t encoding; // 集合包含的元素数量 uint32_t length; // 保存元素的数组 int8_t contents[]; } intset; encoding：\n属性值为INTSET_ENC_INT16，就是int16_t类型数组。（最小值为-32,768 ，最大值为32,767）\n属性值为INTSET_ENC_INT32，就是int32_t类型数组。（最小值为-2,147,483,648 ，最大值为2,147,483,647）\n属性值为INTSET_ENC_INT64，就是int64_t类型数组。（最小值为-9,223,372,036,854,775,808 ，最大值为9,223,372,036,854,775,807） length：contents数组的长度。 contents：整数集合的底层实现，从小到大排列，没有重复项。 升级 当我们新添加的元素类型比现有的所有数据类型都长的时候，就要进行数据类型的升级。 升级整数集合并添加新元素步骤： 根据新元素的类型扩展整个整数集合底层数组的空间大小，并为新元素分配空间。 将底层数组里现有的元素进行类型转换，并放在正确的位置。 将新元素添加到底层数组里。 举例：有一编码为INTSET_ENC_INT16的整数集合\ncontents数组所占位数：\n根据添加元素数据类型扩展空间\n为新元素分配空间并且对已有元素进行类型转换，放在正确位置。\n将新元素添加到底层数组里。\n修改整数集合encoding属性和length属性\n整数集合API 函数 作用 时间复杂度 intsetNew 创建一个新的整数集合。 O(1)。 intsetAdd 将给定元素添加到整数集合里面。 O(N)。 intsetRemove 从整数集合中移除给定元素。 O(N)。 intsetFind 检查给定值是否存在于集合。 O(log N)。 intsetRandom 从整数集合中随机返回一个元素。 O(1)。 intsetGet 取出底层数组在给定索引上的元素。 O(1)。 intsetLen 返回整数集合包含的元素个数。 O(1)。 intsetBlobLen 返回整数集合占用的内存字节数。 O(1)。 6. 压缩列表（ziplist） 定义： 是列表键和哈希键的底层实现之一 当列表键只包含少量列表项，并且每个列表项要么是小整数，要么是短字符串。 当哈希键只包含少量键值对，并且每个键值对的键和值要么是小整数，要么是短字符串。 压缩列表的构成 压缩列表由一组特殊编码的连续内存块组成的顺序型的数据结构，是为了节约内存而开发的。 压缩列表块的构成：\nzlbytes：长度4字节，用于保存压缩列表的总的占用字节数。在对压缩列表进行内存重分配或者计算zlend的位置的时候使用。 zltail：长度4字节，记录压缩列表表尾节点的地址距离表头节点的地址有多少字节。通过偏移量可以计算表尾节点的地址，无需遍历。 zllen：长度2字节，记录压缩列表的节点长度。 entryX：长度不定，用于记录字节数组或者整数数值。 zlend：长度1字节，用于标记压缩列表末端。 示例：\nzlbytes：属性值0xd2十进制为210，表示压缩列表总长210字节。 zltail：属性值0xb3十进制179，如图p指针指向表头节点，用p+179表示末尾节点的地址值。 zllen：属性值0x5十进制5，表示压缩列表包含5个节点 压缩列表节点构成 压缩列表的节点保存的是字节数组或者整数值。 字节数组规定如下： 长度小于等于2^6-1的字节数组 长度小于等于2^14-1的字节数组 长度小于等于2^32-1的字节数组 整数值规定如下 4位，且介于0-12直接的无符号整数 1字节长的有符号整数 3字节长的有符号整数 int16_t类型整数 int32_t类型整数 int64_t类型整数 压缩列表节点构成图\nprevious_entry_length：节点位单位，记录了前一节点的长度。属性可以为1节点或者5节点 如果前一节点小于256字节，那么长度为1字节，前一节点的信息保存在这里 如果前一节点大于256字节，那么长度为5字节，第1个字节用0xFE表示十进制的256字节，然后4位用于保存前一节点的长度。 可以有用此计算前一节点的起始地址，当前起始地址的指针-previous_entry_length得出。 encoding：记录content属性值的数据类型和长度。 content：记录节点的值，可以是字节数组或者整数值 连锁更新 因为previous_entry_length的特殊机制，如果现在设置一个压缩列表，内的节点长度都为250-253之间，那么如果要向表头新添加一个长度大于256字节的节点，那么原头节点的previous_entry_length就要进行内存重分配变成5个字节，然后这个节点整体长度也大于256字节了，就要对后一节点的previous_entry_length进行内存重分配。引发连锁反应。 示例： 设置一个压缩列表，内的节点长度都为250-253之间\n向表头新添加一个长度大于256字节的节点\n原头节点的previous_entry_length进行内存重分配变成5个字节。\n这个节点整体长度也大于256字节了，对后一节点的previous_entry_length进行内存重分配。\n引发连锁反应。\n同理删除节点也会出现连锁反应。 压缩列表API 函数 作用 算法复杂度 ziplistNew 创建一个新的压缩列表。 O(1)。 ziplistPush 创建一个包含给定值的新节点，并将新节点添加到压缩列表的表头或表尾。 平均 O(N)，最坏 O(N^2)。 ziplistInsert 将包含给定值的新节点插入到给定节点之后。 平均 O(N)，最坏 O(N^2)。 ziplistIndex 返回压缩列表给定索引上的节点。 O(N)。 ziplistFind 在压缩列表中查找并返回包含给定值的节点。 平均 O(N^2)，最坏 O(N^2)。 ziplistNext 返回给定节点的下一个节点。 O(1)。 ziplistPrev 返回给定节点的前一个节点。 O(1)。 ziplistGet 获取给定节点所保存的值。 O(1)。 ziplistDelete 从压缩列表中删除给定的节点。 平均 O(N)，最坏 O(N^2)。 ziplistDeleteRange 删除压缩列表在给定索引上的连续多个节点。 平均 O(N)，最坏 O(N^2)。 ziplistBlobLen 返回压缩列表目前占用的内存字节数。 O(1)。 ziplistLen 返回压缩列表目前包含的节点数量。 节点数量 \u0026lt; 65535 时 O(1)，节点数量 \u0026gt; 65535 时 O(N)。 重点回顾 压缩列表是为了节约内存开发的顺序型数据结构 压缩列表用于列表键和哈希键的底层实现之一 压缩列表里的节点可以是字节数组也可以是整数值 新增或删除节点都可能会出现连锁更新操作 对象 Redis创建了一个对象系统，把以上这些数据结构作为底层实现。 5大对象包括：字符串对象、列表对象、哈希对象、集合对象、有序集合对象。 通过这五种对象，Redis在执行命令之前根据对象的类型判断是否可以执行命令。可以针对不同的场景，用不同的数据结构实现。 Redis对象实现了基于引用计数技术的内存回收机制：当对象不再使用，自动释放内存。 Redis对象实现了基于引用计数技术的对象共享机制：在适当情况下多个数据库可以共享同一个对象来节约内存。 对象的类型和编码 Redis用对象来表示数据库中的键值对，当创建一个键值对的时候，至少会创建两个对象，键对象和值对象。 Redis中的对象redisObject结构： typedef struct redisObject { // 类型 unsigned type:4; // 编码 unsigned encoding:4; // 指向底层实现数据结构的指针 void *ptr; // ... } robj; type：对象的类型，值为一下常量中的一个\nREDIS_STRING：字符串对象 REDIS_LIST：列表对象 REDIS_HASH：哈希对象 REDIS_SET：集合对象 REDIS_ZSET：有序集合对象 示例：（用TYPE + 键名来查看值的对象类型） # 键为字符串对象，值为字符串对象 redis\u0026gt; SET msg \u0026#34;hello world\u0026#34; OK redis\u0026gt; TYPE msg string # 键为字符串对象，值为列表对象 redis\u0026gt; RPUSH numbers 1 3 5 (integer) 6 redis\u0026gt; TYPE numbers list # 键为字符串对象，值为哈希对象 redis\u0026gt; HMSET profile name Tome age 25 career Programmer OK redis\u0026gt; TYPE profile hash # 键为字符串对象，值为集合对象 redis\u0026gt; SADD fruits apple banana cherry (integer) 3 redis\u0026gt; TYPE fruits set # 键为字符串对象，值为有序集合对象 redis\u0026gt; ZADD price 8.5 apple 5.0 banana 6.0 cherry (integer) 3 redis\u0026gt; TYPE price zset encoding：记录值对象所使用的编码格式，通俗说就是这个对象使用什么数据结构作为底层实现。\n编码常量如下：\n编码常量 编码所对应的底层数据结构 REDIS_ENCODING_INT long 类型的整数 REDIS_ENCODING_EMBSTR embstr 编码的简单动态字符串 REDIS_ENCODING_RAW 简单动态字符串 REDIS_ENCODING_HT 字典 REDIS_ENCODING_LINKEDLIST 双端链表 REDIS_ENCODING_ZIPLIST 压缩列表 REDIS_ENCODING_INTSET 整数集合 REDIS_ENCODING_SKIPLIST 跳跃表和字典 每种类型的对象至少可以使用两种以上的编码格式，如下表\n类型 编码 对象 REDIS_STRING REDIS_ENCODING_INT 使用整数值实现的字符串对象。 REDIS_STRING REDIS_ENCODING_EMBSTR 使用 embstr 编码的简单动态字符串实现的字符串对象。 REDIS_STRING REDIS_ENCODING_RAW 使用简单动态字符串实现的字符串对象。 REDIS_LIST REDIS_ENCODING_ZIPLIST 使用压缩列表实现的列表对象。 REDIS_LIST REDIS_ENCODING_LINKEDLIST 使用双端链表实现的列表对象。 REDIS_HASH REDIS_ENCODING_ZIPLIST 使用压缩列表实现的哈希对象。 REDIS_HASH REDIS_ENCODING_HT 使用字典实现的哈希对象。 REDIS_SET REDIS_ENCODING_INTSET 使用整数集合实现的集合对象。 REDIS_SET REDIS_ENCODING_HT 使用字典实现的集合对象。 REDIS_ZSET REDIS_ENCODING_ZIPLIST 使用压缩列表实现的有序集合对象。 REDIS_ZSET REDIS_ENCODING_SKIPLIST 使用跳跃表和字典实现的有序集合对象。 使用命令OBJECT ENCODING + 键查看值对象的编码格式：\nredis\u0026gt; SET msg \u0026#34;hello wrold\u0026#34; OK redis\u0026gt; OBJECT ENCODING msg \u0026#34;embstr\u0026#34; redis\u0026gt; SET story \u0026#34;long long long long long long ago ...\u0026#34; OK redis\u0026gt; OBJECT ENCODING story \u0026#34;raw\u0026#34; redis\u0026gt; SADD numbers 1 3 5 (integer) 3 redis\u0026gt; OBJECT ENCODING numbers \u0026#34;intset\u0026#34; redis\u0026gt; SADD numbers \u0026#34;seven\u0026#34; (integer) 1 redis\u0026gt; OBJECT ENCODING numbers \u0026#34;hashtable\u0026#34; 字符串对象 编码格式：int、raw、embstr int 条件：输入值为整数值并且可以用long来表示 转换过程：ptr属性由*void转成long，编码格式设置为int redis\u0026gt; SET number 10086 OK redis\u0026gt; OBJECT ENCODING number \u0026#34;int\u0026#34; raw * 条件：字符串长度大于\u0026lt;font color=\u0026quot;red\u0026quot;\u0026gt;39\u0026lt;/font\u0026gt;字节\r* 转换过程：编码格式设置为`raw`\r```\rredis\u0026gt; SET story \u0026quot;Long, long, long ago there lived a king ...\u0026quot;\rOK\rredis\u0026gt; STRLEN story\r(integer) 43\rredis\u0026gt; OBJECT ENCODING story\r\u0026quot;raw\u0026quot;\r```\r![字符串raw对象](字符串对象sds.png)\rembstr 条件：字符串长度小于等于39字节 转换过程：调用一次内存分配一块连续的内存空间，空间包含redisObject sdshdr，编码格式设置为embstr redis\u0026gt; SET msg \u0026#34;hello\u0026#34; OK redis\u0026gt; OBJECT ENCODING msg \u0026#34;embstr\u0026#34; 好处： 只用分配一次内存空间 释放也只需释放一次 可以更好的利用缓存优势 注意事项 小数浮点类型的数据在Redis中也是用字符串进行保存，保存过程：先转换成字符串，再保存转换后的字符串。在对小数浮点类型的数据进行计算操作的时候先转换成浮点型再操作，再转换成字符串进行保存。 redis\u0026gt; SET pi 3.14 OK redis\u0026gt; OBJECT ENCODING pi \u0026#34;embstr\u0026#34; redis\u0026gt; INCRBYFLOAT pi 2.0 \u0026#34;5.14\u0026#34; redis\u0026gt; OBJECT ENCODING pi \u0026#34;embstr\u0026#34; 程序先取\u0026quot;3.14\u0026quot;转换成浮点值3.14，计算得5.14，再转换成\u0026quot;5.14\u0026quot;字符串进行储存。\n编码转换 int编码的字符串对象和emtstr编码的字符串对象，在满足条件的时候都会转换成raw编码。 如在int编码的字符串对象后加hello world，不满足int编码条件 在emtstr编码的字符串对象后加够39个字节以上。 字符串命令的实现 命令 int 编码的实现方法 embstr 编码的实现方法 raw 编码的实现方法 SET 使用 int 编码保存值。 使用 embstr 编码保存值。 使用 raw 编码保存值。 GET 将整数值转换为字符串值，然后返回。 直接返回字符串值。 直接返回字符串值。 APPEND 转换为 raw 编码，然后按 raw 编码方式操作。 转换为 raw 编码，然后按 raw 编码方式操作。 使用 sdscatlen 追加字符串到末尾。 INCRBYFLOAT 取出整数值，进行加法计算并保存。 取出字符串值，进行加法计算并保存。 取出字符串值，进行加法计算并保存。 INCRBY 进行整数加法计算并保存。 不支持，返回错误。 不支持，返回错误。 DECRBY 进行整数减法计算并保存。 不支持，返回错误。 不支持，返回错误。 STRLEN 将整数值转换为字符串值，计算并返回长度。 使用 sdslen 返回字符串长度。 使用 sdslen 返回字符串长度。 SETRANGE 转换为 raw 编码，然后按 raw 编码方式操作。 转换为 raw 编码，然后按 raw 编码方式操作。 在指定索引处设置字符。 GETRANGE 将整数值转换为字符串值，返回指定索引字符。 直接返回指定索引字符。 直接返回指定索引字符。 列表对象 编码格式：ziplist、linkedlist ziplist 条件：同时满足列表保存的所有字符串小于64字节，列表对象保存的元素数量小于512个。 底层实现：压缩列表 转换过程：ptr指向压缩列表结构 redis\u0026gt; RPUSH numbers 1 \u0026#34;three\u0026#34; 5 (integer) 3 linkedlist 底层实现：双端链表 转换过程：ptr指针指向双端链表的头节点\n对StringObject进行的简化\n编码转换 ziplist的条件不满足时触发编码转换。 示例： 因保存了长度太大的元素而进行编码转换 # 所有元素的长度都小于 64 字节 redis\u0026gt; RPUSH blah \u0026#34;hello\u0026#34; \u0026#34;world\u0026#34; \u0026#34;again\u0026#34; (integer) 3 redis\u0026gt; OBJECT ENCODING blah \u0026#34;ziplist\u0026#34; # 将一个 65 字节长的元素推入列表对象中 redis\u0026gt; RPUSH blah \u0026#34;wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww\u0026#34; (integer) 4 # 编码已改变 redis\u0026gt; OBJECT ENCODING blah \u0026#34;linkedlist\u0026#34; 因保存的元素数量过多而进行编码转换 # 列表对象包含 512 个元素 redis\u0026gt; EVAL \u0026#34;for i=1,512 do redis.call(\u0026#39;RPUSH\u0026#39;, KEYS[1], i) end\u0026#34; 1 \u0026#34;integers\u0026#34; (nil) redis\u0026gt; LLEN integers (integer) 512 redis\u0026gt; OBJECT ENCODING integers \u0026#34;ziplist\u0026#34; # 再向列表对象推入一个新元素，使得对象保存的元素数量达到 513 个 redis\u0026gt; RPUSH integers 513 (integer) 513 # 编码已改变 redis\u0026gt; OBJECT ENCODING integers \u0026#34;linkedlist\u0026#34; 列表命令的实现 命令 ziplist 编码的实现方法 linkedlist 编码的实现方法 LPUSH 调用 ziplistPush 函数，将新元素推入到压缩列表的表头。 调用 listAddNodeHead 函数，将新元素推入到双端链表的表头。 RPUSH 调用 ziplistPush 函数，将新元素推入到压缩列表的表尾。 调用 listAddNodeTail 函数，将新元素推入到双端链表的表尾。 LPOP 调用 ziplistIndex 函数定位压缩列表的表头节点，然后调用 ziplistDelete 函数删除表头节点。 调用 listFirst 函数定位双端链表的表头节点，然后调用 listDelNode 函数删除表头节点。 RPOP 调用 ziplistIndex 函数定位压缩列表的表尾节点，然后调用 ziplistDelete 函数删除表尾节点。 调用 listLast 函数定位双端链表的表尾节点，然后调用 listDelNode 函数删除表尾节点。 LINDEX 调用 ziplistIndex 函数定位压缩列表中的指定节点，然后返回节点所保存的元素。 调用 listIndex 函数定位双端链表中的指定节点，然后返回节点所保存的元素。 LLEN 调用 ziplistLen 函数返回压缩列表的长度。 调用 listLength 函数返回双端链表的长度。 LINSERT 插入新节点到压缩列表时，使用 ziplistPush 函数；插入新节点到其他位置时，使用 ziplistInsert 函数。 调用 listInsertNode 函数，将新节点插入到双端链表的指定位置。 LREM 遍历压缩列表节点，并调用 ziplistDelete 函数删除包含给定元素的节点。 遍历双端链表节点，并调用 listDelNode 函数删除包含给定元素的节点。 LTRIM 调用 ziplistDeleteRange 函数，删除压缩列表中所有不在指定索引范围内的节点。 遍历双端链表节点，并调用 listDelNode 函数删除链表中所有不在指定索引范围内的节点。 LSET 调用 ziplistDelete 函数删除压缩列表指定索引上的节点，然后调用 ziplistInsert 函数插入包含给定元素的新节点。 调用 listIndex 函数定位双端链表指定索引上的节点，然后通过赋值操作更新节点的值。 哈希对象 编码格式：ziplist、hashtable ziplist 条件：同时满足哈希对象保存的所有键值对的键和值小于64字节，哈希对象保存的键值对数量小于512个。 底层实现：压缩列表 转换过程：ptr指向压缩列表，每个新添加的键值对都加在表尾，键先压入，值再压入。 redis\u0026gt; HSET profile name \u0026#34;Tom\u0026#34; (integer) 1 redis\u0026gt; HSET profile age 25 (integer) 1 redis\u0026gt; HSET profile career \u0026#34;Programmer\u0026#34; (integer) 1 hashtable 底层实现：字典 转换过程：ptr指向dict字典 编码转化 ziplist条件不满足时转换成hashtable 哈希对象命令的实现 命令 ziplist 编码实现方法 hashtable 编码的实现方法 HSET 调用 ziplistPush 函数，将键和值分别推入到压缩列表的表尾。 调用 dictAdd 函数，将新节点添加到字典里面。 HGET 调用 ziplistFind 函数，在压缩列表中查找指定键所对应的节点，然后返回值节点。 调用 dictFind 函数，在字典中查找指定键，返回对应的值。 HEXISTS 调用 ziplistFind 函数，在压缩列表中查找指定键所对应的节点，存在则返回1，不存在返回0。 调用 dictFind 函数，在字典中查找指定键，存在则返回1，不存在返回0。 HDEL 调用 ziplistFind 函数，在压缩列表中查找指定键所对应的节点，然后删除键节点和值节点。 调用 dictDelete 函数，从字典中删除指定键的键值对。 HLEN 调用 ziplistLen 函数取得压缩列表节点数量，然后除以2得到键值对数量。 调用 dictSize 函数，返回字典中键值对的数量。 HGETALL 遍历压缩列表，使用 ziplistGet 函数返回所有的键和值。 遍历字典，使用 dictGetKey 函数返回键，使用 dictGetVal 函数返回值。 集合对象 编码格式：intset、hashtable intset 条件：同时满足集合对象保存的所有元素都是整数值，集合对象保存的元素数量小于等于512个。 底层实现：整数集合 转化过程：ptr指向intset redis\u0026gt; SADD numbers 1 3 5 (integer) 3 hashtable 底层实现：字典 转化过程：ptr指向dict\n编码转换 不满足intset编码自动转换成hashtable编码 集合对象命令的实现 命令 intset 编码的实现方法 hashtable 编码的实现方法 SADD 调用 intsetAdd 函数，将新元素添加到整数集合中。 调用 dictAdd 函数，以新元素为键，NULL 为值，将键值对添加到字典中。 SCARD 调用 intsetLen 函数，返回整数集合的元素数量，这个数量即集合的元素数量。 调用 dictSize 函数，返回字典的键值对数量，这个数量即集合的元素数量。 SISMEMBER 调用 intsetFind 函数，在整数集合中查找给定元素，存在返回1，不存在返回0。 调用 dictFind 函数，在字典中查找给定元素，存在返回1，不存在返回0。 SMEMBERS 遍历整数集合，使用 intsetGet 函数返回所有的元素。 遍历字典，使用 dictGetKey 函数返回所有键作为集合元素。 SRANDMEMBER 调用 intsetRandom 函数，从整数集合中随机返回一个元素。 调用 dictGetRandomKey 函数，从字典中随机返回一个键。 SPOP 调用 intsetRandom 函数，随机返回整数集合中的一个元素，然后从集合中删除。 调用 dictGetRandomKey 函数，随机返回字典中的一个键，然后从字典中删除。 SREM 调用 intsetRemove 函数，从整数集合中删除所有给定元素。 调用 dictDelete 函数，从字典中删除所有键为给定元素的键值对。 有序集合对象 编码格式：ziplist、skiplist ziplist 条件：同时满足有序集合对象保存的元素小于128个，有序集合对象保存的所有元素长度小于64字节。 底层实现：压缩列表 转化过程：ptr指向压缩列表，集合元素按从小到大排序，小的在表头，大的在表尾。 redis\u0026gt; ZADD price 8.5 apple 5.0 banana 6.0 cherry (integer) 3 skiplist 底层实现：zset结构作为底层实现，包含跳跃表和字典。 typedef struct zset { zskiplist *zsl; dict *dict; } zset; 为什么同时使用跳跃表和字典来保存有序集合对象 跳跃表是一种有序的数据结构，支持范围查找，提高了有序集合在范围操作上的性能。 字典对单条数据可以快速的查找和更新，并且字典是无序的。 要注意，跳跃表和字典共享元素的成员和分量。所以性能不会差。 结构图 注意：图中的StringObject是共享的图中为了方便给分开了。 编码转换 有序集合对象内的元素同时满足元素个数小于128个，所有元素的长度小于64字节；使用ziplist，否则使用skiplist。 示例：\n元素过多导致编码转换。 # 对象包含了 128 个元素 redis\u0026gt; EVAL \u0026#34;for i=1, 128 do redis.call(\u0026#39;ZADD\u0026#39;, KEYS[1], i, i) end\u0026#34; 1 numbers (nil) redis\u0026gt; ZCARD numbers (integer) 128 redis\u0026gt; OBJECT ENCODING numbers \u0026#34;ziplist\u0026#34; # 再添加一个新元素 redis\u0026gt; ZADD numbers 3.14 pi (integer) 1 # 对象包含的元素数量变为 129 个 redis\u0026gt; ZCARD numbers (integer) 129 # 编码已改变 redis\u0026gt; OBJECT ENCODING numbers \u0026#34;skiplist\u0026#34; 元素过长导致编码转换 # 向有序集合添加一个成员只有三字节长的元素 redis\u0026gt; ZADD blah 1.0 www (integer) 1 redis\u0026gt; OBJECT ENCODING blah \u0026#34;ziplist\u0026#34; # 向有序集合添加一个成员为 66 字节长的元素 redis\u0026gt; ZADD blah 2.0 oooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo (integer) 1 # 编码已改变 redis\u0026gt; OBJECT ENCODING blah \u0026#34;skiplist\u0026#34; 有序集合对象的命令实现 命令 ziplist 编码的实现方法 zset 编码的实现方法 ZADD 调用 ziplistInsert 函数，将成员和分值作为两个节点插入到压缩列表。 先调用 zslInsert 函数，将新元素添加到跳跃表，然后调用 dictAdd 函数，将新元素关联到字典。 ZCARD 调用 ziplistLen 函数，获取压缩列表的节点数量，除以 2 得出集合元素数量。 访问跳跃表数据结构的 length 属性，直接返回集合元素数量。 ZCOUNT 遍历压缩列表，统计分值在给定范围内的节点数量。 遍历跳跃表，统计分值在给定范围内的节点数量。 ZRANGE 从表头向表尾遍历压缩列表，返回给定索引范围内的所有元素。 从表头向表尾遍历跳跃表，返回给定索引范围内的所有元素。 ZREVRANGE 从表尾向表头遍历压缩列表，返回给定索引范围内的所有元素。 从表尾向表头遍历跳跃表，返回给定索引范围内的所有元素。 ZRANK 从表头向表尾遍历压缩列表，查找给定成员，记录经过节点数量，找到成员后的节点数量即排名。 从表头向表尾遍历跳跃表，查找给定成员，记录经过节点数量，找到成员后的节点数量即排名。 ZREVRANK 从表尾向表头遍历压缩列表，查找给定成员，记录经过节点数量，找到成员后的节点数量即排名。 从表尾向表头遍历跳跃表，查找给定成员，记录经过节点数量，找到成员后的节点数量即排名。 ZREM 遍历压缩列表，删除所有包含给定成员的节点，及其分值节点。 遍历跳跃表，删除所有包含给定成员的节点，解除字典中成员与分值的关联。 ZSCORE 遍历压缩列表，查找给定成员的节点，取出分值节点保存的分值。 直接从字典中取出给定成员的分值。 类型检查和命令多态 类型检查 Redis用于操作键的命令包括两种，公共命令和特定命令。 功共命令：可以对所有类型键执行，如：DEL 命令、 EXPIRE 命令、 RENAME 命令、 TYPE 命令、 OBJECT 命令。 特定命令：只对特定类型的键执行 SET 、 GET 、 APPEND 、 STRLEN 等命令只能对字符串键执行； HDEL 、 HSET 、 HGET 、 HLEN 等命令只能对哈希键执行； RPUSH 、 LPOP 、 LINSERT 、 LLEN 等命令只能对列表键执行； SADD 、 SPOP 、 SINTER 、 SCARD 等命令只能对集合键执行； ZADD 、 ZCARD 、 ZRANK 、 ZSCORE 等命令只能对有序集合键执行； 底层实现：底层通过redisObject里的type属性来进行判断。 示例： 执行LLEN命令，先检查redisObject里的type属性是否是REDIS_LIST。 是则执行相应函数，否则服务器拒接执行命令并返回类型错误。 命令多态 Redis会根据编码的不同执行不同的函数。 示例： 列表对象有ziplist和linkdelist两种编码格式 执行LLEN命令： ziplist程序执行ziplistLen函数来返回列表长度 linkedlist程序执行listLength函数返回双端链表的长度\n以上示例说明，命令存在多态，相同的命令不同编码都可以执行，执行的函数不相同。 同时我们也可以将公共命令作为多态命令。前者为对类型的多态，后者为对编码的多态。 内存回收 C语言中没有内置内存回收机制，所以Redis构建了一个引用计数技术实现内存回收机制 通过该机制，程序通过跟踪对象引用计数信息，在适当的时候释放对象并进行内存回收。 引用计数信息在redisObject中的refcount属性中。 typedef struct redisObject { // ... // 引用计数 int refcount; // ... } robj; 引用计数信息变化规则 创建新对象，引用计数值初始化为1 对象被新程序引用，引用计数器+1 对象不再被一个程序使用，引用计数-1 引用计数值=0，对象占用的内存释放。 对引用计数值操作的API 函数 作用 incrRefCount 将对象的引用计数值增一。 decrRefCount 将对象的引用计数值减一，当引用计数值为 0 时，释放对象。 resetRefCount 将对象的引用计数值设置为 0 ，但不释放对象。 对象的生命周期 创建对象、操作对象、释放对象\n// 创建一个字符串对象 s ，对象的引用计数为 1 robj *s = createStringObject(...) // 对象 s 执行各种操作 ... // 将对象 s 的引用计数减一，使得对象的引用计数变为 0 // 导致对象 s 被释放 decrRefCount(s) 对象共享 基于引用计数计数的对象共享 只对基于整数的字符串进行共享。因为对于字符串来说，对象共享进行比较的时间复杂度可能会很高，消耗性能。 示例： redis\u0026gt; SET A 100 OK redis\u0026gt; OBJECT REFCOUNT A (integer) 2 redis\u0026gt; SET B 100 OK redis\u0026gt; OBJECT REFCOUNT A (integer) 3 redis\u0026gt; OBJECT REFCOUNT B (integer) 3 适用于字符串和数据结构中嵌套了字符串对象的对象，如linklist编码的列表对象、hashtable编码的哈希对象、hashtable编码的集合对象、zset编码的有序集合对象。 对象的空转时长 redisObject中的lru属性记录了对象最后一次被程序访问的时间。 我们通过OBJECT IDLETIME命令打印出给定键的空转时长，空转时长=当前时间-lru redis\u0026gt; SET msg \u0026#34;hello world\u0026#34; OK # 等待一小段时间 redis\u0026gt; OBJECT IDLETIME msg (integer) 20 # 等待一阵子 redis\u0026gt; OBJECT IDLETIME msg (integer) 180 # 访问 msg 键的值 redis\u0026gt; GET msg \u0026#34;hello world\u0026#34; # 键处于活跃状态，空转时长为 0 redis\u0026gt; OBJECT IDLETIME msg (integer) 0 注意OBJECT IDLETIME命令是特殊的，不算访问键的值对象。不会修改lru的值。 当服务器打开的maxmemory选项，并且服务器用于内存回收的算法为volatile-lru或者allkey-lru，那么当服务器占用内存超过了maxmemory选项上设置的限值，空转较高的那部分键会优先被释放，并且回收内存。 重点回顾 Redis中的每个键和值都是一个redisObject对象 Redis中有五大对象：字符串对象、列表对象、哈希对象、集合对象、有序集合对象。每个对象都有两种以上的编码格式，不同的编码格式可以应对不同的场景，提高对象效率。 服务器在执行命令的时候，会先检查键的类型是否可以执行。 Redis自己构建了基于引用计数计数的内存回收机制，当对象不再被使用的时候，释放回收内存。 Redis会共享0-9999的字符串对象 对象会记录最后一次被程序访问的时间，用于计算空转时长 ","permalink":"https://zhoujze.github.io/en/posts/tech/redis-01-%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E5%AF%B9%E8%B1%A1/","summary":"数据结构 1. 简单动态字符串 Redis只会使用C语言字符串作为字面量，而经常使用的是自己构建的简单字符串SDS(simple dynamic string)抽象类型。并把SDS作为默认的字符串表示。 SDS的结构 struct sdshdr { // 记录 buf 数组中已使用字节的数量 // 等于 SDS 所保存字符串的长度 int len; // 记录 buf 数组中未使用字节","title":"Redis 01-数据结构与对象"},{"content":"Java基础知识点汇总 0. Java语言特性 简单易学 具有面向对象三大特性：继承、封装、多态 平台无关性（由JVM虚拟机实现与平台无关） 可靠性 安全性 支持多线程 支持网络编程 编译与解释并存 与C++相比： 都是面向对象的语言，都支持继承封装多态 Java不提供指针来直接访问内存，更加安全 Java支持单继承，C++支持多重继承，但是Java中接口支持多重继承 Java有自动内存管理机制，不用程序员手动释放无用内存 1. 面向对象和面向过程区别 面向对象：易复用、易维护、易扩展。因为面向对象有继承、封装、多态的特性，实现的代码的低耦合度，使系统更加灵活、便于维护。 面向过程：性能要比面向对象高。因为类的调用需要实例化，消耗资源。所以当性能是首要选择的时候，我们常常选择面向过程开发。如单片机、嵌入式等。 总结：面向对象低耦合便于维护、易复用、易维护，但是性能没有面向过程高。 2. 面向对象的三大特性 继承： 是什么：继承是以已有的类为基础，扩展新类的技术。新类可以增加新的数据或者新的功能，他也可以复用父类的功能，但是不能选择性的继承父类，要继承父类的全部。 有什么用：通过使用继承我们可以很方便的复用以前的代码。 总结： 子类拥有父类的所有方法和属性，包括私有的方法和属性，但是私有的方法和属性子类无法直接调用，需要父类提供特定的方法去调用。 子类可以有自己的属性和方法。实现对父类的扩展。 子类可以用自己的方式实现父类的方法。即使用对父类方法的重写。 封装：把一个对象的属性进行私有化，然后提供可以被外界访问的属性的方法。典型就是实体类的标准JavaBean。 多态： 什么是多态：就是多种形态，具体来说就是同一件事情，发生在不同对象身上，就会产生不同的结果。 多态实现条件： 继承体系下 子类对父类方法的重写 通过父类的引用调用重写的方法 多态的体现：在代码运行的时候，传递不同类对现象的时候，会调用对应类中的方法 public class Main { public static void main(String[] args) { //向上转型 Animal dog = new Dog(); dog.eat(); // dog.dogShow;//编译看左边 在编译的时候animal类没有.dogShow方法，编译就会报错 Animal cat = new Cat(); cat.eat(); // cat.catShow; //结论：编译看左边，发生向上转型的时候只能调用父类有的成员和方法，不能调用子类特有的 //向下转型 Animal animal = dog; Dog dog1 = (Dog) animal;//animal本来就是dog，所以强制转换后不会抛出异常； // Cat cat1 = (Cat) animal;//强制将狗转换成猫，运行时抛出异常：ClassCastException //引入instanceof对类型进行判断，如果安全则为true if (animal instanceof Cat){ Cat cat2 = (Cat) animal; cat2.catShow(); } if (animal instanceof Dog){ Dog dog2 = (Dog) animal; dog2.dogShow(); } //sout //吃骨头 //吃猫粮 //汪汪汪 } } class Animal{ public void eat(){ System.out.println(\u0026#34;吃饭\u0026#34;); } } class Dog extends Animal{ @Override //@Override重写注解，用于对重写的方法进行检查，重写的方法名返回值和方法的参数列表一定要相同，且对访问修饰符不能做更严格的限制。 public void eat() { System.out.println(\u0026#34;吃骨头\u0026#34;); } public void dogShow(){ System.out.println(\u0026#34;汪汪汪\u0026#34;); } } class Cat extends Animal{ @Override public void eat() { System.out.println(\u0026#34;吃猫粮\u0026#34;); } public void catShow(){ System.out.println(\u0026#34;喵喵喵\u0026#34;); } } 重写涉及到动态绑定机制： 静态绑定(前期绑定):编译器在编译的时候就可以确定调用的方法\nfinal、static、private修饰的方法和构造函数为静态绑定 动态绑定(后期绑定):编译器在运行时才可以确定调用的方法 Java动态绑定机制 当调用对象方法时，该方法会和该对象的内存地址绑定。 当调用对象属性时，没有动态绑定，哪里声明哪里使用。 动态绑定的发生条件： 向上转型 Animal dog = new Dog(); 重写 class Dog extends Animal{ @Override public void eat() { System.out.println(\u0026#34;吃骨头\u0026#34;); } } 通过父类引用调用子类重写的父类方法。 dog.eat(); 向上转型特点： 可以调用父类里的所有成员 不能调用子类所特有的成员和方法。 运行时要看子类的具体表现，也就是子类所重写的父类方法。然后调用。 优点：让代码实现更加简单灵活。 缺点：不能调用子类所特有的成员和方法。 向下转型特点： 向下转型本质是在堆上创建了一个子类的对象赋值给父类的引用然后在回到子类的引用，可以使用子类所特有的方法。 因为在对上创建的子类对象不同，所以在最后回到子类的引用的时候可能是不安全的。有可能出现子类猫的堆赋值给了狗的引用，这是不安全的。因此Java引入了instanceof来判断是否安全 if (animal instanceof Cat){ Cat cat2 = (Cat) animal; cat2.catShow(); } 总结多态优缺点： 优点：降低圈复杂度，就是减少大量使用if-else，只需要在父类里定义方法，然后子类重写该方法，最后在使用的时候进行向上转型即可。可扩展能力强，只需创建继承父类的子类然后在重写方法即可。对于调用者来说只要创建新类的实例就可以了。 缺点：代码的运行效率低。属性没有多态，当父类和子类有同名的属性时，通过父类引用，只能引用到父类的成员属性。构造方法没有多态。 3. 抽象类 是什么：类中存在仅定义而未实现的方法 类中只要包含仅定义的方法均为抽象类 若子类未实现父类的全部方法，则也需要定义为抽象类 抽象类不可实例化 抽象类的定义格式 class abstract 类名 extends 父类{ 权限修饰符 属性1 权限修饰符 属性N 权限修饰符 类名1//构造器 权限修饰符 类名N 权限修饰符 方法1 权限修饰符 abstract 方法N } * 可以通过\u0026lt;font color=\u0026quot;red\u0026quot;\u0026gt;default\u0026lt;/font\u0026gt;指定默认的实现方法\r* 抽象类可以定义多个不需要实现的方法（用abstract修饰）\r* 可以定义抽象类型的变量\r4. 接口 定义：只有定义没有方法体的方法和全局常量组成的类。 接口的特性： 接口不可以被实例化，接口中不能有构造方法。 不可在接口里定义变量，接口里的变量都自动被public static final修饰 接口里的方法都自动被public abstract修饰，即接口中所有的方法都是抽象方法。 接口的实现类必须实现接口的全部方法，否则必须定义为抽象类 作用：实现多重继承的效果，同时避免复杂度和低效性。 接口的定义 访问修饰符 interface 接口名 extends 父接口1,父接口2,...{ 常量 方法 default 方法N(参数){ //提供默认实现方法 } } 接口的实现 访问修饰符 class 类名 extends 父类名 implements 接口1,接口2,...{ 自定义程序 实现接口方法 } 类可以同时实现多个接口，如果接口有冲突，需要在类里解决 如果父类继承的方法和接口的方法冲突则默认接口的方法被省略 接口规范（SPI）：接口和实现类的解耦 本质：应用程序根据接口调用实现类 实现徐奥引入依赖的java.util.ServiceLoader SPI需遍历并加载所有的实现类（无法做到按需加载） SPI调用流程 接口优点： 可实现一个类多个接口，打破了类继承的局限性 对外提供规则接口 降低了程序的耦合性，可实现模块化开发，定义好规则，提高了开发的效率 5. 抽象类和接口的区别 共性：不断抽取共性，没有具体的实现方法，都不能实例化。 区别 接口没有构造方法，抽象类有。 接口是对行为的抽象，是行为规范。抽象类是对类的抽象，是一种模板设计。 接口不能有具体的方法体，java1.8中可以定义default默认方法体，抽象类中可以有抽象方法也可以有普通带方法体的方法。 接口的实现类可以多接口实现，抽象类只能单继承。 接口成员变量和方法默认会被修饰，抽象类中有普通的方法，必须有被abstract修饰的抽象方法。 JVM知识点 1. JVM基础知识 1. JVM类文件结构 Java程序(.java)通过Java编译器(javac)编译成字节码文件(.class)以供Java虚拟机(JVM)解释成计算器可以识别的语言。 .class字节码文件是不同语言在Java虚拟机沟通的桥梁，同时也是Java可以跨平台的重要原因 字节码文件结构 魔数 确定一个文件是否是可以被JVM接收的Class文件 版本号（副版本号、主版本号） 在实际开发中开发和生产的JDK环境要一致 Java虚拟机可以执行低版本编译器生成的Class文件，反之不行 常量池（常量池计数器、常量池数据区） 常量池计数器 访问标志 类索引 父类索引 接口（接口计数器、接口数据区） 字段（字段计数器、字段数据区） 方法（方法计数器、方法数据区） 属性（属性计数器、属性方法区） 2. Java类的加载机制 作用：实现从二进制的数据文件到JVM虚拟机的内存中以供使用 加载过程：加载、连接、初始化 在类的加载过程中需要遵守JVM规范实现。 类的生命周期 加载：将源码转换为JVM字节流\n通过全限定名获取对应类的二进制字节流 把二进制字节流的静态存储结构转换为方法区的运行时数据结构 在堆中创建代表这个类的Java.lang.Class对象，作为方法区的运行时数据结构的访问入口。 验证：验证获取的字节流是否符合JVM规范\n文件格式：验证字节流是否符合Class文件格式规范。 元数据：分析字节码语义是否符合Java语言规范。 字节码：分析控制流和数据流，确保语义合法符合逻辑。 符号引用：验证类合法性，确保解析能正常执行。 准备：为类的静态变量分配内存，初始化为默认值\n为被static修饰的静态变量在方法区中分配内存 被分配内存的静态变量默认设置初始值为零值（0，0L，null，false等），在初始化阶段才去赋值。 如果类字段的字段属性中存在ConstantValue属性也就是，变量被static和final修饰，则一定要在准备阶段进行赋予ConstantVa属性的指定的值。 解析：JVM将常量池中的符号引用转换为直接引用\n作用：将类中特定的符号标记转换为实际储存的地址信息 解析主要针对：类、接口、字段、类方法、接口方法、方法类型、方法句柄、调用点限定符 符号引用：用特定的符号来描述目标\n直接引用：直接指向目标的指针、定位目标的句柄\n初始化：JVM对类变量进行初始化，静态变量赋予初始值。\nJVM初始化步骤： 如果类没有被加载、连接，则先对类进行加载、连接。 如果该类的直接父类没有被初始化，则先初始化直接父类。 如果有初始化语句，则顺序执行初始化语句。 执行初始化方法（clinit()方法） clinit()方法线程安全，多线程下存在堵塞风险。 clinit()方法是编译器自动生成的。 JVM初始化的触发条件： 创建类的实例，new操作 直接父类没有初始化先初始化直接父类 调用类或接口的静态变量或者对静态常量的赋值 调用类的静态方法 反射 Java虚拟机设置默认启动类的类 卸载：将字节流对象回收GC\n回收类中所有实例化对象 回收类的ClassLoader 回收没有被对象引用的类 类加载器（ClassLoader）:实现类的加载 本质：将源码转化为JVM中的字节流 每一个类都有对应的ClassLoader 数组类通过JVM创建，获取ClassLoader会基于元素的数据类型判断 ClassLoader加载类的流程 BootstrapClassLoader（启动类加载器）: 加载JDK内部核心库 ExtClassLoader（扩展类加载类）：加载JDK扩展库 AppClassLoader（应用程序类加载器）：用户类路径（ClassPath）所指定的类 BootstrapClassLoader是由C++实现的属于虚拟机的一部分，无法Java程序直接引用。其他类加载器都是继承自java.lang.ClassLoader 抽象类，这些类加载器需要由启动类加载器加载到内存中后才能去加载其它类。 自定义ClassLoader需要实现loadClass()或findClass()： loadClass()：加载指定二进制名称的类（打破双亲委派机制） findClass()：查找指定二进制名称的类 双亲委派：当ClassLoader加载时先交给其父加载器加载。最终由BootstrapClassLoader加载，如未找到再往下尝试加载类。 所有的ClassLoader都要遵循双亲委派（BootstrapClassLoader除外） 父ClassLoader不仅尝试加载类，还会查找相关类的相关资源 双亲委派不是强制约束，仅时JDK建议的方式 优点： 避免类被重复加载和核心库被修改 通过责任链设计模式实现类加载器的高扩展和解耦性。 2. JVM内存结构 运行时数据区 Java虚拟机在执行Java程序的过程中会把它管理的内存划分成不同的数据区域。\nJDK 1.8和之前版本略有不同。\nJDK 1.8之前：\nJDK 1.8：\n线程私有：\n程序计数器 本地方法栈 程序计数器 线程共享：\n堆 方法区 直接内存 程序计数器 字节码解释器通过改变程序计数器依次读取指令，从而实现代码的流程控制。 在多线程下，程序计数器记录当前线程的执行位置，当进行线程切换，再切换回来的时候，通过程序计数器可以继续当前线程的工作。 虚拟机栈 执行Java方法，生成栈帧用于储存局部变量表，操作数栈，常量池引用等。\n本地方法栈 虚拟机使用到的 Native 方法服务。\n堆 用来储存对象实例和数组。\n同时也是GC垃圾回收的主要管理区域。\n方法区 可认为是堆的一部分，在JDK1.8的时候合并在直接内存的元空间里。\n用来储存已经被虚拟机加载的信息，常量、静态变量、即时编译器编译后的代码。\n运行时常量池 运行时常量池是方法区的一部分，JDK1.8的时候从方法区移出，放到堆内存中。\n直接内存 本机直接分配的内存，会受到本机总内存大小的限制。直接内存并不是虚拟机运行时数据区的一部分，但是也经常被使用。\n虚拟机创建对象全过程 对象的创建 类加载器检查 虚拟机收到new指令，首先去常量池中查找有无对应的符号引用以及符号引用所代表的类是否被加载、解析、初始化过。如果没有，要先进行类的加载过程。\n分配内存 在堆内存中为其分配内存空间。\n分配方式有两种：\n指针碰撞 空闲列表 选择哪种分配方式由Java堆是否规整决定，Java堆是否规整由垃圾收集器是否带有压缩功能决定。\n初始化零值 内存分配完成后，虚拟机需要将分配到的内存空间初始化为零值，这样在对象实例化字段没有赋初始值的时候，也可以被程序访问到对应的零值。\n设置对象头 虚拟机要堆对象进行必要的设置，例如对象时哪个类的实例、对象的哈希码、GC分代年龄信息等。这些信息存放在对象头中。\n执行init方法 以上步骤执行完成后，从虚拟机角度看完成了对象的创建，但是从Java程序角度看，对象创建才刚刚开始。init字段没有执行，所有字段都还为零值。在new之后执行init方法，把对象按照程序员意愿初始化，才算对象完成创建。\n对象的内存布局 对象内存分为三部分：对象头、实例数据、对齐填充\n对象头：对象头分为两部分 储存自身的自身运行时数据（哈希码、GC年龄、锁状态等） 类型指针，通过指针确定对象是哪个类的实例 实例数据：对象真正储存的有效数据 对齐填充：占位作用（对象头必须为8字节的整数倍） 对象的访问定位 对象的访问方式由虚拟机决定，有两种：\n句柄：在Java堆中划分内存作为句柄池。拥有稳定的句柄池地址。 直接指针：包含在对象实例数据里。拥有较快的访问速度。 重点补充 String和常量池 String str1 = \u0026#34;abcd\u0026#34;;//先检查字符串常量池中有没有\u0026#34;abcd\u0026#34;，如果字符串常量池中没有，则创建一个，然后 str1 指向字符串常量池中的对象，如果有，则直接将 str1 指向\u0026#34;abcd\u0026#34;\u0026#34; String str2 = new String(\u0026#34;abcd\u0026#34;);//堆中创建一个新的对象 String str3 = new String(\u0026#34;abcd\u0026#34;);//堆中创建一个新的对象 System.out.println(str1==str2);//false System.out.println(str2==str3);//false String创建方式有两种：\n方式是在常量池中拿对象； 方式是直接在堆内存空间创建一个新的对象。 只要new就创建新的对象（新的内存地址）\nString s1 = new String(\u0026#34;计算机\u0026#34;); String s2 = s1.intern(); String s3 = \u0026#34;计算机\u0026#34;; System.out.println(s2);//计算机 System.out.println(s1 == s2);//false，因为一个是堆内存中的 String 对象一个是常量池中的 String 对象， System.out.println(s3 == s2);//true，因为两个都是常量池中的 String 对象 双引号声明的String，直接储存在常量池中 new 声明的String，先从常量池中查有无，如有则直接在堆空间创建。如无则现在常量池中创建，再在堆空间创建。 字符串拼接：\nString str1 = \u0026#34;str\u0026#34;; String str2 = \u0026#34;ing\u0026#34;; String str3 = \u0026#34;str\u0026#34; + \u0026#34;ing\u0026#34;;//常量池中的对象 String str4 = str1 + str2; //在堆上创建的新的对象 String str5 = \u0026#34;string\u0026#34;;//常量池中的对象 System.out.println(str3 == str4);//false System.out.println(str3 == str5);//true System.out.println(str4 == str5);//false 字符串拼接优先使用StringBuild和StringBuffer\n8种基本数据类型的包装类和常量池 Java基本类型的包装类大部分都实现了常量池技术，即Byte,Short,Integer,Long,Character,Boolean；这五种包装类默认在常量池创建了[-128,127]区间的缓存数据。 两种浮点数类型的包装类Float,Double并没有实现常量池技术。 Integer i1 = 33; Integer i2 = 33; System.out.println(i1 == i2);// 输出 true Integer i11 = 333; Integer i22 = 333; System.out.println(i11 == i22);// 输出 false Double i3 = 1.2; Double i4 = 1.2; System.out.println(i3 == i4);// 输出 false 示例：\nInteger i1 = 40; Integer i2 = 40; Integer i3 = 0; Integer i4 = new Integer(40); Integer i5 = new Integer(40); Integer i6 = new Integer(0); System.out.println(\u0026#34;i1=i2 \u0026#34; + (i1 == i2)); //true System.out.println(\u0026#34;i1=i2+i3 \u0026#34; + (i1 == i2 + i3)); //true System.out.println(\u0026#34;i1=i4 \u0026#34; + (i1 == i4)); //false System.out.println(\u0026#34;i4=i5 \u0026#34; + (i4 == i5)); //false System.out.println(\u0026#34;i4=i5+i6 \u0026#34; + (i4 == i5 + i6)); //true System.out.println(\u0026#34;40=i5+i6 \u0026#34; + (40 == i5 + i6)); //true 解释：\n语句 i4 == i5 + i6，因为+这个操作符不适用于 Integer 对象。\n首先 i5 和 i6 进行自动拆箱操作，进行数值相加，即 i4 == 40。\n然后 Integer 对象无法与数值进行直接比较，所以 i4 自动拆箱转为 int 值 40，最终这条语句转为 40 == 40 进行数值比较。\n3. 垃圾回收——GC机制 什么是GC：GC是一种自动的存储管理机制，当程序分配内存使用完成后，这部分内存就会成为垃圾，需要释放。这种存储资源管理就称为垃圾回收。对于Java而言就是，自动进行垃圾回收的机制。\n什么是JVM垃圾回收 Java的自动内存管理主要是针对对象的内存分配与回收。\n核心功能：堆内存中对象的内存分配与回收。\nJava堆是垃圾收集器管理的主要区域，也被称为GC堆。\n垃圾收集器采用分代垃圾收集算法，把Java堆细分为新生代和老年代。再次细分为Eden空间、From Survivor、To Survivor 空间等。\n进一步划分的目的是更好地回收内存，或者更快地分配内存。\n堆的基本结构：\n新生代：eden 区、s0(“From”) 区、s1(“To”) 区 老年代：tentired 区 在一次新生代垃圾回收后，如果对象依旧存活，则会进入s1，年龄+1(Eden 区-\u0026gt;Survivor 区后对象的初始年龄变为 1)\n当年龄到一定程度(默认15岁), 晋级到老年代\n经过这次GC，eden区和From区被清空，然后From区和To区交换角色。\n不论怎么样，都会保证新的To区域是空的，然后当To区被填满，所有To区对象转入老年代。\n对象优先分配在eden区 主流的垃圾回收器都采用分代回收算法，因此需要将堆内存分为新生代和老年代。根据年代特性选择相应的垃圾收集算法。\n多数情况下，对象分配在eden区分配，当eden区没有足够空间分配的时候，虚拟机将发起一次MinorGC。\nMinor GC 和 Full GC 有什么不同呢？\n新生代GC Minor GC：新生代的垃圾收集动作，非常频繁，回收速度较快 老年代GC Full GC Major GC： 老年代的垃圾收集动作，出现Major GC 绝大多数情况下会出现一次Minor GC，Major GC速度比Minor GC 慢10倍以上 分配担保机制：\n有一个对象1占据了大部分的eden区，对象2在eden区没有足够的内存分配。\n虚拟机将发起一次Minor GC。GC期间发现对象1太大了无法存入Survivor区。\n通过分配担保机制直接把对象1转移到老年代，老年代区有足够的空间存放对象1，所以不会出现Full GC。\n大对象直接进入老年代 大对象：在内存上大量连续储存的对象（如字符串、数组）\n为什么直接进入老年代：\n因为大对象会占据大部分eden区，触发分配担保机制进行复制存入老年代区，降低效率。\n长期存活的对象进入老年代 虚拟机通过对象年龄计数器判断新生代和老年代。\n对象出生就会放在eden区，经过一次Minor GC，如果能够被survivor区容纳，那么年龄初始化为1，然后移入Survivor区。每次Minor GC年龄+1，当年龄增加到一定程度（默认为15岁），移入老年代区。\n动态的对象年龄判断 为了更好的适应不同程序的内存情况，虚拟机不是定死必须要达到某个年龄。\n当Survivor区相同年龄对象的内存总和大于Survivor区总内存的一半，那么将直接把比这部分对象大或者这部分对象，直接移入老年代区。\n对象死亡判断 1. 引用计数法 给对象添加一个引用计数器，每当对象被引用计数器+1，引用失效计数器-1。任何时候当计数器为0，则不会再被使用。\n简单高效，但是无法处理对象间相互引用的问题。\n如，对象1和对象2相互引用，但是除此之外无任何引用。因为引用计数器的值一直不为0，所以引用计数算法没办法通知GC回收器回收他们。\n2. 可达性分析算法 引入GC Roots，把每个对象都称为GC Roots，从这个节点开始往下搜索，节点走过的路径称为引用链。当GC Roots没有任何引用链，那么此对象不可用。\n3. 再谈引用 对象存活判断都与 引用 有关。jdk1.2之后对引用进行了分类。\n强引用\n相当于必不可少的生活品。是我们最常用的引用，这种引用绝对不会被虚拟机回收。\n软引用\n相当于可有可无的生活品。内存空间足够，不会回收；内存空间不够，回收对象。可用来实现内存敏感的高速缓存。\n弱引用\n相当于可有可无的生活品。弱引用和软引用的区别在于：弱引用具有更短的生命周期，只要被垃圾回收器发现，不论内存空间是否足够，直接回收。\n虚引用\n形同虚设，在任何时候都可能被垃圾回收器回收。 虚引用主要用来跟踪垃圾回收的活动\n程序中软引用用到较多，因为软引用可以加速JVM对垃圾回收器的回收速度，可以维护系统运行安全，防止内存泄漏等问题。\n4. 不可达的对象并非“非死不可” 可达性分析后，不可达的对象处于“缓刑阶段”。需要经过两次标记，才会真正死亡。\n第一次：可达性分析后，不可达的对象被第一次标记，并且进行筛选。筛选条件是对象有无必要执行finalize方法，当对象没有覆盖finalize方法，或者虚拟机已经执行过一次方法，那么虚拟机认为没必要执行。 第二次：被判定为需要执行的对象，放入一个队列里进行第二次标记，如果对象没有任何关连对象，那么就真的被回收。 5. 如何判断一个常量是废弃常量 运行时常量池内主要回收废弃常量。\n如何判断：\n假设有个字符串常量\u0026quot;abc\u0026quot;，如果没有任何的String对象引用该字符串。那就说明该字符串常量为废弃常量。在下次内存回收的时候，\u0026quot;abc\u0026quot;就会被回收。\n6. 如何判断一个类是无用的类 方法区主要回收无用类。\n如何判断，满足一下三个条件：\n该类的所有实例都被回收。 加载类的ClassLoader 被回收 该类对应的java.lang.Class没有任何引用，并且没有反射。 垃圾收集算法 1. 标记-清除算法 分类两个步骤，先标记所有需要回收的对象，然后统一回收对象。\n该算法是最基础的算法，后续算法都是依据该算法改善。\n带来了两个问题：\n效率问题 回收后，内存会出现大量不连续的碎片空间。 2. 复制算法 为了解决效率问题衍生出的算法。\n该算法把内存分为两相等个部分，每次使用其中一部分，当这部分使用完后，将还存活的对象复制到另一部分，接着把这部分空间清空。这样每次回收都是一半内存。\n3. 标记-整理算法 根据老年代提出的一种标记算法。\n标记过程与标记-清除算法一样，在整理的时候让所有存活的对象向一端移动。接着清理这个端边界以外的内存。\n4. 分代收集算法 当前虚拟机的垃圾收集都采用垃圾收集算法。\n根据对象的存活周期，划分几个不同的内存块。将java堆分为新生代和老年代，根据年龄特点，选择不同的垃圾收集算法。\n如：\n新生代中：每次收集都会有大量对象死去，所以可以原则复制算法。 老年代中：老年代中都是存活率很高的对象，并且没有额外的空间对它进行分配担保，就需要采用标记-清除算法，标记-整理算法。 垃圾收集器 垃圾收集算法是 方法，垃圾收集器是 实现。\n没有最好的垃圾收集器，根据场景选择适合的垃圾收集器\n1. Serial收集器 Serial（串行）收集器，最基本、久远的垃圾收集器。\n单线程收集器。它在垃圾收集工作的时候，必须暂停别的所有工作线程。\n新生代采用复制算法，老年代采用标记-整理算法。\n优点：简单高效。\n2. ParNew收集器 Serial收集器的多线程版本。算法和它一样。\n并行：多条垃圾收集线程一起处理，用户线程等待。 并发：用户线程和垃圾收集线程同时执行，（不一定并行，可能交替），用户程序继续执行，垃圾收集在另外CPU上。 3. Parallel Scavenge收集器 基本和ParNew收集器差不多，但是该收集器更注重吞吐量（高效利用CPU）。\n4. Serial Old收集器 Serial收集器的老年代版本。同样是单线程。\n5. Parallel Old收集器 Parallel Scavenge收集器的老年版本。\n4. CMS收集器 CMS（Concurrent Mark Sweep）收集器以获取最短回收停顿时间为目标的收集器，注重用户体验。\n真正意义上的并发收集器，实现垃圾收集线程和用户线程同时工作。\n标记-清除算法的实现。分为四个部分：\n初始标记 并发标记 重新标记 并发清除 优点：并发处理、低停顿\n缺点：\n对CPU资源敏感 无法处理浮动垃圾 清除后会出现大量碎片空间。 5. G1收集器 面向服务器的收集器，以极高的概率满足GC低停顿和高吞吐量。\n标记-收集算法\n特点：\n并行和并发 分代收集 空间整理 可预测停顿 实现步骤：\n初始标记 并发标记 最终标记 筛选回收 4. JVM调优 GC性能指标和基本调优策略 GC性能衡量指标 吞吐量 停顿时间 垃圾回收频率 GC调优策略 降低 Minor GC 频率 因新生代空间较小，eden区很快被填满，导致的频繁Minor GC。\n通过增大新生代空间来降低Minor GC频率。\n因空间增大导致的Minor GC时间增加问题：\n通常在虚拟机中复制的成本远大于扫描。\n如果在堆内存中存在较多的长期存活对象，此时增加新生代空间，由于要复制的存活对象增对，反而会导致Minor GC时间增加。 如果堆内存中短期对象很多，那么扩容后，单次Minor GC时间不会显著增加。 所以，单次Minor GC的时长取决于GC后存活的对象数量，而非eden区大小。\n降低 Full GC 频率 由于堆内存不足，或者老年代对象太多导致Full GC。\n减少创建大对象 增大堆内存空间 选择合适的GC回收器 当我们以用户体验为第一的时候，一般选择CMS收集器，或者G1收集器。\n当我们对吞吐量有要求的时候，可选择Parallel Scavenge回收器来提高系统的吞吐量。\nJVM调优实例 1 ：线上CPU100%怎么排查 JVM调优实例 2 ：偏向锁导致Minor GC频繁问题 JVM调优实例 3 ：-Xmm参数设置错误导致的OMM JVM调优实例 4 ：连接池和Mybatis导致的OMM 多线程 1. 线程和进程 进程：进程是程序一次执行的过程，是系统运行的基本单位。系统运行就是一次进程创建运行消亡的过程。 线程：线程是比进程更小的执行单位，可多个存在在进程里。线程共享进程的堆内存和方法区，又有自己独立的虚拟机栈，本地方法栈和程序计数器。\n程序计数器： 字节码解释器通过程序计数器来依次读取命令。 多线程下，记录当前线程执行到的位置，以便线程切换回来时知道执行到哪里。 为什么私有：为了切换回当前线程时恢复到正确的执行位置。 虚拟机栈和本地方法栈： 虚拟机栈：执行Java方法，生成栈帧用于储存局部变量表，操作数栈，常量池引用等。 本地方法栈：虚拟机使用到的 Native 方法服务。 为什么私有：为了保证线程中的局部变量不被别的线程访问。 堆：储存新创建的对象。 方法区：存放编译器编译后的数据，如类信息，常量，静态变量。 2. 并行和并发 并行：同一时间点，多个线程同时执行。 并发：同一时间段内，多个线程执行（可能不在统一时间点）。 3. 为什么使用多线程： 总体：线程是程序执行的最小单位，多线程切换和调度成本远小于进程。多线程执行减少了线程上下文切换的时间。 入微：单核多线程可以提高效率，比如当一个线程先要使用cpu再使用IO，另一个线程先使用IO再使用cpu，当两个线程同时进行的时候效率都是百分百的。多核多线程提高了cpu的利用率，一个线程只被一个cpu核心利用，那么多个线程就可被多个cpu核心利用，大大提高了cpu利用率。 上下文切换：当前任务执行完cpu时间片会先保存当前状态，然后再切换到别的任务，当再切换回来时，还可以加载当前状态。概述:任务从保存到再加载还可以回到当前状态的过程\n4. 多线程存在的问题： 可能出现内存泄漏、上下文切换、死锁等问题。 5. 线程的生命周期 新建(new)：创建线程 可运行(runnable)：准备和运行两种状态的统称 阻塞(blocked)：线程阻塞于锁 等待(waiting)：线程进入等待状态，等待其他线程中断或者通知 超时等待(timed_waiting)：不是一直等待，超过指定时间自行返回 终止(terminated)：线程执行完毕\n6. 线程状态迁变图 经典五态模型\n7. 创建线程 线程类Thread，任务类Runnable Callable 线程是载体，任务才是线程具体做的事情。 Thread 线程创建的三种方式 直接继承Thread类 // 自定义线程对象 class Thread1 extends Thread { @Override public void run() { // 线程需要执行的任务 ...... } } // 创建线程对象 Thread1 t1 = new Thread1(); Thread+Runnable接口实现类（无返回值）\n有多个线程执行的任务是一样的。 我们可以把任务体和线程分开。\nThread中提供了包含Runnable类型参数的构造方法。 class MyRunnable implements Runnable { @Override public void run() { // 线程需要执行的任务 ...... } } // 创建任务类对象 MyRunnable runnable = new MyRunnable(); // 创建线程对象 Thread t2 = new Thread(runnable); 简化版 // 创建任务类对象 Runnable runnable = new Runnable() { public void run(){ // 要执行的任务 ...... } }; // 创建线程对象 Thread t2 = new Thread(runnable); Thread+Callable接口实现类（有返回值） class MyCallable implements Callable\u0026lt;Integer\u0026gt; { @Override public Integer call() throws Exception { // 要执行的任务 ...... return 100; } } // 将 Callable 包装成 FutureTask MyCallable callable = new MyCallable(); FutureTask\u0026lt;Integer\u0026gt; task = new FutureTask\u0026lt;\u0026gt;(callable); // 创建线程对象 Thread t3 = new Thread(task); 8. 启动线程为什么用start()而不用run() new一个Thread，就会新建一个线程。 调用start()，执行线程准备工作，自动调用run()。线程进入就绪状态，分配到时间片就执行。 如果直接调用run()，执行的是main线程下的一个普通方法体，不会在线程中去执行。 总结：\n调用start方法可以启动线程，并进入就绪状态。而调用run方法进入的是Thread中的一个普通方法，是在主线程中执行的。 9. 死锁 是什么：死锁是多个线程同时被堵塞，他们都在等一个资源被释放，大家都在在等，就形成了无限期堵塞，程序无法正常终止，形成死锁。 举例：线程A持有资源2想要获取资源1，线程B持有资源1想要获取资源B，他们都在等待对方释放资源，形成死锁。\n解决方案： 让线程A或者线程B通过Thread.sleep(1000)来释放资源，解决死锁。 死锁形成的四个条件： 互斥：资源只被一个线程占有。 请求与保持：线程请求资源而堵塞，以获得资源保持占有。 不剥夺：线程以获得的资源在没有释放前不能被其他线程剥夺。 循环等待：进程与进程之间形成首位相连的循环等待关系。 避免死锁： 破坏互斥：无法破坏 破坏请求与保持：一次性获取所有资源，再释放所有资源 破坏不剥夺：线程申请不到资源的时候主动释放自己的资源 破坏循环等待：按序申请资源访问 10. sleep()和wait()的区别和共同点 区别： sleep()没有释放锁，wait()方法释放了锁。 sleep()常用于暂停线程，wait()常用于线程之间通信 sleep()执行完成会自动苏醒，wait()不会自动苏醒，需要其它线程调用同一对象上的notify() notifyAll()方法苏醒。 共同点： 两者都可以暂停线程的执行 11. 锁的概念 乐观锁 | 悲观锁 悲观锁：获取数据先加锁，确保数据不会被修改 乐观锁：获取不加锁，更新的时候校验数据是否已经被修改，底层采用CAS算法实现。 自旋锁 | 自适应自旋锁 自旋锁： 在获取锁失败时自动进入阻塞队列，假设线程刚进入阻塞，别的线程就释放了锁，这样再去唤醒线程再去获取锁，就会造成系统性能了浪费。 自旋锁就是通过在获取锁失败的时候，不进入堵塞队列，多获取几遍锁，统称就是自旋。 底层源码层面就是利用了do while进行重试。 自适应自旋锁： 自旋锁会在获取锁失败的时候多重复获取锁，但是如果一直获取不到锁，就会造成系统性能浪费，还不如进入堵塞队列，等待唤醒。 自适应自旋锁就是代表获取的次数不再固定，而是通过规则来就决定 规则1：如果同一个锁上的对象刚刚成功获取了锁，那么系统就会认为很有可能再次获取成功，会多自旋等待。 规则2：如果对于一个锁，他很少有线程成功共获取锁，那么系统就会认为不能再获取锁，就会直接进入堵塞队列。 公平锁 | 非公平锁 公平锁：对于线程获取锁，遵守锁的申请顺序，按序获得锁。 非公平锁：线程获取先直接试着获取锁，如果获取不到，再采用公平锁进行排队获取。 通俗说：公平锁需要排队，非公平锁先插队，插队失败再排队。\n优缺点：\n公平锁：\n优点：线程不会一直堵塞。\n缺点：整体效率比非公平锁低，每个堵塞的线程都需要唤醒，造成系统性能消耗过大。\n非公平锁:\n优点：线程有几率可以直接获取锁，减少系统性能开销，提高性能。\n缺点：可能会造成有的线程会一直堵塞。\n可重入锁 | 不可重入锁 可重入锁：也称递归锁，一个线程可以重复获取同一锁。优点可一定程度上避免死锁。 不可重入锁：一个线程不能重复获取同一锁，即如果一个线程获取了一个锁，然后想要再次获取这个锁，就要先释放再重新获取。 以 synchronized 举例：\nsynchronized 是可重入锁，同一线程调用func1可以直接获得当前对象的锁，进入func2操作。\n如果时不可重入锁，那么线程在调用func1的时候需要把func1的锁释放掉才能进入func2，但是该对象锁又正在被当前线程持有，无法释放，形成死锁。\n共享锁 | 排它锁 共享锁：即读锁，可以被多个线程持有。如果线程A对数据1加共享锁，那么别的线程只能对数据1加共享锁，且只能读数据，不能修改数据。 排它锁：即读写锁，只能被一个线程持有，如果线程A对数据1加排它锁，那么别的线程就不能对数据1添加任何锁，且线程A可以对数据1进行读写操作。 Java 中的 synchronized 和 ReentrantLock 就是排他锁。\n","permalink":"https://zhoujze.github.io/en/posts/tech/java/","summary":"Java基础知识点汇总 0. Java语言特性 简单易学 具有面向对象三大特性：继承、封装、多态 平台无关性（由JVM虚拟机实现与平台无关） 可靠性 安全性 支持多线程 支持网络编程 编译与解释并存 与C++相比： 都是面向对象的语言，都支持继承封装多态 Java不提供指针来直接访问内存，更加安全 Java支","title":"Java"},{"content":" 待更新\n","permalink":"https://zhoujze.github.io/en/posts/life/life/","summary":"待更新","title":"Life"},{"content":" 待更新\n","permalink":"https://zhoujze.github.io/en/posts/read/read/","summary":"待更新","title":"Read"},{"content":"奇怪的介绍\n菜狗技术宅，代码是热爱，熬夜冠军🌛\n喜欢吃橘子的小橙子🍊\n我要变有钱！\n","permalink":"https://zhoujze.github.io/en/about/","summary":"奇怪的介绍 菜狗技术宅，代码是热爱，熬夜冠军🌛 喜欢吃橘子的小橙子🍊 我要变有钱！","title":"🙋🏻‍♂️关于"},{"content":"1. Linux环境安装MySQL 略\u0026hellip;\n2. MySQL数据目录 举例：数据库A，表a\n如果表a采用InnoDB，data\\A中会产生一个或两个文件 a.frm(5.7特有)：描述表结构，字段长度等 如果采用系统表空间模式，数据信息和索引信息储存在ibdata1中 如果采用独立表空间模式，data\\A中会产生a.ibd文件，储存数据信息和索引信息。 此外：\nMySQL5.7会在data\\A目录下生成db.opt用于保存数据库相关配置，如字符集、比较规则，而MySQL8.0不再提供。 MySQL不再单独提供a.frm文件，而是合并在a.ibd中 如果表a采用MyISAM，data\\A中会产生3个文件 MySQL5.7中，a.frm：描述表结构，字段长度等\nMySQL8.0中，a.XXX.sdi：描述表结构，字段长度等 a.MYD（MyData）：数据文件信息，储存数据信息（采用独立表存储模式） a.MYI（MyIndex）：存放索引信息文件 3. 用户与权限管理 1. 用户管理 MySQL分为普通用户和root用户。\n提供了许多语句来管理用户账号。\nMySQL数据库的安全性需要通过用户管理实现。\n1.1 登录用户 1.2 创建用户 1.3 修改用户 1.3 修改用户 1.4 删除用户 1.5 设置密码 权限管理 表权限 列权限 过程权限 权限授予原则 满足需求的最小权限 限制登录主机 设置密码 定期清理不需要的用户 授予权限 MySQL如何实现权限控制\n权限表 4. 逻辑架构 1. 逻辑架构 1.1 服务器处理客户端请求 Mysql是典型的C/S架构，Clinet/Server 架构，服务端使用的是mysqld。\n实现效果：客户端向服务端发送sql语句，服务端进程处理后向客户端返回处理结果。\n具体展开图:\n1.2 Connectors 连接器 1.3 第一层：连接层 1.4 第二层：服务层 1.5 第三层：引擎层 1.6 存储层 2. SQL执行流程 2.1 MySQL 中的SQL执行流程 查询缓存 8.0 舍弃，缓存命中率很低，需要sql完全一样才能命中，如空格等，也不行 对数据库进行增删改查的时候，存在缓存失效问题，需要从缓存中删除，更新压力大\n解析器 词法分析 语法分析 生成语法树 优化器\n在优化器中会确定 SQL 语句的执行路径，比如是根据全表检索 ，还是根据索引检索等 物理查询优化：索引表连接优化 逻辑查询优化：语法逻辑优化 生成执行计划 执行器\n执行器执行 执行计划 3. 数据库缓冲池(buffer pool) 内存级别的缓存，占用内存作为数据缓冲池\n3.1 缓冲池 vs 查询缓存 缓冲池和查询缓存是一个东西吗？不是。\n3.2 缓冲池 缓存原则 位置 * 频次原则\n优先对使用频次高的热数据进行加载\n3.3 缓冲池如何读取数据 重点回顾 5. 存储引擎 简而言之，存储引擎就是表的类型。它的功能：接收上层传下来的指令，然后对表中的数据进行提取或写入操作。\n查看存储引擎 命令：show engines; 引擎介绍 InnoDB引擎 MySQL 5.5 以后默认采用 优点： 具备外键支持功能的事务存储引擎 事务性引擎，确保事务的完整提交和回滚 除了增加和查询外，还要更新、删除操作，优先InnoDB 支持行级锁，只对单条数据加锁而不是整张表 缺点： 写的效率较差 对内存要求较高：.ibd文件索引即数据需全部加载，MyISAM索引和数据分开，只要加载索引。 MyISAM引擎 不支持事务：崩溃后无法安全恢复 不支持外键 不支持行级锁 优点 访问速度快 针对数据统计有常数存储 区别 外键 事务 行级锁 缓存 自带系统表使用 关注点 性能：节省资源、消耗少、简单业务 事务：并发写、事务、更大资源 默认安装 默认使用 Archive引擎：用于数据存档 仅支持插入和查询，无法修改删除 MySQL 5.5 以后支持索引 拥有很好的压缩机制，使用zlib压缩库 采用行级锁 适合日志和数据采集（归档）类应用 Blackhole引擎：丢弃写操作，读操作返回空内容 CSV引擎：存储数据，以逗号分隔各个数据项 Memory 引擎：置于内存中的表 媒介时内存，响应速度快，但是mysql守护进程崩溃时数据会丢失\n特征：\n同时支持哈希索引和B+索引 ","permalink":"https://zhoujze.github.io/en/posts/mysql/01-mysql%E6%9E%B6%E6%9E%84%E7%AF%87/","summary":"1. Linux环境安装MySQL 略\u0026hellip; 2. MySQL数据目录 举例：数据库A，表a 如果表a采用InnoDB，data\\A中会产生一个或两个文件 a.frm(5.7特有)：描述表结构，字段长度等 如果采用系统表空间模式，数据信息和索引信息储存在ibdata1中 如果采用独立表空间模","title":""},{"content":"1. 索引的数据结构 为什么使用索引 索引时一种快速找到数据记录的数据结构，类比书的目录。\n顺序查找磁盘I/O次数较多。\n搜索树，减少了磁盘I/O次数。\n索引优缺点 索引是什么 帮助MySQL高效获取数据的数据结构\n索引在储存引擎中实现，不同的储存索引不一定完全一样。\n优点 提高数据检索效率，降低数据IO成本 通过创建唯一索引，可以保证数据的唯一性 加速表与表中间的连接，如父子表联合查询 减少查询中的分组和排序时间 缺点 索引需要创建和维护，耗费时间 索引也要占用磁盘空间 提高查询速度，但是降低了增删改的速度。因为更新需要维护索引。 对于3问题，最好先删除索引。然后插入数据，完成后再创建索引。\nInnoDB中索引的推演 索引之前的查找 在一页内查找 假设目前表中的记录比较少，所有的记录都可以被存放在一个页中，在查找记录的时候可以根据搜索条件的不同分为两种情况：\n以主键为搜索条件：页目录中使用二分法快速定位对应槽，遍历槽对应分组中记录。 以其他列作为搜索条件：因数据页中没有对非主键简历页目录。通过最小记录依次遍历，找到。 在很多页查找 记录很多，有两步：\n定位到所在页 在所在页查找相应记录 没有索引的情况下，需要从头把数据页加载到内存然后依次遍历找到，耗时耗力。\n设计索引 简单索引方案 建表：\nCREATE TABLE index_demo( c1 INT, c2 INT, c3 CHAR(1), PRIMARY KEY(c1) ) ROW_FORMAT = COMPACT; 表示意图： record_type：记录类型，0普通记录，1目录记录，2最小记录，3最大记录。 next_record：指针，指向下一个记录。 添加记录：\n在原始数据1，3，5后添加主键为4的数据，因为主键递增。所以4，5互换位置，称为页分裂\n给所有的页简历目录项 因为数据页编号不连续，所以要找到记录需要遍历整个数据\n为数据页编号设置目录项\nkey：为页中最小的主键值 page_no：页号 目录项称为索引。\nInnDB中索引方案 迭代1次-目录项记录的页 目录项记录和普通用户记录区别：\nrecord_type目录项记录为1，普通为2 目录项记录页只有两项，普通由用户定义 （了解）记录头信息中有min_rec_mask属性，只有目录项记录页中为1，别的都为0 相同点：\n都是同样的数据页，都用页目录，都可以用二分加快查找速度 迭代2次-多个目录项记录的页 IO最多加载很多次。\n迭代3次-目录项记录页的目录页 IO最多加载3次。\n用图来描述\n这个数据结构，称为B+树。\nB+树通常不超过4层\n原因：层数越低，IO次数越少。4层所容纳的记录很庞大，4层以上没有必要。\n常见索引概念 聚簇索引 据此索引不是单独的索引类型，而是一种数据存储方式，所有用户记录保存在叶子节点，所谓索引即数据，数据即索引。\n特点：\n记录和页排序方向： 页内：按主键大小顺序排成单向链表 用户记录页：按页内主键大小顺序排成双向链表 目录项记录页：页内外都是通过主键大小顺序排成双向链表 叶子节点存储完整的用户数据 添加数据的时候，底层自动构建聚簇索引\n优点：\n数据访问更快 主键排序查找和范围查找速度块 节省大量IO操作 缺点：\n插入速度严重依赖于插入顺序 更新主键值代价很高 二级索引需要两次索引查找，第一次找主键，第二次找数据 限制：\n只有InnDB支持聚簇索引 如没有定义主键，InnDB会选择非空的唯一索引代替，如果没有InnDB会隐式定义一个主键。 InnDB表主键尽量选用有序的顺序Id，不建议用无需，无法宝珠主键数据的顺序长度。 二级索引（辅助索引、非聚簇索引） 非主键字段做索引，把主键做记录，对每个非主键字段都做一次索引。\n在通过非主键查询整个记录的时候，再通过查找到的c1主键回到聚簇索引查找其他字段的操作叫回表。\n聚簇索引于非聚簇索引原理不同，使用也有区别：\n聚簇索引的叶子节点存储的是数据记录，非聚簇索引叶子节点是数据位置，非聚簇索引不会影响数据表的物理存储顺序。 一个表只能由一个聚簇索引，因为只能有一种排序储存的方式，但是可以有多个非聚簇索引，也就是多个索引目录提供数据检索。 使用聚簇索引，数据查询效率高，但是插入、删除、更新操作效率比非聚簇索引低。 联合索引 两个字段进行联合索引，在c2相同的时候，按照c3的顺序排列。\nInnoDB的B+树索引的注意事项 根位置万年不动 在实际的B+树生成过程中是由上而下创建的。\n最初的时候只有一层根页面。 插入数据，当跟页面空间用完的时候，对根页面中的数据复制到新的数据页A内，然后对新页A进行页分裂得到新页B，这时新插入的数据根据顺序插入到新页A或者新页B。最后把根页面升级为存储目录项记录的页。 根节点从始至终都存在，不会移动。\n内节点中目录项记录的唯一性 在非聚簇索引中举例：\n构建二级索引：\n当要插入新数据：c1：9、c2：1、c3：\u0026lsquo;p\u0026rsquo;的时候，我们无法判断是在页4添加还是页5添加。没法保持唯一性。\n解决办法把主键添加进去：\n这样通过c1和c2就可以唯一的确定位置。\n一个页面最少存储2条记录 一个B+树只需要很少的层级就可以轻松存储数亿条记录，查询速度很快。\nMyISAM中的索引方案 Memory默认采用Hash索引。MyISAM和InnoDB默认采用B+树索引。\nMyISAM引擎使用B+树作为索引结构，叶子节点的data域存放数据记录的地址值\nMyISAM索引的原理 MyISAM数据和索引分开存储。\n原理图：\n在Col2上建立一个二级索引，则此索引的结构如下图所示： MyISAM与InnoDB对比 MyISAM中索引方式都是非聚簇的，与InnoDB包含1个聚簇索引是不同的\nInnoDB中需要根据主键值对聚簇索引进行一次查找就能找到记录，MyISAM中却需要进行一次回表操作，所以MyISAM中的索引都是二级索引。 InnoDB数据本身也是索引，MyISAM中索引文件和数据文件分开存储，索引仅存储数据地址值。 InnoDB的非聚簇索引data域存储相应的主键值，而MyISAM索引记录的是地址。 MyISAM因为data域存储的是地址值，所以在进行回表操作的时候非常快。InnoDB则需要先获取主键，然后再去聚簇索引里找记录，没有MyISAM迅速。 InnoDB要求必须有主键，而MyISAM可以没有，如果没有，MySQL洗头膏会自动选择一个字段做主键，如果不存在符合条件的字段，则生成隐含字段做主键。 索引的代价 空间上的代价\n只要创建索引，就会创建数据页，一个页默认16KB，会占用很大一片存储空间\n时间上的代价\n每次对表中数据进行增删改，都要去修改B+树索引，增加了维护B+树的时间成本。\n一个表上索引越多，占用空间就越大，增删改性能就越差，索引我们要学会在哪些地方合理的使用索引。\nMySQL数据结构选择的合理性 归根结底就是减少对磁盘I/O的操作次数，提升效率。\n全表遍历 顺序遍历查找\nHash结构 Hash本身是一个函数，又称散列函数，可以帮助我们大幅提升检索数据效率\n相同的输入永远可以得到相同的输出。\nHash结构效率高，那为什么索引要设计成树\n原因1：Hash索引只能满足等值查询，对范围查询时间复杂度还是O(n)\n原因2：Hash索引数据的存储是没有顺序的，在Order by情况下，使用Hash索引还需要进行排序\n原因3：对于联合查询，hash值相加有可能出现相同的情况\n原因4：对于等值查询，如果出现类似性别的情况，重复太多，那么hash碰撞就很频繁。\nHash索引的适用性：\n采用自适应 Hash 索引目的是方便根据 SQL 的查询条件加速定位到叶子节点，特别是当 B+ 树比较深的时候，通过自适应 Hash 索引可以明显提高数据的检索效率。\n二叉搜索树 极端情况\n时间复杂度退化到了O(n)。\nAVL树 又称平衡二叉树。左右节点高度差不超过1\n针对同样的数据，如果我们把二叉树改成M 叉树（M\u0026gt;2）呢？当 M=3 时，同样的 31 个节点可以由下面的三叉树来进行存储：\n为了提高查询效率，就需要减少磁盘IO数。为了减少磁盘IO的次数，就需要尽量降低树的高度，需要把原来“瘦高”的树结构变的“矮胖”，树的每层的分叉越多越好。\nB-Tree 结构图：\nB 树相比于平衡二叉树来说磁盘 I/O 操作要少，在数据查询中比平衡二叉树效率要高。所以只要树的高度足够低，IO次数足够少，就可以提高查询性能。\n举例1：\nB+Tree 基于B树做出了改进，更适合文件索引系统。\nB+树与B树的差异：\n有k个孩子节点就有k个关键字，孩子数=关键字数，而B数，孩子数=关键字数+1 非叶子节点的关键字也会存在在叶子节点，并且是在子节点中所有关键字的最大或最小 非叶子节点仅用于索引，不保存数据记录，跟记录有关的信息都放在叶子节点。B树中，非叶子节点既保存数据又保存索引。 所有关键字都在叶子节点出现，叶子节点构成双向有序链表，叶子节点本身按照关键字大小从小到大顺序链接。 思考\n为了减少IO，索引会一次性加载吗？\n数据库索引是储存在磁盘上的，如果数据量很大，必然导致索引很大。 当我们利用索引查询时，不可能全部加载，只能逐一加载每个磁盘页，因为磁盘页对应着索引树的节点。 B+树能力如何，为什么说一般查找行记录，只要1-3次磁盘IO\nB+树中储存页标准16kb，我们主键INT为4字节，BIGINT为8字节，指针类型一般也为4-8字节。那么一个储存也就可以存储16kb/(8b+8b)=1k个键值，估算下来如果B+树为3层结构，那么就是1k * 1k * 1k = 10亿条记录，非常庞大。 实际情况每个节点不一定填满，因此B+树高度一般为2-4层，所以如果有4层那么第四层到根节点最多也就3次IO。 为什么说B+树比B-树更适合实际应用中操作系统的文件索引和数据库索引？\nB+树磁盘读写代价更低 B+树的查询效率更加稳定 R树 了解\n2. InnoDB数据存储结构 数据库的储存结构：页 索引储存在页结构里，索引是在储存引擎中实现的。不同引擎存放格式不同。\nInnoDB是MySQL默认存储引擎。\n磁盘与内存交互基本单位：页 InnoDB将数据划分为若干个页，InnoDB中页的大小默认为16KB\n页是磁盘与内存之间交互的基本单位。一次最少从磁盘中读取16KB内容到内存中。一次最少把内存中的16KB内容刷新到磁盘中。\n数据库管理存储空间的基本单位是页（Page），数据库I/O操作的最小单位是页。\n页结构概述 页不在物理结构上相连，只需通过双向链表关联即可。\n数据页中每条记录按照主键值从小到大顺序组成单向链表。\n每个数据页都会储存的记录生成页目录。\n通过主键查找记录，可以在页目录中使用二分法快速定位。\n页的大小 在MySQL的InnoDB引擎中，默认页大小为16KB。\n页的上层结构 页的上层结构有区、段、表空间。\n区：比页大一级，一个区分配64个连续页，区大小为64*16KB=1MB 段：由一个或多个连续区组成，区是连续分配的空间。段时数据库中的分配单位，不同类型的数据库对象以不同的段形式存在。 表空间：一个表空间可以有一个或多个段。表空间从管理上可以划分为系统表空间、用户表空间、撤销表空间、临时表空间 等。 页的内部结构 常见：数据页（保存B+树节点） 、系统页、Undo页和事务数据页\n数据页被分为7个部分：\n7部分作用：\n第一部分：File Header（文件头）和File Trailer（文件尾） 文件头部（38字节） 作用： 描述各种页的通用信息（如页编号、下一页信息）\n名称 占用空间大小 描述 FIL_PAGE_SPACE_OR_CHKSUM 4字节 页的校验和（checksum值） FIL_PAGE_OFFSET 4字节 页号 FIL_PAGE_PREV 4字节 上一个页的页号 FIL_PAGE_NEXT 4字节 下一个页的页号 FIL_PAGE_LSN 8字节 页面被最后修改时对应的日志序列位置 FIL_PAGE_TYPE 2字节 该页的类型 FIL_PAGE_FILE_FLUSH_LSN 8字节 仅在系统表空间的一个页中定义，代表文件至少被刷新到了对应的LSN值 FIL_PAGE_ARCH_LOG_NO_OR_SPACE_ID 4字节 页属于哪个表空间 FIL_PAGE_OFFSET：记录页的编号。\nFIL_PAGE_TYPE：当前页类型。如下：\n类型名称 十六进制 描述 FIL_PAGE_TYPE_ALLOCATED 0x0000 最新分配，还没有使用 FIL_PAGE_UNDO_LOG 0x0002 Undo日志页 FIL_PAGE_INODE 0x0003 段信息节点 FIL_PAGE_IBUF_FREE_LIST 0x0004 Insert Buffer空闲列表 FIL_PAGE_IBUF_BITMAP 0x0005 Insert Buffer位图 FIL_PAGE_TYPE_SYS 0x0006 系统页 FIL_PAGE_TYPE_TRX_SYS 0x0007 事务系统数据 FIL_PAGE_TYPE_FSP_HDR 0x0008 表空间头部信息 FIL_PAGE_TYPE_XDES 0x0009 扩展描述页 FIL_PAGE_TYPE_BLOB 0x000A 溢出页 FIL_PAGE_INDEX 0x45BF 索引页，也就是我们所说的数据页 第二部分：空闲空间、用户记录和最小最大记录 3. 索引的创建与设计原则 3.1 索引的声明与使用 索引的分类 功能逻辑：普通索引、唯一索引、主键索引、全文索引 物理实现方式：聚簇索引、非聚簇索引 作用字段：单列索引、联合索引 不同存储引擎支持的索引类型不一样\n创建索引 1）在建表的时候创建索引 CREATE TABLE dept( dept_id INT PRIMARY KEY AUTO_INCREMENT, #主键索引 dept_name VARCHAR(20) ); CREATE TABLE emp( emp_id INT PRIMARY KEY AUTO_INCREMENT, emp_name VARCHAR(20) UNIQUE, #唯一索引 dept_id INT, CONSTRAINT emp_dept_id_fk FOREIGN KEY(dept_id) REFERENCES dept(dept_id) #外键索引 ); 显示创表时创建索引，基础语法\nCREATE TABLE table_name [col_name data_type] [UNIQUE | FULLTEXT | SPATIAL] [INDEX | KEY] [index_name] (col_name [length]) [ASC | DESC] UNIQUE、FULLTEXT和SPATIAL为可选参数，分别表示唯一索引、全文索引和空间索引； INDEX 与 KEY 为同义词，两者的作用相同，用来指定创建索引； index_name 指定索引的名称，为可选参数，如果不指定，那么MySQL默认col_name为索引名； col_name 为需要创建索引的字段列，该列必须从数据表中定义的多个列中选择； length为可选参数，表示索引的长度，只有字符串类型的字段才能指定索引长度； ASC 或 DESC 指定升序或者降序的索引值存储。 2）创建普通索引 CREATE TABLE book( book_id INT , book_name VARCHAR(100), `authors` VARCHAR(100), info VARCHAR(100) , `comment` VARCHAR(100), year_publication YEAR, INDEX(year_publication) #普通索引 ); 3）创建唯一索引 CREATE TABLE test1( id INT NOT NULL, NAME VARCHAR(30) NOT NULL, UNIQUE INDEX uk_idx_id(id) #使用id作为唯一索引 ); 使用SHOW CREATE TABLE查看表结构\nSHOW INDEX FROM test1; 4）主键索引 设定为主键后数据库自动建立索引，innodb为聚簇索引\nCREATE TABLE student ( id INT(10) UNSIGNED AUTO_INCREMENT , student_no VARCHAR(200), student_name VARCHAR(200), PRIMARY KEY(id) ); SHOW INDEX FROM student; 删除主键索引\nALTER TABLE student DROP PRIMARY KEY; 有概率会出现特殊情况\n因主键有自动递增，需要先删除自动递增约束\nALTER TABLE student MODIFY id INT(10) UNSIGNED; ALTER TABLE student DROP PRIMARY KEY; 4. 性能分析工具的使用 5. 索引优化与查询优化 6. 数据库设计规范 7. 数据库其它调优策略 ","permalink":"https://zhoujze.github.io/en/posts/mysql/02-mysql%E7%B4%A2%E5%BC%95%E8%B0%83%E4%BC%98%E7%AF%87/","summary":"1. 索引的数据结构 为什么使用索引 索引时一种快速找到数据记录的数据结构，类比书的目录。 顺序查找磁盘I/O次数较多。 搜索树，减少了磁盘I/O次数。 索引优缺点 索引是什么 帮助MySQL高效获取数据的数据结构 索引在储存引擎中实现，不同的储存索引不一定完全一样。 优点 提高数据检索效率，降低数据I","title":""},{"content":"1. 事物基础知识 2. MySQL事物日志 3. 锁 4. 多版本并发控制MVCC ","permalink":"https://zhoujze.github.io/en/posts/mysql/03-mysql%E4%BA%8B%E5%8A%A1%E7%AF%87/","summary":"1. 事物基础知识 2. MySQL事物日志 3. 锁 4. 多版本并发控制MVCC","title":""},{"content":"1. 其他数据库日志 2. 主从复制 3. 数据备份与恢复 ","permalink":"https://zhoujze.github.io/en/posts/mysql/04-mysql%E6%97%A5%E5%BF%97%E4%B8%8E%E5%A4%87%E4%BB%BD%E7%AF%87/","summary":"1. 其他数据库日志 2. 主从复制 3. 数据备份与恢复","title":""}]