Jekyll2023-04-11T07:19:46+00:00https://dezhen.vip/feed.xml今晚打老虎You never know what you don't know.
zhaodezhendezhen98@gmail.com领域驱动设计(ddd)教程2023-04-11T00:00:00+00:002023-04-11T00:00:00+00:00https://dezhen.vip/2023/04/11/%E9%A2%86%E5%9F%9F%E9%A9%B1%E5%8A%A8%E8%AE%BE%E8%AE%A1(DDD)%E6%95%99%E7%A8%8B<p>领域驱动设计(DDD,Domain-Driven Design)是一种软件开发方法,旨在将领域知识(如业务规则、策略和逻辑)与软件实现相互对应。在大型项目中,使用 DDD 来帮助业务和研发更好的配合。</p>
<h2 id="ddd的核心概念">DDD的核心概念</h2>
<p>领域驱动设计的核心概念是领域模型(Domain Model)。领域模型是一个概念模型,用于表示特定业务领域的关键元素、规则和逻辑。领域模型的目标是提供一种通用的业务语言(Ubiquitous Language),让开发人员和领域专家可以有效地沟通。
要理解DDD,您需要熟悉以下几个关键概念:
<!--more--></p>
<ul>
<li>实体(Entity):具有唯一标识符的领域对象,如用户、订单或产品。</li>
<li>值对象(Value Object):表示领域概念的不可变对象,不具有唯一标识符。例如,日期、金额或地址。</li>
<li>聚合(Aggregate):一组实体和值对象,它们共同组成一个一致性边界。聚合内的对象之间可以自由关联,但聚合之间的交互应通过聚合根(Aggregate Root)进行。</li>
<li>领域服务(Domain Service):表示领域中的一个操作或行为,但无法自然地归属于实体或值对象。领域服务通常是无状态的,并封装了领域逻辑。</li>
<li>领域事件(Domain Event):表示领域中的一个重要事件,通常由实体或聚合根触发。领域事件可以用于解耦系统组件,并支持异步处理。</li>
<li>存储库(Repository):负责管理聚合的持久化和检索。存储库将领域模型与底层数据存储(如数据库)隔离。
DDD的战略设计
DDD的战略设计是关注系统的高层结构和划分边界。以下是战略设计的两个重要概念:</li>
<li>限界上下文(Bounded Context):限界上下文是一个明确界定的系统边界,内部具有一致性的领域模型。不同限界上下文之间可以通过上下文映射(Context Mapping)进行协作。</li>
<li>上下文映射(Context Mapping):描述不同限界上下文之间的关系和交互方式。常见的上下文映射模式包括:合作关系(Partnership)、共享内核(Shared Kernel)、客户/供应商(Customer/Supplier)和防腐层(Anticorruption Layer)</li>
</ul>
<h2 id="实践领域驱动设计">实践领域驱动设计</h2>
<p>以下是实践领域驱动设计的一些建议:</p>
<h3 id="与领域专家合作">与领域专家合作</h3>
<p>与领域专家(如业务分析师、产品经理或业务人员)紧密合作,共同定义领域模型和通用语言。确保开发人员和领域专家在整个项目中始终保持良好沟通。</p>
<h3 id="设计领域模型">设计领域模型</h3>
<p>根据业务需求,创建实体、值对象、聚合、领域服务和领域事件。关注领域逻辑的封装和模型的内聚性,确保模型能够准确地反映业务领域的概念。</p>
<h3 id="划分限界上下文">划分限界上下文</h3>
<p>根据业务功能和组织结构,将系统划分为多个限界上下文。确保每个限界上下文内部具有一致性的领域模型。在不同限界上下文之间建立上下文映射,明确它们之间的关系和协作方式。</p>
<h3 id="实现存储库">实现存储库</h3>
<p>为聚合根实现存储库,以支持持久化和检索操作。尽量将存储库与底层数据存储(如数据库)解耦,以便于替换存储实现或支持多种数据源。</p>
<h3 id="应用层与领域层解耦">应用层与领域层解耦</h3>
<p>将应用层(如Web控制器、API端点)与领域层(领域模型、领域服务)解耦。避免在应用层直接实现领域逻辑。可以使用命令模式、领域事件或事件总线等模式来实现解耦。</p>
<h3 id="重构与持续改进">重构与持续改进</h3>
<p>在项目进行过程中,随着业务需求的变化和领域知识的积累,不断地重构和优化领域模型。保持领域模型与业务领域的同步,提高模型的可维护性和可扩展性。</p>
<h2 id="结束语">结束语</h2>
<p>领域驱动设计是一种强大的软件开发方法,可以帮助您创建更具内聚性和可维护性的系统。通过深入理解DDD的概念和原则,以及与领域专家的紧密合作,您可以更好地应对复杂业务领</p>zhaodezhendezhen98@gmail.com领域驱动设计(DDD,Domain-Driven Design)是一种软件开发方法,旨在将领域知识(如业务规则、策略和逻辑)与软件实现相互对应。在大型项目中,使用 DDD 来帮助业务和研发更好的配合。 DDD的核心概念 领域驱动设计的核心概念是领域模型(Domain Model)。领域模型是一个概念模型,用于表示特定业务领域的关键元素、规则和逻辑。领域模型的目标是提供一种通用的业务语言(Ubiquitous Language),让开发人员和领域专家可以有效地沟通。 要理解DDD,您需要熟悉以下几个关键概念:浅谈redis持久化2019-05-04T00:00:00+00:002019-05-04T00:00:00+00:00https://dezhen.vip/2019/05/04/%E6%B5%85%E8%B0%88redis%E6%8C%81%E4%B9%85%E5%8C%96<h3 id="前言">前言</h3>
<p>前面我们讲了 Redis 的数据结构(<a href="https://dezhen.vip/2019/04/21/%E6%B5%85%E8%B0%88Redis%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84.html">Redis那些事之数据结构</a>),今天我们来看看 Redis 的持久化,Redis 的持久化分两种 RDB 和 AOF。这两种各有优缺点,我们先看下官方是怎么描述这两种结构的:</p>
<blockquote>
<p>RDB持久性以指定的时间间隔执行数据集的时间点快照。<br />
AOF持久性记录服务器接收的每个写入操作,将在服务器启动时再次播放,重建原始数据集。 使用与Redis协议本身相同的格式以仅追加方式记录命令。 当Redis太大时,Redis能够重写日志背景。</p>
</blockquote>
<p>我们可以选择关闭持久化,把 Redis 作为一个内存缓存使用,也可以开启持久化做数据库使用,RDB 和 AOF 可以同时开启,同时开启的情况下 Redis 优先读取 AOF 里面的数据。最重要的是理解 RDB 和 AOF 的区别和各自的优缺点,以便我们在不同的业务员场景下择优选择。
<!--more--></p>
<h3 id="rdb">RDB</h3>
<p>RDB 持久化可以手动执行,也可以根据配置选项定期执行,该功能可以将某个时间点的数据库状态保存到一个 RDB 文件中。因为 RDB 是保存在硬盘中,所以即使 Redis 服务器进程挂掉,只要 RDB 文件存在,Redis 就可以用它来还原数据状态。<br />
Redis 提供两个手动执行命令 SAVE、BGSVE,SAVE命令执行时,客户端发送的所以命令请求都会被阻塞。BGSAVE 命令是由主进程 fork 出一个子进程进行持久化。所以在持久化的过程中 Redis 还可以继续执行客户端的命令,持久化由子进程执行。因为 BGSAVE 命令是非阻塞的,所以自动执行全部采用的是 BGSAVE 命令。<br />
RDB 文件保存了用户的写入和删除命令,并根据不同的数据类型进行相应的编码,一个标准的 RDB 文件一般有一下部分组成:</p>
<ol>
<li>REDIS</li>
<li>db_version RDB 文件版本号</li>
<li>database 数据库</li>
<li>EOF 结束标识符</li>
<li>check_sum 校验和</li>
</ol>
<p>其中 database 的格式为:</p>
<ol>
<li>SELECTDB 类似 flag 标识,当程序读到这儿的时候标识接下来读入的是一个数据库号码</li>
<li>db_number 数据库号</li>
<li>key_value_pairs 保存了数据库中所以的键值对数据、过期条件等</li>
</ol>
<p>其中 key_value_pairs 部分的 key 是采用的 SDS 编码, value 是根据不同的数据类型进行存储(参考:<a href="https://dezhen.vip/2019/04/21/%E6%B5%85%E8%B0%88Redis%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84.html">Redis那些事之数据结构</a>)</p>
<h3 id="aof">AOF</h3>
<p>除了 RDB 持久化功能之外 Redis 还提供了 AOF(append only file)持久化功能,AOF 是通过保存 Redis服务器所执行的写命令来记录数据库状态的。<br />
AOF 的实现可以功能大体分为 命令追加(append)、文件写入、文件同步(sync)。<br />
当 AOF 功能打开的时候,服务器在执行完一个写命令后,会以写一个是将被执行命令追加到服务器状态的 aof_buf 缓冲区的末尾。Redis 服务器在一个事件循环(loop)周期里它会调用一次 flushAppendOnlyFile 函数。考虑是否将 aof_buf 缓冲区中的内容写入和保存到 AOF 文件里面。<br />
因为 AOF 是通过客户端发送的写命令来保证数据库的状态的,所以随着时间的流失, AOF 文件中的内容可能会出现越来越多对同样一个键的修改。文件的体积也越来越大,如不加以控制,可能会对 Redis 服务器造成影响,使用 AOF 恢复所需的时间也会加大。 Redis 为此提供了 AOF 重写功能, AOF 的重写并不是对现有文件进行分析然后进行重写,而是遍历读取数据库里的数据,然后把对应的命令转成写入命令,写入到一个新的文件。这个执行过程同样是阻塞进行的,所以可以继续采用子进程来执行,等子进程处理完后对父进程发送一个信号通知重新执行完毕。然后父进程对原来的 AOF 文件原子性的覆盖。(在子进程执行重写的过程,父进程同样会接收命令,这个期间主进程所接收到的命令都会保存到缓冲区,在执行覆盖前,Redis服务会把缓冲区的命令追加到子进程发来的 AOF 文件里面)。</p>
<h3 id="rdb-和-aof-的优缺点">RDB 和 AOF 的优缺点</h3>
<p><strong>RDB 的优点</strong></p>
<ul>
<li>RDB 文件非常适合备份,比如可以根据时间或者是修改的次数,进行归档,然后在发生灾难的时候轻松的根据恢复不同的版本数据。</li>
<li>RDB 非常适合灾难恢复,可以将单个压缩文件传输到远端数据中心,也可以传输到 Amazon S3(可能是加密的)。</li>
<li>RDB 最大限度地提高了 Redis 的性能,因为 Redis 父进程为了持久化需要做的唯一工作就是分配一个将完成所有其余工作的孩子。 父实例永远不会执行磁盘 I/O 或类似操作。</li>
<li>与 AOF 相比,RDB 允许使用大数据集更快地重启。</li>
</ul>
<p><strong>AOF 的优点</strong></p>
<ul>
<li>使用 AOF Redis 更持久:你可以使用不同的 fsync 策略:根本没有 fsync,每秒 fsync,每次查询都有 fsync。使用 fsync 的默认策略,每秒写入性能仍然很好(使用后台线程执行 fsync,并且当没有 fsync 正在进行时,主线程将努力执行写入。)但是你只能丢失一秒的写入。</li>
<li>AOF 日志是仅附加日志,因此如果停电,则没有搜索,也没有损坏问题。即使由于某种原因(磁盘已满或其他原因)日志以半写命令结束,redis-check-aof 工具也能够轻松修复它。</li>
<li>当 Redis 太大时,Redis 能够在后台自动重写 AOF。重写是完全安全的,因为当 Redis 继续附加到旧文件时,使用创建当前数据集所需的最小操作集生成一个全新的文件,并且一旦第二个文件准备就绪,Redis 会切换两个并开始附加到新的那一个。</li>
<li>AOF 以易于理解和解析的格式一个接一个地包含所有操作的日志。你甚至可以轻松导出 AOF 文件。例如,即使你使用 FLUSHALL 命令刷新了所有错误,如果在此期间未执行重写日志,你仍然可以保存数据集,只需停止服务器,删除最新命令,然后重新启动 Redis。</li>
</ul>
<p><strong>RDB 的缺点</strong></p>
<ul>
<li>如果你需要在 Redis 挂掉后将数据的丢失性降到最小,那么 RDB 可能不合适。你可以根据配置文件设置不同的时间保存点,例如;对数据集进行至少 5 分钟和100次写入之后保存 RDB 文件,那么 5 分钟内或者修改次数不满足 100 次的数据可能就会丢失。</li>
<li>RDB 为了持久化会经常 fork 才能将子进程的数据持久储存在磁盘上,如果数据集很大, fork 可能会非常耗时,并且如果数据集非常大且 CPU 性能不佳,可能会导致 Redis 停止服务客户端几毫秒甚至一秒钟。 AOF 也需要 fork ,但你可以调整你想要重写日志的频率而不需要对持久性进行任何权衡。</li>
</ul>
<p><strong>AOF 的缺点</strong></p>
<ul>
<li>因为 AOF 是保存的写入命令,所以同样数据下 AOF 一般比 RDB 文件更大。</li>
<li>根据确切的 fsync 策略,AOF 可能比 RDB 慢。一般来说,fsync 设置为每秒性能仍然非常高,并且在 fsync 禁用的情况下,即使在高负载下也应该与 RDB 一样快。即使在写入负载很大的情况下,RDB 仍能够提供有关最大延迟的更多保证。</li>
</ul>zhaodezhendezhen98@gmail.com前言 前面我们讲了 Redis 的数据结构(Redis那些事之数据结构),今天我们来看看 Redis 的持久化,Redis 的持久化分两种 RDB 和 AOF。这两种各有优缺点,我们先看下官方是怎么描述这两种结构的: RDB持久性以指定的时间间隔执行数据集的时间点快照。 AOF持久性记录服务器接收的每个写入操作,将在服务器启动时再次播放,重建原始数据集。 使用与Redis协议本身相同的格式以仅追加方式记录命令。 当Redis太大时,Redis能够重写日志背景。 我们可以选择关闭持久化,把 Redis 作为一个内存缓存使用,也可以开启持久化做数据库使用,RDB 和 AOF 可以同时开启,同时开启的情况下 Redis 优先读取 AOF 里面的数据。最重要的是理解 RDB 和 AOF 的区别和各自的优缺点,以便我们在不同的业务员场景下择优选择。浅谈redis数据结构2019-04-21T00:00:00+00:002019-04-21T00:00:00+00:00https://dezhen.vip/2019/04/21/%E6%B5%85%E8%B0%88Redis%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84<h3 id="前言">前言</h3>
<p>Redis 数据库里面的每个键值对都是由对象组成的,其中数据库的键总是一个字符串对象(string object),数据库的值则可以使字符串对象、列表对象(list object)、哈希对象(hash object)、集合对象(set object)和有序集合对象(sorted object)这五种数据结构。下面我们一起来看下这些数据对象在 Redis 的内部是怎么实现的,以及 Redis 是怎么选择合适的数据结构进行存储等。</p>
<h3 id="简单动态字符串">简单动态字符串</h3>
<p>Redis 没有直接使用 C 语言传统的字符串标识,而是自己构建了一种名为简单动态字符串 SDS(simple dynamic string)的抽象类型,并将 SDS 作为 Redis 的默认字符串。<br />
SDS 结构(如果没有特殊说明,代码采用的一律为 Redis 5.0 版本)</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>struct __attribute__ ((__packed__)) sdshdr64 {
uint64_t len; /* used */
uint64_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
</code></pre></div></div>
<!--more-->
<ul>
<li>len 表示 SDS 的长度,使我们在获取字符串长度的时候可以在 O(1)情况下拿到,而不是像 C 那样需要遍历一遍字符串。</li>
<li>alloc 可以用来计算 free 就是字符串已经分配的未使用的空间,有了这个值就可以引入预分配空间的算法了,而不需要使用者去考虑内存分配的问题。预分配在这个字符串对象内存小于 1M 的时候分配和 len 同样大小的内存,大于 1M 的时候分配 1M内存。</li>
<li>buf 表示字符串数组</li>
</ul>
<p>SDS 有五种长度,分别为sdshdr5、sdshdr8、sdshdr16、sdshdr32、sdshdr64。其中从不使用sdshdr5,只是直接访问flags字节用的。 <br />
Redis 的字符串结构并没有抛弃 C字符串,这意味着它可以向下兼容 C 风格的字符串,可以重用 C 字符串函数。</p>
<h3 id="链表">链表</h3>
<p>链表提供了高效的节点排重能力,以及顺序性的节点访问方式,而且可以通过增加节点来灵活地调整链表的长度。它是一种常用的数据结构,被内置在很多高级语言中。因为C语言并没有内置这种数据结构,所以 Redis 构建了自己的链表实现。 <br />
链表的节点</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>typedef struct listNode {
struct listNode *prev;
struct listNode *next;
void *value;
} listNode;
</code></pre></div></div>
<ul>
<li>prev 前置节点</li>
<li>next 后置节点</li>
<li>value 节点的值
多个 listNode 结构组成一个链表;</li>
</ul>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>typedef struct list {
listNode *head;
listNode *tail;
void *(*dup)(void *ptr);
void (*free)(void *ptr);
int (*match)(void *ptr, void *key);
unsigned long len;
} list;
</code></pre></div></div>
<ul>
<li>head 节点头指针</li>
<li>tail 节点尾指针</li>
<li>dup 用于复制链表节点所保存的值</li>
<li>free 用于释放链表节点所保存的值</li>
<li>match 用于对比链表节点所保存的值和另一个输入的值是否相等</li>
<li>len 链表计数器</li>
</ul>
<p>Redis 链表的特性;</p>
<ul>
<li>双端;有 prev 和 next 获取某个节点的前置和后置都是 O(1)</li>
<li>无环;头结点的 prev 和尾节点的 next 都指向 NULL</li>
<li>链表计数器;获取链表的长度为 O(1)</li>
<li>多态;链表节点使用 void* 指针来保存节点的值,所以链表可以用于保存各种不同类型的值。
<h3 id="字典">字典</h3>
<p>字典中一个键(key)可以和一个值关联(value),这种关联的键和值我们称之为键值对。所以字典的每个键都是独一无二的,我们可以根据键在 O(1) 的时间复杂度下找到与之相关联的值。字典也是很多高级语言都内置的一种数据结构,但是 C语言并没有内置这种数据结构,因此 Redis 自己构建了字典的实现。</p>
</li>
</ul>
<p>字典的内部是采用的哈希表结构:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/* This is our hash table structure. Every dictionary has two of this as we
* implement incremental rehashing, for the old to the new table. */
typedef struct dictht {
dictEntry **table;
unsigned long size;
unsigned long sizemask;
unsigned long used;
} dictht;
</code></pre></div></div>
<ul>
<li>dictEntry 哈希表数组</li>
<li>size 哈希表大小</li>
<li>sizemask 哈希表大小掩码</li>
<li>used 哈希表已有节点的数量</li>
</ul>
<p>其中 table 是一个数组,数组中的每个元素都是指向 dictEntry 的指针。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>typedef struct dictEntry {
void *key;
union {
void *val;
uint64_t u64;
int64_t s64;
double d;
} v;
struct dictEntry *next;
} dictEntry
</code></pre></div></div>
<ul>
<li>val 键</li>
<li>union 值</li>
<li>dictEntry 指向下个哈希表节点</li>
</ul>
<p>哈希表在添加一个新的键值对的时候,程序会根据键值对的键计算出哈希索引值,然后根据索引值将包含键值对的哈希表节点放到哈希表数组的指定索引上。<br />
当有两个或者以上的键被分配到哈希表数组的同一个索引上面时,会产生冲突。 Redis 的哈希表使用链地址法(separate chaining)来解决建冲突。<br />
随着不断的执行,哈希表保存的键值对随之也会做多或者减少,为了让哈希表的负载因子维持在一个合理范围内,所以程序需要对哈希表的大小进行相应的扩展或者收缩。执行原理类似动态数组。当空间不够或者剩余的时候自动申请一块内存空间进行数据转移,在 Redis 中叫做 rehash。</p>
<h3 id="跳跃表">跳跃表</h3>
<p>跳跃表是一种有序的链性数据结构,通过维护层级 (level) 来达到快速访问节点的目的。平均查找复杂度为 O(logN),最坏 O(N)。因为是链性结构,还支持顺序性操作。<br />
关于 Redis 为什么采用跳跃表而不采用红黑树之前我写过一篇文章,所以就不在这细诉了,我觉得其主要原因不外乎两点,一是红黑树不易于实现,而且在频繁的添加修改之后,为了维持树的平衡还要进行左右旋转。二是红黑树查找虽然是 O(logN),但是在进行区间查找中往往就做到不 O(logN) 了,甚至需要遍历整个树。跳表就不需要了,它只需要找到第一个节点然后根据链性结构的特点向下走就可以了。Redis 有序集合一般就是用的这种实现。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/* ZSETs use a specialized version of Skiplists */
typedef struct zskiplistNode {
sds ele;
double score;
struct zskiplistNode *backward;
struct zskiplistLevel {
struct zskiplistNode *forward;
unsigned long span;
} level[];
} zskiplistNode;
typedef struct zskiplist {
struct zskiplistNode *header, *tail;
unsigned long length;
int level;
} zskiplist;
</code></pre></div></div>
<p>zskiplistNode 为跳跃表节点:</p>
<ul>
<li>sds 元素</li>
<li>score 分值</li>
<li>backward 后退指针</li>
<li>level 层级</li>
</ul>
<p>zskiplist 为跳跃表:</p>
<ul>
<li>header 头结点</li>
<li>tail 尾节点</li>
<li>length 节点数量</li>
<li>level 最大节点的层数</li>
</ul>
<h3 id="整数集合">整数集合</h3>
<p>当一个集合的元素只包含整数值元素,并且集合的元素不多时,Redis 就会使用整数集合作为集合的底层实现。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>typedef struct intset {
uint32_t encoding;
uint32_t length;
int8_t contents[];
} intset;
</code></pre></div></div>
<ul>
<li>encoding 编码方式</li>
<li>length 集合元素个数</li>
<li>contents 保存集合数据的数组</li>
</ul>
<p>当集合数据类型大于 int8_t 所表示的最大空间时,Redis 会自动为该集合升级。一旦升级,不支持降级。</p>
<h3 id="压缩列表">压缩列表</h3>
<p>压缩列表是一种为节约内存而开发的顺序性数据结构。常常被用作列表、哈希的底层实现。是由一系列特殊编码的连续内存块组成的。</p>
<h3 id="总结">总结</h3>
<p>Redis 内部是由一系列对象组成的,字符串对象、列表对象、哈希表对象、集合对象有序集合对象。<br />
字符串对象是唯一一个可以应用在上面所以对象中的,所以我们看到向一些 keys exprice 这种命令可以在针对所有 key 使用,因为所有 key 都是采用的字符串对象。
列表对象默认使用压缩列表为底层实现,当对象保存的元素数量大于 512 个或者是长度大于64字节的时候会转换为双端链表。<br />
哈希对象也是优先使用压缩列表键值对在压缩列表中连续储存着,当对象保存的元素数量大于 512 个或者是长度大于64字节的时候会转换为哈希表。<br />
集合对象可以采用整数集合或者哈希表,当对象保存的元素数量大于 512 个或者是有元素非整数的时候转换为哈希表。<br />
有序集合默认采用压缩列表,当集合元素数量大于 128 个或者是元素成员长度大于 64 字节的时候转换为跳跃表。</p>
<h3 id="参考资料">参考资料</h3>
<p><a href="https://book.douban.com/subject/25900156/">Redis 设计与实现</a><br />
<a href="https://book.douban.com/subject/10597898/"> Redis in Action </a></p>zhaodezhendezhen98@gmail.com前言 Redis 数据库里面的每个键值对都是由对象组成的,其中数据库的键总是一个字符串对象(string object),数据库的值则可以使字符串对象、列表对象(list object)、哈希对象(hash object)、集合对象(set object)和有序集合对象(sorted object)这五种数据结构。下面我们一起来看下这些数据对象在 Redis 的内部是怎么实现的,以及 Redis 是怎么选择合适的数据结构进行存储等。 简单动态字符串 Redis 没有直接使用 C 语言传统的字符串标识,而是自己构建了一种名为简单动态字符串 SDS(simple dynamic string)的抽象类型,并将 SDS 作为 Redis 的默认字符串。 SDS 结构(如果没有特殊说明,代码采用的一律为 Redis 5.0 版本) struct __attribute__ ((__packed__)) sdshdr64 { uint64_t len; /* used */ uint64_t alloc; /* excluding the header and null terminator */ unsigned char flags; /* 3 lsb of type, 5 unused bits */ char buf[]; };从自身成长聊一下我理解的“终身学习”2019-04-07T00:00:00+00:002019-04-07T00:00:00+00:00https://dezhen.vip/2019/04/07/%E4%BB%8E%E8%87%AA%E8%BA%AB%E6%88%90%E9%95%BF%E8%81%8A%E4%B8%80%E4%B8%8B%E6%88%91%E7%90%86%E8%A7%A3%E7%9A%84%E2%80%9C%E7%BB%88%E8%BA%AB%E5%AD%A6%E4%B9%A0%E2%80%9D<h3 id="前言">前言</h3>
<p>这周我们来聊点轻松的,聊一下技术人员的“终身学习”。个人浅见,希望能帮助到你<br />
其实我当初在选择方向的时候还是挺纠结的,稀里糊涂的就选择了程序员这条路。也浑浑噩噩的度过了几年的日子。后来工作了,发现之前有规划有计划的小伙伴们都成长的很快。而自己就像一条没有梦想也没有追求的咸鱼,根本不知道自己想要什么。其实我一开始工作的时候,对这个专业的兴趣并不是很浓,之所以选择这个专业,无非是因为好找点工作,多挣点钱,毕竟都说:“CS专业是寒门之子逆袭的一条捷径”。<strong>既然家庭条件决定了要打工,就早点为打工做准备</strong>。可惜当初并没有明白这个道理,现在看来<strong>从大学开始,个人选择开始变得格外重要,越早认识到这一点,大学阶段的收获就越大</strong>。一些优秀的家庭不止是关注孩子的成绩,更关注的是孩子的认知,一个人的认知往往决定了它的高度,大多数人都不懂这个道理。</p>
<h3 id="成长经历">成长经历</h3>
<p>随着自己慢慢成熟,自己的认知也提升了不少。我渐渐明白做一个人,要做负责的人(包括不限于对自己负责、家人负责、工作负责、爱情负责等)。既然选择这个专业了,并且没有换方向的想法,那就把它做好,对自己负责。<br />
我开始慢慢接触一些技术的圈子,开始慢慢了解我所属的这个行业。在中国互联网的行业常说;程序员的35岁危机、框架更新太快学不会等。弄得程序员这个行业充满焦虑,凡是撩拨这种情绪的文章都能获得大量的转发。我一度也陷入这个焦虑和恐慌中,现在看来其实每个时代都一样,社会并不欠我们一个成功。无论你怎么忙、没时间、有困难、不能痛下决心、拖延症等,社会不管,它不能保证每一个人的成功。<br />
<!--more-->
前两年的知识付费的兴起恰恰证明了这一点,越来越多人意识到了焦虑,开始寻找缓解这种问题的解。资本家发现现在的人极度焦虑,却又不肯深入性的去学习和思考,所以推出了知识付费,打着利用碎片时间学习的口号来收割韭菜。大家积极性都很高,而且一个课程只需要几分钟到十几分钟,很容易就坚持下来了,但是往往会陷入一个死循环,被困在知识的底层,因为知识这东西本来就是需要自己去学习,自己去思考提炼总结的。你想要别人把现成的给你,那是不可能的。相信大家都知道一万小时的定律,一个高手的成长需要一万小时。这其中包括了很多,简单的听一些专栏不去思考的话,是不可能成为一个高手的。而且大多数作者为了课时,把密度很大的知识抽象成一小段。你不去深入的学习或者是和作者交流,你根本不会理解其中的内容。只会让你知其然而不知所以然,往往会越学越焦虑。当然我并不是质疑作者的能力,这是知识的本质,知识是不能不劳而获的。<br />
我之前也是知识付费的一员,当初是什么火学什么,今天这个框架比较火学这个,明天那个技术出来的学那个… 但是互联网上层技术本来就是跟新迭代比较快的,一直学习着表面的东西我也觉得累,慢慢的我就在想,按照这样下去,等我35岁以后能留下什么。技术更新往往新技术出来老技术就没用了,三年前的技术拿到现在来不一定有人用了。等自己年龄大了,学习新知识的能力不如年轻人了,自己又有什么优势呢?可能除了年龄和经验就一无所有了吧,我想这也是大多数做技术人的焦虑。所以我就慢慢停下来看,我学习了这么多究竟给我带来了什么,对以后又能带来什么?往往都是学完一个技术再去学下一个技术,而学完下个技术,前一个技术可能已经不流行了。这样下去不行的,我开始停止学习框架,开始研究有没有一种技术领域的“元知识”,又或者是一种很牛的技术可以在以后一直通用下去。显然通用下去的技术我是找不到,因为我不能预测未来,所以只能找一种”元知识”,或者是变化很小的知识。最后发现一些底层的知识是变化很少的,但凡是有心的都能看出来TCP发展了四十年了,除了性能方面的一些略微修改,到现在来说几乎没怎么更新过,操作系统随着冯洛伊曼提出操作系统结构就没更改过。而且了解操作系统的I/O模型可以有助于自己编写各种并发编程,了解内存分配可以有助于写出高性能的代码。这正是我一直寻找的“元知识”,学会了这些东西,可以保证我在技术浪潮中保证学习效率的速度,并且可以优于他人。</p>
<h3 id="技术人员的终身学习">技术人员的终身学习</h3>
<p>最近一段时间和几个大佬聊到”终身学习”这个概念(我主要是听大佬讲),为什么说这是个“概念”,其实我觉得这是一个伪命题,一个只存在概念中不存在现实中的题。<br />
很多人提到过缓解技术焦虑,保持终身学习。我觉得一个人能不能终身学习最重要的是他爱不爱这个东西,对这个东西是不是有兴趣。能不能爱上这个东西我并不知道,但是兴趣是可以培养的我知道,并且我就是把兴趣“培养”出来的。<br />
学习一般分为两种:</p>
<ul>
<li>
<p>主动学习<br />
一般主动学习的人都是一些自律性很强的,有着明确目的,早知道自己想要什么的人。</p>
</li>
<li>
<p>被动学习<br />
被动学习就是被逼着学的,可能是因为工作的事不得不学习,因为房贷车贷要还,或者是干脆说是不想被别人看不起想要证明自己的等等。</p>
</li>
</ul>
<p>这两种都可以保证你在一段时间内保持学习,但是要是说保持终身学习,那是不切实际的。因为你学习一段时间后就会觉得枯燥无趣,慢慢就会放弃自己的坚持。但凡是能长期坚持下去的人,除了自己的自律性不说,还有主要的一点就是兴趣。<br />
兴趣这一点其实可以慢慢培养,做技术这个就怕闷头不交流,培养兴趣重要的一点就是有正反馈,让你有成就感。成就感的来源自别人的认同或者赞赏,所以你要有交流,多交流的好处就不多说了。举个正反馈的例子;比如你现在正在学操作系统的内存管理和,然后你学完了,并且学的还可以,但是你不知道有什么用,很是枯燥,自己完全是靠着坚持才学下去的。然后你和别人交流,别人向你请教个问题调优性能的问题,你发现他们在存储数据的时候存储了一些冗余的数据,导致存储这些数据的时候存在内存的不同地方,你在不影响业务的情况下通过简单的去除一些冗余数据,把这些数据压缩在一个连续的内存空间上,并且保证一次仿盘就可以拿到,大大的提升的性能,从当初50台server降到了1台。这个例子是一个大佬告诉我的,这是事实经历的,并不是杜钻的。所以你在解决了上面的问题的时候你的会有成就感,而且给你带来的赞赏,和满足感,你会发现学习这个原来这个好玩。再说一个我的例子,我当初在学习TCP/IP的时候也觉得很枯燥,后来在排查TC的问题的时候那本书给我带来的很大的帮助,所以再去看那本书的时候就不会觉得枯燥了,反而觉得很有意思。<br />
自律也很重要,兴趣决定了你适不适合这行,自律决定了你在这行能走多远。自律不单单是管好自己这么简单,这其中包括了很多,像时间管理这块也属于自律。说下我对时间的管理供大家参考;<br />
早上:<br />
5:30 起床<br />
6:00 - 8:00 学习<br />
8:00-8:30 吃早餐<br />
8:30-9:00 总结一下当日学习的东西,然后去上班<br />
中午:<br />
1:00-1:30 看书<br />
晚上:<br />
9:00-9:30 总结一下今天完成的任务和计划,规划明天的任务<br />
10:30 休息<br />
这套作息坚持半年了,周末也是如此,带来的提升很大。一般周六会总结一些本周、周日计划一下下周,不过我会在周日休息半天(利用好周末的时间很重要)。做技术的想要早睡一般很难,经常晚上加班和项目上线,不过利用好时间也不是不可能的。一开始很难,坚持下去,利用习惯的力量就很简单了。<br />
最后希望大家看待问题的时候都能<strong>通过表面看到本质,而不只是沉浮在表面</strong>。</p>zhaodezhendezhen98@gmail.com前言 这周我们来聊点轻松的,聊一下技术人员的“终身学习”。个人浅见,希望能帮助到你 其实我当初在选择方向的时候还是挺纠结的,稀里糊涂的就选择了程序员这条路。也浑浑噩噩的度过了几年的日子。后来工作了,发现之前有规划有计划的小伙伴们都成长的很快。而自己就像一条没有梦想也没有追求的咸鱼,根本不知道自己想要什么。其实我一开始工作的时候,对这个专业的兴趣并不是很浓,之所以选择这个专业,无非是因为好找点工作,多挣点钱,毕竟都说:“CS专业是寒门之子逆袭的一条捷径”。既然家庭条件决定了要打工,就早点为打工做准备。可惜当初并没有明白这个道理,现在看来从大学开始,个人选择开始变得格外重要,越早认识到这一点,大学阶段的收获就越大。一些优秀的家庭不止是关注孩子的成绩,更关注的是孩子的认知,一个人的认知往往决定了它的高度,大多数人都不懂这个道理。 成长经历 随着自己慢慢成熟,自己的认知也提升了不少。我渐渐明白做一个人,要做负责的人(包括不限于对自己负责、家人负责、工作负责、爱情负责等)。既然选择这个专业了,并且没有换方向的想法,那就把它做好,对自己负责。 我开始慢慢接触一些技术的圈子,开始慢慢了解我所属的这个行业。在中国互联网的行业常说;程序员的35岁危机、框架更新太快学不会等。弄得程序员这个行业充满焦虑,凡是撩拨这种情绪的文章都能获得大量的转发。我一度也陷入这个焦虑和恐慌中,现在看来其实每个时代都一样,社会并不欠我们一个成功。无论你怎么忙、没时间、有困难、不能痛下决心、拖延症等,社会不管,它不能保证每一个人的成功。Redis cluster2019-03-31T00:00:00+00:002019-03-31T00:00:00+00:00https://dezhen.vip/2019/03/31/Redis%20Cluster<h3 id="前言">前言</h3>
<p>本来最近打算学习Unix网络编程,但是项目中项目中用到了Redis Cluster,自己对Redis集群这方面并不是很熟悉,所以打算花点时间来系统的学习一下Redis,Redis主要分四个部分;<br />
1.数据结构与对象<br />
2.单机数据库实现<br />
3.多机数据库实现<br />
4.独立功能的实现 <br />
本文先简单介绍下Redis 的集群实现,后续会针对上面的在进行详解;</p>
<h3 id="redis-集群介绍">Redis 集群介绍</h3>
<p>Redis因为具有丰富的数据结构和超高额性能以及简单的协议,使其能够很好的作用为数据库的上游。但是当数据量变大的时候(如数据达到千万级别时),会受限以多个地方,单机内存有限、单点问题、动态扩容问题等。<br />
为了解决上面的问题,Redis的集群方案就显得比较重要了。使用Redis集群通常有三个途径;</p>
<ul>
<li>官方提供的 Redis Cluster</li>
<li>通过Proxy分片</li>
<li>客户端分片(Smart Client)</li>
</ul>
<!--more-->
<p>本文主要介绍一下官方提供的 Redis Cluster,Redis Cluster 是在Redis 3.0开始支持的,3.0主要是更新了多机方面的功能,在5.0之前Redis Cluster 主要是采用 Redis提供的 redis-trib.rb(Ruby实现的脚本,需要安装相应的依赖环境)这个管理工具。5.0版本开始支持 –cluster 参数来进行管理。</p>
<h3 id="redis-cluster-数据分片">Redis Cluster 数据分片</h3>
<p>Redis Cluster 并没有使用一致性hash,而是引用了一个叫<strong>哈希槽</strong>的概念。
Redis Cluster 中有16384个哈希槽,每个key通过CRC16校验后对16384取模来决定放置哪个槽。集群的每个节点分配一部分的哈希槽,这样有一点很方便就是在增加和移除的时候,只需要分配相应的哈希槽就可以了。<br />
在单命令执行下和单机Redis并无区别,客户端写入命令,由Redis Cluster计算当前key属于哪个槽点然后返回相应数据。但是多命令的情况下(如求集合的交集)有可能多个key不在同一个槽点里,可以采用{}来设置需要多命令执行的key。{key}:xxx它只对括号里面的进行hash操作,所以可以保证在同一个哈希槽中。</p>
<h3 id="redis-cluster-端口">Redis Cluster 端口</h3>
<p>Redis Cluster 的每个节点都需要维护两个TCP端口,用于为Redis提供服务的普通端口,例如6370,加上通过向数据端口添加10000获得的端口,如16379。<br />
第二个端口是用于集群总线,使用二进制节点到节点的通信通道(gossip 协议)。总线端口的偏移量是固定的,始终为10000。节点使用的集群总线主要用来进行故障检测、数据更新、故障转移授权等 。为了保证集群的正常使用,客户端永不应该尝试与集群总线通讯,始终使用正常的端口通讯。防火墙要打开这两个端口,否则Redis节点将无法通讯。</p>
<h3 id="redis-cluster-故障恢复问题">Redis Cluster 故障恢复问题</h3>
<p>为了提高可用性,Redis 采用了主从模型,其中每一个节点拥有一个master N个slave,如果其中一个节点的 master出现问题,它的一个slave会被选举为master节点。如果某个节点的master和slave全部挂掉,那么这个Redis Cluster将无法服务。其中还有一点需要注意,选举的时候需要有其它一半以上的master节点参加。如果没有一半以上的master节点存活参与选举则slave不会被选举为master。如两个master服务,其中一个挂掉,挂掉服务的slave是不会被选举成为master的,因为存活的master不到一半以上。或者是master刚好分配在一台服务器上,这个服务器挂掉,即使他的slave存在,集群也是没办法将slave节点选举为master节点做到自动故障转移的。所以我们在分配节点的时候一定要注意合理分配,最好自己分配(默认自动分配)。</p>
<h3 id="redis-cluster-强一致性">Redis Cluster 强一致性</h3>
<p>首先我们要知道Redis Cluster 并不提供强一致性。这意味着在某种条件下,Redis 会造成数据丢失。主要原因是因为Redis Cluster 采用了异步复制:</p>
<ol>
<li>客户端向主机写入</li>
<li>主机向客户端回复确认</li>
<li>主机通知该节点的slave设备</li>
</ol>
<p>基于这种情况,如果出现了上面的故障问题,主机在崩溃后没来得及通知其slave服务,并且这个slave被选为master,那么这个数据则会永久丢失。<br />
Redis Cluster 也支持同步复制,通过<a href="https://redis.io/commands/wait">WAIT</a>命令实现,这样会大大的降低数据丢失的可能性。但即使是同步复制,也不可能保证强一致性。在更复杂的情况下总是可以实现失败场景,无法接收写入的slave被选为master。<br />
还有另一个值得注意的情况是,Redis Cluster将丢失写入,这种情况发生在网络分区中 。客户端向节点请求是有最大时间限制(cluster-node-timeout)
不推荐用redis事务机制,因为采用了分片处理。如果一个事物中涉及到多个key的操作的话,这么多个key不一定都存储在同一个节点上。<br />
Redis 分布式锁 <br />
如果确实需要强一致性的话可以考虑采用分布式锁:</p>
<ul>
<li>实现逻辑<br />
在获取锁的时候,使用<a href="https://redis.io/commands/setnx">SETNX</a>加锁,并使用<a href="https://redis.io/commands/expire">EXPIRE</a>命令为锁添加一个超时时间,超过该事件自动释放锁,锁的value值会随机生成一个UUID,释放锁的时候,通过UUID判断是不是该锁,若是该锁,则释放。如果在执行EXPIRE之前服务挂掉了那么这个锁就永远得不到释放了。可以用<a href="https://redis.io/commands/set">SET</a>命令同时设置SETNX和EXPIRE</li>
<li>并发竞争问题<br />
如果不要求执行顺序则让大家去抢锁,抢到了就进行操作。如果需要顺序,可以考虑设置一个时间戳抢到的节点判断当前时间戳是否属于自己,若属于自己在进行操作。</li>
</ul>zhaodezhendezhen98@gmail.com前言 本来最近打算学习Unix网络编程,但是项目中项目中用到了Redis Cluster,自己对Redis集群这方面并不是很熟悉,所以打算花点时间来系统的学习一下Redis,Redis主要分四个部分; 1.数据结构与对象 2.单机数据库实现 3.多机数据库实现 4.独立功能的实现 本文先简单介绍下Redis 的集群实现,后续会针对上面的在进行详解; Redis 集群介绍 Redis因为具有丰富的数据结构和超高额性能以及简单的协议,使其能够很好的作用为数据库的上游。但是当数据量变大的时候(如数据达到千万级别时),会受限以多个地方,单机内存有限、单点问题、动态扩容问题等。 为了解决上面的问题,Redis的集群方案就显得比较重要了。使用Redis集群通常有三个途径; 官方提供的 Redis Cluster 通过Proxy分片 客户端分片(Smart Client)Transmission control protocol2019-03-23T00:00:00+00:002019-03-23T00:00:00+00:00https://dezhen.vip/2019/03/23/Transmission%20Control%20Protocol<p>最近重读的 Stevens 老先生的TCP/IP详解,梳理了一下,打算把自己理解的写出来。<br />
TCP/IP是一种面向连接的、可靠的、基于字节流的传输层通信协议,它会保证数据不丢包、不乱序。TCP全名是Transmission Control Protocol,它是位于网络OSI模型中的第四层(Transport layer)。</p>
<h3 id="tcp-首部">TCP 首部</h3>
<p><img src="/assets/images/tcp header.jpg" alt="tcp header.jpg" /></p>
<ul>
<li>Port<br />
每个TCP数据段都包含源端口和目的端口号,用于寻找发送端和接收端的应用进程。这两个值加上IP首部中的源端IP和目的端IP地址唯有时候我们也会把它称为socket四元组(源IP地址、目的IP地址、源端口、目的端口)<br />
<!--more--></li>
<li>Sequence number<br />
序列号用来标识从TCP发送端向TCO接收端发送的数据字节流,它标识在这个报文段中的第一个数据字节。序号是32 bit的无符号数,序号到达232-1后又从0开始。TCP为应用层提供全双工服务。这意味数据能在两个方向上独立地进行传输。因此,连接的每一端必须保持每个方向上的传输数据序号。</li>
<li>Acknowledgment number<br />
确认号,和序列号类似,<strong>不过它是用来确认已经收到的序号并下次想收到的序号</strong>。这两个序号保证了TCP传输过程中不乱序、不丢包的问题。</li>
<li>TCP Flag<br />
NS:隐藏保护。<br />
CWR:发送主机设置拥塞窗口减少(CWR)标志,以指示它收到了一个设置了ECE标志的TCP段,并在拥塞控制机制中作出响应。<br />
ECE:ECN-Echo具有双重角色,具体取决于SYN标志的值。它表明:<br />
如果SYN标志设置为(1),则TCP对等体具有ECN能力。<br />
如果SYN标志为清除(0),则在正常传输期间接收到IP报头中具有拥塞经历标志设置(ECN = 11)的分组。这用作TCP发送方的网络拥塞(或即将发生的拥塞)的指示。<br />
URG:表示紧急指针字段是重要的<br />
ACK:表示确认字段是重要的。客户端发送的初始SYN数据包之后的所有数据包都应设置此标志。<br />
PSH:推送功能。要求将缓冲的数据推送到接收应用程序。<br />
RST:重置连接<br />
SYN:同步序列号。只有从每一端发送的第一个数据包应该设置此标志。其他一些标志和字段会根据此标志更改含义,有些仅在设置时有效,有些仅在明确时有效。<br />
FIN:来自发送方的最后一个数据包。<br />
关于SYN和FIN可以参考我这篇文章<a href="https://zhaodezhen.github.io/dezhen.github.io/2019/03/15/TCP-3-way-and-4-way-handshake.html">TCP三次握手和四次挥手</a></li>
<li>Window size <br />
TCP的流量控制是由连接的每一端通过声明窗口大小来提供。Window size 是一个16bit的字段,所以窗口最大为65535。</li>
<li>Checksum
它是有发送端计算,然后接收端进行验证。其目的是为了保证在传输过程中出现什么差错,如果校验和验证失败,TCP直接丢弃这个数据段(校验过程中会涉及到一个伪首部,伪首部的数据都是从IP数据报头获取的,其目的就是为了检测TCP数据段是否已经正确到达,只是单纯用来做校验的)。</li>
<li>Urgent pointer
紧急指针,它只在URG标志设置为1的时候生效。</li>
</ul>
<h3 id="tcp-数据传输">TCP 数据传输</h3>
<p>TCP的建立连接之前写过一篇文章,所以就不在这里细赘了,我们直接聊TCP数据传输中如何保证数据的传输顺序和丢包问题的,以及怎么提高TCP传输的吞吐量。</p>
<ul>
<li>Acknowledgement of delay<br />
通常TCP在收到数据的时候不会立刻发送一个ACK确认,它会延迟发送,可以和对方需要的数据一起发送(数据捎带ACK)或者是等待第二个数据来了直接回复第二个ACK,通常的实现采用的延迟是200ms(就是说它会等待200ms有没有数据一起发送)</li>
<li>Nagle <br />
在数据传输过程中,通常会遇到一些小分组的传输(比如 41 bit的数据分组,除去TCP首部和IP首部真正传输的数据只有1 bit),像这种小分组多的话,在网络上传输就加大了造成网络拥塞的可能。为了提传输效率,所以提出了Nagle算法。<br />
这个算法要求一个TCP连接最多只能有一个未被确认的未完成的小分组,在该分组到达之前不能发送其他的小分组。然后,TCP会收集这些小分组,并在确认到来时以一个分组的方式发送出去,这样就可以有效的减少了小分组。
在一些实时性要求比较高的场景下,采用了Nagle算法会让用户感觉到时延,所以我们可以选择关闭Nagle算法,Socket API 可以用 TCP_NODELAY 选项来关闭,nginx上的 tcp_nodely也是采用的这个系统调用。</li>
<li>Retransmission<br />
TCP为了保证数据不丢失所采用的重传策略。 TCP超时重传比较严重,它表示已经超时了还没有收到数据确认的回复,所以他会进入到慢启动,而快速重传则不用。<br />
TCP超时重传:TCP发送方首先会维护一个TCP的重传定时器(有的也叫超时时间RTO),这个定时器是根据往返时间(RTT)进行计算,具体算法的实现可以参考 <a href="https://tools.ietf.org/html/rfc6298">RFC 6298</a>,当RTO到了还没有收到数据的确认,那么TCP就认为数据已经丢失了。TCP会重传数据,接着进入到拥塞控制里的慢启动(关于拥塞控制会在后面讲)。 <br />
TCP快速重传:<strong>它主要是收到了三个重复的ACK后</strong>(接受方如果收到的数据是乱序的。它会重发自己最近接收到的正确顺序的ACK)进行重传,因为收到重复的ACK代表数据已经发送过去了,其中的一个数据可能因为其他原因(如数据传输中换了比较远的路由,或者是数据干脆直接丢了)造成数据没有收到。所以这个情况不算太严重,它不会进入到慢启动,它会进入到快速恢复。<br />
TCP 在收到连续重复ACK后会重传最后顺序确认包的下一个,这样原先已经正确传输的包可能会重复发送,降低了TCP性能。为改善这种情况,发展出SACK(Selective Acknowledgement)技术,使用SACK选项可以告知发包方收到了哪些数据,发包方收到这些信息后就会知道哪些数据丢失,然后立即重传丢失的部分。</li>
</ul>
<h3 id="tcp-滑动窗口">TCP 滑动窗口</h3>
<ul>
<li>滑动窗口<br />
TCP在双方数据传输的过程中,都会维护一个窗口,它代表了我还可以接受的数据的大小。如果接收方窗口大小为0,发送方就会停止发送。之所以叫滑动窗口(Sliding Window)是因为它是动态可变的,不是固定的(张开、合拢、收缩)。它保证了数据的可靠传递、它确保数据按顺序传递、并且它强制发送者之间的流量控制。
<img src="/assets/images/window.jpg" alt="window" /><br />
上图中我们可以看到:<br />
发送端的LastByteAcked指向了接收端最后一次顺序ACK的位置,LastByteSent指向了发送出的数据,但是还没有收到确认ACK。<br />
接收端的NextByteExpected指向了已经收到的最后一个连续数据,LastByteAcked指向了接收到的最后一个数据,其中的空白代表还未收到的数据。<br />
下面看一张滑动窗口的示意图:<br />
<img src="/assets/images/tcpswpointers.png" alt="tcpswpointers.png" />
SND.UNA:已发送但尚未确认的数据的第一个字节的序列号。 这标志着传输类别#2的第一个字节; 所有先前的序列号都是指传输类别#1中的字节。<br />
SND.NXT:要发送到另一个设备(在这种情况下是服务器)的下一个数据字节的序列号。 这标志着传输类别#3的第一个字节。 <br />
SND.WND:发送窗口的大小。 回想一下,窗口指定任何设备在任何时候都可能具有“未完成”( 未确认 )的总字节数。 因此,添加第一个未确认字节( SND.UNA )和发送窗口(SND.WND )的序列号标记发送类别#4的第一个字节。
SND.UNA:已经发送但是尚未确认的
SND.NXT:将要发送的
SND.WND:发送窗口的大小
#1 表示已经确认过的数据,所以窗口右移,黑色代表窗口大小。 <br />
#2 表示已经发送的,但是还没有收到确认。<br />
#3 表示还没有发送的,接受方可以接收的数据。<br />
#4 表示不能发送的数据,接收方不能接收的数据。</li>
</ul>
<p>下面看一张TCP窗口滑动的示意图:
<img src="/assets/images/tcpswexampleserver.png" alt="tcpswexampleserver.png" /></p>
<ul>
<li>糊涂窗口<br />
我们看到了TCP通过让接收方指明窗口来进行流量控制,这将有效的组织发送方放松数据,直到窗口变为非0为止。但是其中会遇到一个问题,就是接收方发送的的的窗口更新数据丢失,这样会让发送方进入到无限等待状态,因为他要等待窗口更新为非0。为了解决这个问题TCP采用了坚持定时器(persist timer)去探测窗口更新。
这样又会导致一种被称为“糊涂窗口综合症SWS (Silly Window Syndrome)”的状况。如果发生这种情况,则少量的数据将通过连接进行交换,而不是满长度的报文段。 <br />
该现象可发生在两端中的任何一段,接受方可以通告一个小的窗口(而不是一直等待有大的窗口才通告),发送方也可以发送少量的数据(而不是等待其他的数据以便发送一个大的数据段)。可以在任何一端采取避免SWS的现象。<br />
1.接收方不通告小窗口。通常的算法是接收方不通告一个比当前窗口大的窗口(可以为0),除非窗口可以增加一个报文段大小(也就是将要接收的MSS)或者可以增加接收方缓存空间的一半,不论实际有多少。<br />
2.发送方避免出现糊涂窗口综合症的措施是只有以下条件之一满足时才发送数据:(a)可以发送一个满长度的报文段;(b)可以发送至少是接收方通告窗口大小一半的报文段;(c)可以发送任何数据并且不希望接收ACK(也就是说,我们没有还未被确认的数据)或者该连接上不能使用Nagle算法。
<h3 id="tcp-拥塞控制">TCP 拥塞控制</h3>
<p>TCP不仅可以可以控制端到端的数据传输,还可以对网络上的传输进行监控。这使得TCP非常强大智能,它会根据网络情况来调整自己的收发速度。网络顺畅时就可以发的快,拥塞时就发的相对慢一些。拥塞控制算法主要有四种:慢启动,拥塞避免,快速重传和快速恢复。</p>
</li>
<li>慢启动和拥塞避免<br />
慢启动和拥塞避免算法必须被TCP发送端用来控制正在向网络输送的数据量。为了
实现这些算法,必须向TCP每连接状态加入两个参量。拥塞窗口(cwnd)是对发送端收到确
认(ACK)之前能向网络传送的最大数据量的一个发送端限制,接收端通知窗口(rwnd)是对
未完成数据量的接收端限制。cwnd和rwnd的最小值决定了数据传送。
另一个状态参量,慢启动阀值(ssthresh),被用来确定是用慢启动还是用拥塞避免
算法来控制数据传送。
在不清楚环境的情况下向网络传送数据,要求TCP缓慢地探测网络以确定可用流量,避免突然传送大量数据而使网络拥塞。在开始慢启动时cwnd为1,每收到一个用于确认新数据的ACK至多增加SMSS(SENDER MAXIMUM SEGMENT SIZE)字节。
慢启动算法在cwnd<ssthresh时使用,拥塞避免算法在cwnd>ssthresh时使用。当cwnd和ssthresh相等时,发送端既可以使用慢启动也可以使用拥塞避免。
当拥塞发生时,ssthresh被设置为当前窗口大小的一半(cwnd和接收方通告窗口大小的最小值,但最少为2个报文段)。如果是超时重传,cwnd被设置为1个报文段(这就是慢启动,其实慢启动也不慢,它是指数性增长,只是它的起始比较低)当达到ssthresh时,进入拥塞避免算法(拥塞避免是线性增长)。
<img src="/assets/images/congwin.jpg" alt="congwin.jpg" /></li>
</ul>
<p>在该图中我们可以清楚的看到,ssthresh最初等于8 MSS 。 拥塞窗口在慢启动期间以指数方式快速上升并在第三次传输时达到ssthresh。 然后,拥塞窗口线性地爬升,直到发生丢失(超时),就在发送7之后。当发生丢失时,拥塞窗口是12 MSS 。 然后将ssthresh设置为6 MSS并且将cwnd设置为1,并且该过程继续。</p>
<ul>
<li>快速重传和快速恢复 <br />
当接收端收到一个顺序混乱的数据,它应该立刻回复一个重复的ACK。这个ACK的目的是通知发送端收到了一个顺序紊乱的数据段,以及期望的序列号。发送端收到这个重复的ACK可能有多种原因,可能丢失或者是网络对数据重新排序等。在收到三个重复ACK之后(包含第一次收到的一共四个同样的ACK),TCP不等重传定时器超时就重传看起来已经丢失(可能数据绕路并没有丢失)的数据段。因为这个在网络上并没有超时重传那么恶劣,所以不会进入慢启动,<strong>而进入快速恢复</strong>。快速恢复首先会把ssthresh减半(一般还会四舍五入到数据段的倍数),然后cwnd=ssthresh+收到重复ACK报文段累计的大小。
<img src="/assets/images/1553401836470.jpg" alt="1553401836470.jpg" />
这个图上我们可以看出,在三次重复ACK后cwnd并没有进入到慢启动,而是进入到了快速重传。在第二段超时重传时,进入到了慢启动cwnd置1。
<h3 id="总结">总结</h3>
<p>本来打算以最少的文字去解释TCP,但是并不是很成功。TCP发展至今已经有几十年了,其中的技术点都可以出好几本书了。你可以把它当个索引,快速浏览一遍。下面我列一下在写这篇文章时参考的文档,都很不错,值得一读。<br />
<a href="https://www.net.t-labs.tu-berlin.de/teaching/computer_networking/03.07.htm">TCP Congestion Control</a><br />
<a href="https://en.wikipedia.org/wiki/Transmission_Control_Protocol">Transmission Control Protocol</a><br />
<a href="http://www.cs.uni.edu/~diesburg/courses/cs3470_fa14/sessions/s29/s29.pdf">TCP Sliding Window</a><br />
<a href="http://www.tcpipguide.com/free/t_TCPSlidingWindowDataTransferandAcknowledgementMech-5.htm">TCP/IP Guide</a><br />
<a href="https://tools.ietf.org/html/rfc5681#section-9.1">rfc 5681</a></p>
</li>
</ul>zhaodezhendezhen98@gmail.com最近重读的 Stevens 老先生的TCP/IP详解,梳理了一下,打算把自己理解的写出来。 TCP/IP是一种面向连接的、可靠的、基于字节流的传输层通信协议,它会保证数据不丢包、不乱序。TCP全名是Transmission Control Protocol,它是位于网络OSI模型中的第四层(Transport layer)。 TCP 首部 Port 每个TCP数据段都包含源端口和目的端口号,用于寻找发送端和接收端的应用进程。这两个值加上IP首部中的源端IP和目的端IP地址唯有时候我们也会把它称为socket四元组(源IP地址、目的IP地址、源端口、目的端口)Tcp 3 Way and 4 Way handshake2019-03-15T00:00:00+00:002019-03-15T00:00:00+00:00https://dezhen.vip/2019/03/15/TCP%203-way%20and%204-way%20handshake<h2 id="tcp三次握手">TCP三次握手</h2>
<p>TCP是一个面向连接的的协议,所以无论哪一方发送数据之前都必须要建立连接。三次握手是TCP/IP网络中用于在本地主机/客户端和服务器之间创建连接的方法。这是一种三步方法,要求客户机和服务器在实际数据通信开始之前交换SYN和ACK(确认)数据包。
要建立连接,将发生三次握手:</p>
<p>1.SYN:主动打开由客户机向服务器发送syn来执行。客户机将段的序列号(SEQ)设置为随机值A。<br />
2.SYN-ACK:作为响应,服务器用SYN-ACK进行响应。确认号被设置为比接收到的序列号多一个,即A+1,服务器为数据包选择的序列号是另一个随机数B。<br />
3.最后,客户机将一个ACK发送回服务器。序列号设置为接收到的确认值,即A+1,确认号设置为接收到的序列号(即B+1)的一个以上。</p>
<p>此时,客户机和服务器都已收到连接确认。步骤1、2建立一个方向的连接参数(序列号),并确认。步骤2、3为另一个方向建立连接参数(序列号),并确认。通过这些,建立了全双工通信。<br />
<img src="/assets/images/tcp3%264.jpg" alt="TCP 3-way and 4-way handshake" /></p>
<h2 id="tcp四次挥手">TCP四次挥手</h2>
<p>TCP的关闭是四次,也就是说需要双方都发送FIN告知对方我要关闭了,并回复FIN-ACK。所以在一方关闭的时候另一方还可以发送数据,我们把这种现象称为TCP半关闭。当然主动方发起方也可以选择不接受。TCP四次挥手:<br />
<!--more--> <br />
1.FIN:主动打开方通知向被动方发送FIN,告知被动方我已经完成数据发送要关闭连接了。自己进入FIN_WAIT1<br />
2.ACK:服务器收到对方FIN并回复ACK告知我已收到(这里不像建立连接一样,把ACK和自己的FIN一起发送是因为自己有可能还没完成数据的传输)自己可以继续进行未完成的数据传输并把状态设置成CLOSE_WAIT,对方收到ACK把自己的状态变成FIN_WAIT2并继续等待被动方的FIN<br />
3.FIN:服务端数据传输完成后向主动打开方发送发送FIN,等待对方的ACK,被动关闭的一方进入LAST_ACK状态<br />
4.ACK:主动方收到被动方发送的FIN,回复ACK</p>
<p>此时,主动方进入到TIME_WAIT状态然后等待2MSL(Maximum segment lifetime 它是任何报文段被丢弃前在网络内的最长时间)后进入到CLOSED关闭,被动方收到ACK后进入到CLOSED。因为网络环境是复杂多变的,有可能自己的最后一个ACK丢失导致对方重传FIN。所以主动发起方要等待2MSL来预防对方重传<br />
最后附上一副TCP状态机,可以参考TCP/IP illustrated Vol. 1 第18章(强烈推荐英文版,如果英文不好可以买本中文的参考着看)
<img src="/assets/images/TCP%20Finite%20State%20Machine.jpg" alt="TCP state machine" /></p>zhaodezhendezhen98@gmail.comTCP三次握手 TCP是一个面向连接的的协议,所以无论哪一方发送数据之前都必须要建立连接。三次握手是TCP/IP网络中用于在本地主机/客户端和服务器之间创建连接的方法。这是一种三步方法,要求客户机和服务器在实际数据通信开始之前交换SYN和ACK(确认)数据包。 要建立连接,将发生三次握手: 1.SYN:主动打开由客户机向服务器发送syn来执行。客户机将段的序列号(SEQ)设置为随机值A。 2.SYN-ACK:作为响应,服务器用SYN-ACK进行响应。确认号被设置为比接收到的序列号多一个,即A+1,服务器为数据包选择的序列号是另一个随机数B。 3.最后,客户机将一个ACK发送回服务器。序列号设置为接收到的确认值,即A+1,确认号设置为接收到的序列号(即B+1)的一个以上。 此时,客户机和服务器都已收到连接确认。步骤1、2建立一个方向的连接参数(序列号),并确认。步骤2、3为另一个方向建立连接参数(序列号),并确认。通过这些,建立了全双工通信。 TCP四次挥手 TCP的关闭是四次,也就是说需要双方都发送FIN告知对方我要关闭了,并回复FIN-ACK。所以在一方关闭的时候另一方还可以发送数据,我们把这种现象称为TCP半关闭。当然主动方发起方也可以选择不接受。TCP四次挥手:最近的半年的总结2019-03-15T00:00:00+00:002019-03-15T00:00:00+00:00https://dezhen.vip/2019/03/15/%E6%9C%80%E8%BF%91%E7%9A%84%E5%8D%8A%E5%B9%B4%E7%9A%84%E6%80%BB%E7%BB%93<h3 id="前言">前言</h3>
<p>写这篇文章中心中也是思绪万千,最近做的事情很多,成长也蛮大。所以自己一直想把自己最近做的事都写出来,可是真正开始写的时候,又不知从何写起。</p>
<h3 id="做好项目规划真的很重要">做好项目规划真的很重要</h3>
<p>最近一年负责的项目运营的还不错,现在大概每天能有亿级流量。当时是为了快速开发,就用PHP做了一个最小可用版本。上线之后运营那边数据很好,每天都能有10万+的新增,用户粘合性也比较高,所以就开始迭代。因为自己是第一次用0-1把控整个项目,再加上是为了抢占用户红利快速开发,很多事情提前都没有架构好。所以可想而知,迭代几个版本后维护成本急剧增加。每次改版或者发布新功能都要小心翼翼,生怕影响到哪儿导致出致命bug。好在及时感知到了问题所在,在不影响新功能开发的情况下,我加班加点重新梳理规划了项目。规划后感觉一切都明朗了起来,维护起来也不是那么困难了,开发效率也提高了很多。</p>
<h3 id="基础重要吗">基础重要吗</h3>
<!--more-->
<p>之前看过TCP/IP详解,当时觉得很枯燥不懂为什么要学习这枯燥的理论知识,一点都没有编程好玩。好在我还是按照书上的知识一个一个的学习起来了,对应的练习题也写了七七八八。年前做性能调优的时候遇到几个服务器问题,其中有几个是关于TCP的问题,然后发现当初我看那本早已不知道放哪儿吃土的TCP/IP详解竟然派上了很大的用处。像基本的TIME_WAIT过多、连接队列积压过多、溢出等可以排上用处,更有像系统调用、上下文切换也和TCP有关。没想到当初觉得枯燥而随便看看的书,竟然能派上这么大的用处,至那以后,我才算真正的看进去那本书了。<br />
所以我们面临一个技术时,如果觉得它枯燥或者是生涩难懂,那么可能是你现在还不适合读这本书,很难和作者达到共鸣,等到和作者达到共鸣,然后带着问题或者疑问去看的时候,看到里面有解决你心中的疑问或思考时,那种理性和感性的双重认识真的很爽,看书也变得有滋有味。<br />
之前一直面试的时候经常被问到一些关于大流量网站的处理、高并发、分布式、负载均衡、监控、熔断、降级、限流等等问题。因为之前没有处理过较大流量的网站,关于大流量网站的处理的知识全来自自己看过的书,或者听过的公开课,所以自己对这些也是知之甚少,而且有些东西只知道表面。
后来着手做一些关于分布式的相关处理时,一开始压力其实蛮大的,一边学习着一边规划着做。后来慢慢的也做下来,此次过程中我发现分布式的一些问题和我之前学习的计算机的操作系统有很大一部分类似,包括lock,同步互斥,进程之间的通讯、共享内存等,无奈操作系统学的不是很好,未能反补到分布式架构上。当初计算机刚刚出现的时候是很贵的,所以程序员们会想方设法的提高计算机的利用率,一台机器往往会有很多用户共同使用工作,所以会解决一些多用户并发并行执行任务等思想,很适合嫁接到分布式上面。关于分布式的技术栈是很大的,我目前也是在学习当中,所以就不描述分布式的一些实现,google上面有很多论文可以参考。</p>
<h3 id="程序员的荒谬之言还是至理名言">程序员的荒谬之言还是至理名言</h3>
<p>引用这个标题其实是在耗子叔的coolshell上看到的一篇文章 <a href="https://coolshell.cn/articles/4235.html">程序员的谎谬之言还是至理名言?</a> 还有这篇传世之文也推荐阅读一下 <a href="http://matt.might.net/articles/what-cs-majors-should-know/">What every computer science major should know</a> 很多网上关于职业发展的文章上都有谈“到带着问题去学习,会学的更快。” 但是我发现很多人误解成了“遇到问题了再去学习,会学的很快。”确实,遇到问题了你不得不学,可能会比你自主学习快。但事实是谁给你时间让与遇到问题了去学习?公司会吗?显然公司不会。公司肯定从无论是从公司的利益考虑还是其他方面考虑肯定都是让你解决问题,而不是遇到问题了去学习怎么解决问题然后再去解决。你可以耽搁的起事件,但是往往公司可能会因为这个问题造成巨大的损失,公司是耽搁不起的。
如果是你自己遇到问题了,你自己可能会给你事件让你带着问题去学习。拿我举例,我之前就是带着问题一遍学着一边做。遇到问题其实不可怕,因为问题伴随着的往往是机会。可是如果你解决不了是人没有机会给你的,我这面呢一是我态度比较好,自学能力和抗压能力都比较强,二是公司这块也缺人,如果公司不缺人我觉得也没有我的什么事了。因为我目前的能力负责这块确实不能应付自如。所以不可能每个人都有机会等用到时再去学,一般都是行你就上,不行就下来。所以很多时候我们应该要提前准备好,职业危机感也要有的,而不是等到自己遇到问题了才想着去翻书本,被辞退了才想着学补算法。</p>
<h3 id="2019期望">2019期望</h3>
<p>说一下今年的个人学习计划吧,期待明年来打脸;<br />
1.是学习基础知识,新技术往往运行在底层知识之上的,是变化很快的,唯一不变的可能就是一些底层知识,所以学好底层知识才能在这个快速变化的行业中跟的上脚步。<br />
2.学习好行业知识,不要等到用的时候才发现这个不会<br />
3.加入了耗子叔组织的学习小组,跟着大家一起学习
针对上面点细化一下<br />
1.学习计算机操作系统,复习C语言用C语言写一个小型的OS系统<br />
2.学习计算机网络编程<br />
3.使用golang为目前负责的项目重构一版<br />
4.每周分享一篇有观点和思考的技术文章
5.每周至少做一个 leetcode 的算法题<br />
6.每周阅读并点评至少一篇英文技术文章<br />
7.每周学习至少一个技术技巧<br />
学习参考:<br />
<a href="https://www.coursera.org/learn/os-pku/lecture/7Srsj/ci-pan-kong-jian-guan-li">操作系统原理</a><br />
深入理解计算机操作系统<br />
<a href="https://www.xuetangx.com/courses/course-v1:TsinghuaX+30240243X+sp/courseware/4e59d5c6e03246efac6c1c8b3a6233c3/">操作系统(自主模式)</a><br />
TCP/IP详解<br />
UNIX环境高级编程<br />
UNIX网络编程</p>zhaodezhendezhen98@gmail.com前言 写这篇文章中心中也是思绪万千,最近做的事情很多,成长也蛮大。所以自己一直想把自己最近做的事都写出来,可是真正开始写的时候,又不知从何写起。 做好项目规划真的很重要 最近一年负责的项目运营的还不错,现在大概每天能有亿级流量。当时是为了快速开发,就用PHP做了一个最小可用版本。上线之后运营那边数据很好,每天都能有10万+的新增,用户粘合性也比较高,所以就开始迭代。因为自己是第一次用0-1把控整个项目,再加上是为了抢占用户红利快速开发,很多事情提前都没有架构好。所以可想而知,迭代几个版本后维护成本急剧增加。每次改版或者发布新功能都要小心翼翼,生怕影响到哪儿导致出致命bug。好在及时感知到了问题所在,在不影响新功能开发的情况下,我加班加点重新梳理规划了项目。规划后感觉一切都明朗了起来,维护起来也不是那么困难了,开发效率也提高了很多。 基础重要吗Eclipse报错信息jvm terminated. exit code=12018-09-28T00:00:00+00:002018-09-28T00:00:00+00:00https://dezhen.vip/2018/09/28/Eclipse%E6%8A%A5%E9%94%99%E4%BF%A1%E6%81%AFJVM%20terminated.%20Exit%20code=1<h2 id="报错信息如下">报错信息如下:</h2>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>JVM terminated. Exit code=1
/usr/local/eclipse/jre/bin/java
-Dosgi.requiredJavaVersion=1.8
-Dosgi.instance.area.default=@user.home/eclipse-workspace
-XX:+UseG1GC
-XX:+UseStringDeduplication
-Dosgi.requiredJavaVersion=1.8
-Xms256m
-Xmx1024m
-jar /usr/local/eclipse//plugins/org.eclipse.equinox.launcher_1.4.0.v20161219-1356.jar
-os linux
-ws gtk
-arch x86_64
-showsplash /usr/local/eclipse//plugins/org.eclipse.epp.package.common_4.7.1.20170914-1200/splash.bmp
-launcher /usr/local/eclipse/eclipse
-name Eclipse
--launcher.library /usr/local/eclipse//plugins/org.eclipse.equinox.launcher.gtk.linux.x86_64_1.1.500.v20170531-1133/eclipse_1624.so
-startup /usr/local/eclipse//plugins/org.eclipse.equinox.launcher_1.4.0.v20161219-1356.jar
--launcher.appendVmargs
-exitdata 21000b
-product org.eclipse.epp.package.java.product
-vm /usr/local/eclipse/jre/bin/java
-vmargs
-Dosgi.requiredJavaVersion=1.8
-Dosgi.instance.area.default=@user.home/eclipse-workspace
-XX:+UseG1GC
-XX:+UseStringDeduplication
-Dosgi.requiredJavaVersion=1.8
-Xms256m
-Xmx1024m
-jar /usr/local/eclipse//plugins/org.eclipse.equinox.launcher_1.4.0.v20161219-1356.jar
</code></pre></div></div>
<p>第一种: eclipse.ini中内存设置过大的问题,修改了一下,256m改成128m,把512m 改为 256m,即可。<br />
<!--more--><br />
原因:大内存的配置导致的。<br />
第二种:在eclipse.ini 中,增加了如下两行后,问题解决: <br />
mv /usr/local/Java/jdk1.8.0/bin/java <br />
其中java是我的JDK安装路径。同时,完整的eclipse.ini如下</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>-startup plugins/org.eclipse.equinox.launcher_1.0.101.R34x_v20081125.jar
showsplash org.eclipse.platform
launcher.XXMaxPermSize 512m
vm /usr/local/Java/jdk1.8.0/bin/java
vmargs -Xms40m -Xmx256m
Djava.net.preferIPv4Stack=true
</code></pre></div></div>zhaodezhendezhen98@gmail.com报错信息如下: JVM terminated. Exit code=1 /usr/local/eclipse/jre/bin/java -Dosgi.requiredJavaVersion=1.8 -Dosgi.instance.area.default=@user.home/eclipse-workspace -XX:+UseG1GC -XX:+UseStringDeduplication -Dosgi.requiredJavaVersion=1.8 -Xms256m -Xmx1024m -jar /usr/local/eclipse//plugins/org.eclipse.equinox.launcher_1.4.0.v20161219-1356.jar -os linux -ws gtk -arch x86_64 -showsplash /usr/local/eclipse//plugins/org.eclipse.epp.package.common_4.7.1.20170914-1200/splash.bmp -launcher /usr/local/eclipse/eclipse -name Eclipse --launcher.library /usr/local/eclipse//plugins/org.eclipse.equinox.launcher.gtk.linux.x86_64_1.1.500.v20170531-1133/eclipse_1624.so -startup /usr/local/eclipse//plugins/org.eclipse.equinox.launcher_1.4.0.v20161219-1356.jar --launcher.appendVmargs -exitdata 21000b -product org.eclipse.epp.package.java.product -vm /usr/local/eclipse/jre/bin/java -vmargs -Dosgi.requiredJavaVersion=1.8 -Dosgi.instance.area.default=@user.home/eclipse-workspace -XX:+UseG1GC -XX:+UseStringDeduplication -Dosgi.requiredJavaVersion=1.8 -Xms256m -Xmx1024m -jar /usr/local/eclipse//plugins/org.eclipse.equinox.launcher_1.4.0.v20161219-1356.jar 第一种: eclipse.ini中内存设置过大的问题,修改了一下,256m改成128m,把512m 改为 256m,即可。Codeigniter3 session问题2018-08-30T00:00:00+00:002018-08-30T00:00:00+00:00https://dezhen.vip/2018/08/30/codeigniter3%20session%E9%97%AE%E9%A2%98<p>近期负责维护一个16年的老项目,需要迁移到我负责的服务上,PHP版本7.1。移植完成后打开看到登录页面,正高兴着发现系统登录不进去,看起来像是session问题,在本地环境PHP7.2测试是没问题的 。</p>
<h2 id="问题描述">问题描述</h2>
<p>观察http请求,发现http请求带的sessionID和相应的ID不匹配,每次服务都response一个新的sessionID。在框架入口文件加入 session_start(); 观察返回的sessionID并没有问题,但是框架内置的session就会出现问题,这是为什么呢?<br />
查看框架封装的session类,在 system/libraries/Session/Session.php:129 有如下代码</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>if (isset($_COOKIE[$this->_config['cookie_name']])
&& (
! is_string($_COOKIE[$this->_config['cookie_name']])
OR ! preg_match('/^[0-9a-f]{40}$/', $_COOKIE[$this->_config['cookie_name']])
)
)
{
unset($_COOKIE[$this->_config['cookie_name']]);
}
</code></pre></div></div>
<p>这个会检查cookie的sessionID格式是否正确,刚好我现在的格式不正确(可以在看cookie值或者是session_id();查看)所以这个cookie的值被清理掉了,导致session值也取不出来
<!--more--></p>
<h2 id="解决方案">解决方案</h2>
<p>找到问题了就好解决了,查看官网上有关于session长度的设置</p>
<p>`
** session.sid_length **<br />
session.sid_length allows you to specify the length of session ID string. Session ID length can be between 22 to 256. The default is 32. If you need compatibility you may specify 32, 40, etc.<br />
Longer session ID is harder to guess. At least 32 chars is recommended.Compatibility Note: Use 32 for session.hash_func=0 (MD5) and session.hash_bits_per_character=4,session.hash_func=1 (SHA1) and session.hash_bits_per_character=6. Use 26 for session.hash_func=0 (MD5) and session.hash_bits_per_character=5. Use 22 for session.hash_func=0 (MD5) andsession.hash_bits_per_character=6. You must configure INI values to have at least 128 bits in session ID. Do not forget set appropriate value to session.sid_bits_per_character, otherwise you will have weaker session ID.
`</p>
<p>还有一个问题,如果有a-f以外的字符也是不行的(参考下面把正则改成自己相应的)</p>
<p>`
session.sid_bits_per_character<br />
session.sid_per_character allows you to specify the number of bits in encoded session ID character. The possible values are ‘4’ (0-9, a-f), ‘5’ (0-9, a-v), and ‘6’ (0-9, a-z, A-Z, “-“, “,”). The default is 4. The more bits results in stronger session ID. 5 is recommended value for most environments.
`</p>
<p>回到codeigniter的GitHub查,发现这个问题是在一些早期版本的codeigniter 3,这是一个bug已经在他们的网站上报道。bug已经被修复,在最新版本的codeigniter 3中,这个问题不会发生。如果你已经在一个错误的版本codeigniter考虑替换系统文件夹与最新版本的一个<br />
参考连接 :<a href="https://stackoverflow.com/questions/43718961/codeigniter-3-session-not-working-with-php-7-1-4">Codeigniter 3 Session not working With PHP 7.1.4</a></p>zhaodezhendezhen98@gmail.com近期负责维护一个16年的老项目,需要迁移到我负责的服务上,PHP版本7.1。移植完成后打开看到登录页面,正高兴着发现系统登录不进去,看起来像是session问题,在本地环境PHP7.2测试是没问题的 。 问题描述 观察http请求,发现http请求带的sessionID和相应的ID不匹配,每次服务都response一个新的sessionID。在框架入口文件加入 session_start(); 观察返回的sessionID并没有问题,但是框架内置的session就会出现问题,这是为什么呢? 查看框架封装的session类,在 system/libraries/Session/Session.php:129 有如下代码 if (isset($_COOKIE[$this->_config['cookie_name']]) && ( ! is_string($_COOKIE[$this->_config['cookie_name']]) OR ! preg_match('/^[0-9a-f]{40}$/', $_COOKIE[$this->_config['cookie_name']]) ) ) { unset($_COOKIE[$this->_config['cookie_name']]); } 这个会检查cookie的sessionID格式是否正确,刚好我现在的格式不正确(可以在看cookie值或者是session_id();查看)所以这个cookie的值被清理掉了,导致session值也取不出来