learning, progress, future.

skydh


  • 首页

  • 归档

mysql 入门一条查询sql如何运行的

发表于 2018-11-15

mysql的构成

server层

连接器

  客户端连接到mysql数据库的时候,接待你的是连接器。连接器是连接器负责跟客户端建立连接、获取权限、维持和管理连接。在完成3次tcp握手后,通过账号密码进行身份验证,验证通过后从权限表里面读取权限,该链接后续的操作权限都是基于这个时候读取的权限。

  数据库连接有长连接和短连接,和tcp的很类似,都是连接器长期持有这些连接。长连接是客户端有请求时长时间持有这个连接,而短连接则是短暂的处理之后就断掉,建议客户端使用连接池,来保持多个长连接,减少连接的成本。
  但是可能出现OOM异常,原因如下:mysql在执行过程中临时使用的内存是管理在连接对象里面的,这些资源会在连接断开才释放,一直保持长连接,可能导致内存占用太大。解决方案为当你执行一个大的操作时,通过执行 mysql_reset_connection 来重新初始化连接资源,这个过程不需要重连,只是释放这个连接在mysql中占用的内存,将其初始化到最初形态。
  一个连接建立连接后,管理员修改了其权限,但是不会影响其现在的连接,权限的修改只能影响到新的连接。

查询缓存

  当你连接建立完毕后,使用select语句的时候,可以从查询缓存里面查询,因为在这个查询缓存里面 查询语句,结果,会以键值对的方式放到里面,当可以从查询缓存里面取出数据时返回,但是,你缓存表的数据出现问题时,缓存就会失效,因此很不划算,在mysql8已经取消查询缓存这个模块。

分析器

  真正的第二步。在这个步骤里面,mysql会对sql进行词法分析,词法分析器会对sql进行分析,判断其是否符合mysql语法。

优化器

  mysql的优化器是为了提高sql的效率,比如表里面有多个索引,用哪个索引(一条sql一个表只用一个索引),表连接,比如内连接,谁连接谁,优化器会采用优化策略,让有索引的放在右边边被连接,或者都有索引的,且高度一样的。小表放在左边,因为左边的表是必须全扫描的。对表连接感兴趣的可以看我的下面这个文章,关于表的连接算法。
点我一下就到了

执行器

  经过优化器后,下一步就是执行,执行这个必须先判断你是否有这个权限,然后通过表的引擎来执行,比如查询,如果没有索引,那么就通过存储引擎读取第一行数据,执行器调用一次,扫描一次。比较,然后不断循环直到找到数据,或者循环完毕。使用索引同样的道理。我们以前查看explain查看扫描的行树,来看查询效率。但是在有些情况下,执行器执行一次,引擎扫描多行数据。就是调用存储引擎干活。

between 和大小范围

  between a and b。
  这个是索引的between,逻辑如下,先找到a,或者第一个大于a的,然后不断向后取数据,直到出现数据大于b为止。

git双库合并

发表于 2018-11-15

原因

  1.本项目a开发时基于其他项目组的一个项目b进行扩展开发,但是b项目没有开发完,我们也要赶进度,于是基于b项目的主分支,copy出来后,新建了一个新的仓库,进行新的开发。然后,a,b项目都开发完了,现在a项目要发布到线上,需要合并b项目代码仓库的代码。

方案

  1.添加b的代码的源,作为新的源,命名为other。
git remote add other http://….

  2.获取b的代码
git fetch other

  3.新建一个分支存放b的代码
git checkout -b newbranch other/dev

  4.在切换为dev分支
git checkout dev

  5.在合并代码
git merge newbranch

问题

  1.为何可以直接merge,本质上这2个库是有共同的祖先的。因此可以直接merge。调用git merge-base dev new,是有共同祖先的。
  2.没有共同祖先怎么破,已经找不到了,直接使用git merge newbranch 时报错的,
  那如何,使用命令如下:
  git merge newbranch  –allow-unrelated-histories
  可以直接强行merge。只不过要解决冲突的文件更加多了。   

微服务入门001 OAuth 2.0

发表于 2018-10-24

什么是OAuth2.0

  1.用于rest/api的代理请求框架。
  2.解耦认证和授权
  3.基于令牌Token的授权,在无需暴露用户密码的情况下,让应用能获得对用户数据的访问权限。
  4.事实上的标准安全框架,支持多种用例场景。适用于,服务端webapp,单页spa,原生app,服务器服务器之间。

OAuth2.0的优势

  1.易于实现。
  2.更加安全,客户端不接触密码,服务端集中保护。本质上就是解决这个问题。
  3.广泛传播并持续采用。
  4.短寿命和封装的token。
  5. 资源服务器和授权服务器解耦。一个负责授予token权限(客户端访问其,其返回页面给用户,判断是否同意授权,同意,则授予让其有资格访问我的数据),一个负责判断请求是否具有token
  6.集中授权
  7.基于http/json,易于请求和传递token
  8.考虑了多种客户端场景
  9.客户端有多种信任级别。

OAuth2.0的不足

  1.协议太宽泛了,因此各个实现版本兼容性不好。
  2.和Oauth1.0不兼容。
  3.Oauth2.0不是一个认证协议,他是个授权框架,他本省不会告诉你任何用户信息。是个授权协议。

Oauth2.0主要角色术语

  1.资源拥有者:资源的拥有者,用户。
  2.客户应用:通常是一个web应用,想要访问收到保护的数据。比如微信用户数据等。
  3.资源服务器:一个web站点或者service api,客户想要访问的数据在我这里。
  4.授权服务器:客户想要访问受保护的数据,必须先到我这里来认证,来获取到access token。
  5.客户凭证:在授权服务器上注册后获得的,客户的ClientId和密码用于认证客户。
  6.令牌:授权服务器接受到客户端请求后,验证后颁发的访问令牌
  7.作用域:客户请求访问令牌是,由资源额外指定的细分权限。

Oauth2.0 令牌类型

  1.access token:访问令牌用来代表一个用户或者服务直接去访问受保护的资源。
  2.refresh token:刷新令牌,勇于去授权服务器获取一个新的令牌。(有时候访问令牌过期了,有的流程会支持获取到一个refreshtoken,我可以通过这个换取一个新的令牌)
  3.Bearer Token:不管谁拿到token,都可以访问资源。
  4.pop token:可以校验client是否对token的用友权。
  5.Authorization Code Token:授权码,仅用于授权码授权类型。用于交换获取访问令牌和刷新令牌。

Ouath2.0的误区

  1.该协议仅仅支持http协议。
  2.Oauth 是个授权协议,不是认证协议。
  3.oauth 没有定义token格式
  4.没有定义加密算法
  5.不是单个协议
  6.没有定义授权处理机制
  7.仅仅是个授权框架,用于授权代理   

授权模式

授权码模式

用下面转载自码农翻身的一张图来解释下

aaa

  1.用户访问客户端,客户端将用户重定向到授权服务器。同时附上客户端凭证和处理完后要重定向的url。
  2.用户选择是否给予客户端授权。
  3.用户给予授权,授权服务器将用户导向客户端事先指定的”重定向URI”(redirection URI),同时附上一个授权码。
  4.客户端收到授权码,附上早先的”重定向URI”,向认证服务器申请令牌。这一步是在客户端的后台的服务器上完成的,对用户不可见。
  5.认证服务器核对了授权码和重定向URI,确认无误后,向客户端发送访问令牌(access token)和更新令牌(refresh token)。

简化模式

  (A)客户端将用户导向认证服务器。
  (B)用户决定是否给于客户端授权。
  (C)假设用户给予授权,认证服务器将用户导向客户端指定的”重定向URI”,并在URI的Hash部分包含了访问令牌。
  (D)浏览器向资源服务器发出请求,其中不包括上一步收到的Hash值。
  (E)资源服务器返回一个网页,其中包含的代码可以获取Hash值中的令牌。
  (F)浏览器执行上一步获得的脚本,提取出令牌。
  (G)浏览器将令牌发给客户端。

密码模式

  这个通常需要客户端是操作系统的一部分,或者是一个著名公司出品,而认证服务器无法授权处理才使用。
  (A)用户向客户端提供用户名和密码。
  (B)客户端将用户名和密码发给认证服务器,向后者请求令牌。
  (C)认证服务器确认无误后,向客户端提供访问令牌。

客户端模式

  其实不存在授权问题,客户端自己向授权服务器拿令牌。
  (A)客户端向认证服务器进行身份认证,并要求一个访问令牌。
  (B)认证服务器确认无误后,向客户端提供访问令牌。

为何要加个授权码呢?
为了避免把token透露给浏览器,最好是透明的,我们不希望处理到这个token暴露在浏览器里面,每次重定向都会经过浏览器的。因此如果直接返回token的话就会产生问题。因此我们一般使用授权码的方式。

redis 数据类型实现机制

发表于 2018-09-27

redis 对象信息

struct RedisObject {
int4 type; // 4bits 什么数据类型,比如字符串,hash,set之类的
int4 encoding; // 4bits 比如字符串用什么方式存储,比如emb,raw.
int24 lru; // 24bits 前文说过关于内存不够是采用的lru算法,这个是保存的是上一次使用的时间戳。
int32 refcount; // 4bytes 这个是对象的引用计数,当这个计数为0的时候,这个对象就要被回收。
void *ptr; // 8bytes,64-bit system 这个保存的实际上对象的存储位置。
} robj;

String

  其实现是一个带长度信息的字节数组

struct SDS<T> {
T capacity; // 数组容量
T len; // 数组长度
byte flags; // 特殊标识位,不理睬它
byte[] content; // 数组内容
}

  很类似于java里面ArrayList的实现,一个动态数组。
  redis字符串有2种存储方式,一个是长度小于44时用emb方式存储,当长度大于44时采用raw方式存储。
  为何?
  前面说了,每个对象头的数据结构大约是16字节,字符串自身数据结构是>=3字节,字符串以\0结尾,一个字节,因此长度为20字节,内存分配器分配内存都是2的幂次,为了存储redis字符串,起码要32,稍微长点就是64,而redis认为大于64字节就是大字符,而64-20=44,因此当数据量小的时候采用emb,方式,就是对象头和sds放在一起,分配一次内存即可,当大约44时就采用raw模式,对象头和sds分开,分配2次内存即可。

字典内部实现

  字典这个数据结构在redis里面用处很频繁,hash结构,所有的redis里面的key和value,带过期时间的key集合,zset里面value和score的集合

struct RedisDb {
dict* dict; // all keys  key=>value
dict* expires; // all expired keys key=>long(timestamp)

}

struct zset {
    dict *dict; // all values  value=>score
    zskiplist *zsl;
}

  dict书籍结构包含2个hashtable,因为redis的扩容时渐进的。因此同时存在2个hashtable,这个迁移主要来自客户端的hset/hdel指令等,当然还有定时任务来配合。

压缩列表

  前面说过了,当hashmap,zset数据量小的时候回采用ziplist,这个压缩列表来存储,将2维变成一维。

struct ziplist<T> {
int32 zlbytes; // 整个压缩列表占用字节数
int32 zltail_offset; // 最后一个元素距离压缩列表起始位置的偏移量,用于快速定位到最后一个节点
int16 zllength; // 元素个数
T[] entries; // 元素内容列表,挨个挨个紧凑存储
int8 zlend; // 标志压缩列表的结束,值恒为 0xFF
}

struct entry {
int<var> prevlen; // 前一个 entry 的字节长度
int<var> encoding; // 元素类型编码
optional byte[] content; // 元素内容
}

  压缩列表为了支持双向遍历,所以才会有 ztail_offset 这个字段,用来快速定位到最后一个元素,然后倒着遍历。

  当 set 集合容纳的元素都是整数并且元素个数较小时,Redis 会使用 intset 来存储结合元素。intset 是紧凑的数组结构,同时支持 16 位、32 位和 64 位整数

struct intset<T> {
int32 encoding; // 决定整数位宽是 16 位、32 位还是 64 位
int32 length; // 元素个数
int<T> contents; // 整数数组,可以是 16 位、32 位和 64 位
}

快速列表

  quicklist 是 ziplist 和 linkedlist 的混合体,它将 linkedlist 按段切分,每一段使用 ziplist 来紧凑存储,多个 ziplist 之间使用双向指针串接起来。

struct ziplist {
...
}
struct ziplist_compressed {
int32 size;
byte[] compressed_data;
}
struct quicklistNode {
quicklistNode* prev;
quicklistNode* next;
ziplist* zl; // 指向压缩列表
int32 size; // ziplist 的字节总数
int16 count; // ziplist 中的元素数量
int2 encoding; // 存储形式 2bit,原生字节数组还是 LZF 压缩存储

}
struct quicklist {
quicklistNode* head;
quicklistNode* tail;
long count; // 元素总数
int nodes; // ziplist 节点的个数
int compressDepth; // LZF 算法压缩深度
...
}

  quicklist 内部默认单个 ziplist 长度为 8k 字节,超出了这个字节数,就会新起一个 ziplist。ziplist 的长度由配置参数list-max-ziplist-size决定。

zset——跳跃表

  个人觉得跳跃表的查找方式类似于B+树。

struct zslnode {
    string value;
    double score;
    zslnode*[] forwards;  // 多层连接指针
    zslnode* backward;  // 回溯指针
}

struct zsl {
    zslnode* header; // 跳跃列表头指针
    int maxLevel; // 跳跃列表当前的最高层
    map<string, zslnode*> ht; // hash 结构的所有键值对
}

  每个节点 之间使用指针串起来形成了双向链表结构,它们是 有序 排列的,从小到大。不同的 节点 层高可能不一样,层数越高的 节点 越少。同一层的 节点 会使用指针串起来。每一个层元素的遍历都是从 节点header 出发。

紧凑列表

struct listpack<T> {
int32 total_bytes; // 占用的总字节数
int16 size; // 元素个数
T[] entries; // 紧凑排列的元素列表
int8 end; // 同 zlend 一样,恒为 0xFF
}


struct lpentry {
int<var> encoding;
optional byte[] content;
int<var> length;//本元素大小
}

  首先这个 listpack 跟 ziplist 的结构几乎一摸一样,只是少了一个zltail_offset字段。ziplist 通过这个字段来定位出最后一个元素的位置,用于逆序遍历。不过 listpack 可以通过其它方式来定位出最后一个元素的位置(这个位置可以通过total_bytes字段和最后一个元素的长度字段计算出来。首先通过total_bytes找到最后的标记位end,占一个字符,去除这个字符,前面就是最后一个元素,而元素length放在了后面,那end的前面就是entry的length,length又是通过固定编码方式存储,要读取出来并不难),所以zltail_offset字段就省掉了。

基数树

  你可以将一本英语字典看成一棵 radix tree,它所有的单词都是按照字典序进行排列,每个词汇都会附带一个解释,这个解释就是 key 对应的 value。有了这棵树,你就可以快速地检索单词,还可以查询以某个前缀开头的单词有哪些。你也可以将公安局的人员档案信息看成一棵 radix tree,它的 key 是每个人的身份证号,value 是这个人的履历。因为身份证号的编码的前缀是按照地区进行一级一级划分的,这点和单词非常类似。有了这棵树,你就可以快速地定位出人员档案,还可以快速查询出某个小片区都有哪些人。

  Rax 被用在 Redis Stream 结构里面用于存储消息队列,在 Stream 里面消息 ID 的前缀是时间戳 + 序号,这样的消息可以理解为时间序列消息。使用 Rax 结构进行存储就可以快速地根据消息 ID 定位到具体的消息,然后继续遍历指定消息之后的所有消息。

  Rax 被用在 Redis Cluster 中用来记录槽位和key的对应关系,这个对应关系的变量名成叫着slots_to_keys。这个raxNode的key是由槽位编号hashslot和key组合而成的。我们知道cluster的槽位数量是16384,它需要2个字节来表示,所以rax节点里存的key就是2个字节的hashslot和对象key拼接起来的字符串。

redis GeoHash 和scan

发表于 2018-09-26

GeoHash的产生

  我们用x,y坐标轴表示你的位置,当我们查看附近的人这个功能的时候,就要对一定范围的人查询,如果放在数据里面,这么算的。

select id from positions where x0-r < x < x0+r and y0-r < y < y0+r

  但是当数据量大,并发量大的时候就会存在性能瓶颈。
  
  业界有个算法将经纬度2维坐标转换为一维整数,当我们想要计算附近的人的时候,我们只要计算这个线上的点就好了。编码后,二维坐标变成了整数,但是同时整数也可以重新还原坐标。GeoHash算法会对整数进行编码,生成一个字符串,然后放到zset里面。这个字符串作为score,value是元素的key.

  geoadd company 116.48105 39.996794 juejin
  我们用这个geoadd来加入到zset里面,redis会按照上面的算法将其2个数字转换为一个score,然后存进去。
   geodist company juejin ireader km
  这个是计算2个地点的距离。
georadiusbymember company ireader 20 km count 3 asc
  这个是查询company这个zset里面 ireader,20km范围内最多3个元素的正排序,包括自身。

scan

  keys codess*
  找出所有以codess开头的key。但是有缺点,数据量太大,会让服务器卡顿,一长串,数据太多。不好看,不好处理。时间复杂度为o(n).
  后面多了个scan:
  1多了limit参数,可控了
  2通过游标分步进行进行。不阻塞线程。
  3.返回的结果可能会有重复,需要客户端去重复,这点非常重要;
  4.遍历的过程中如果有数据修改,改动后的数据能不能遍历到是不确定的;
  5.单次返回的结果是空的并不意味着遍历结束,而要看返回的游标值是否为零;
  scan 参数提供了三个参数,第一个是 cursor 整数值,第二个是 key 的正则模式,第三个是遍历的 limit hint。第一次遍历时,cursor 值为 0,然后将返回结果中第一个整数值作为下一次遍历的 cursor。一直遍历到返回的 cursor 值为 0 时结束。
 limit 不是限定返回结果的数量,而是限定服务器单次遍历的字典槽位数量(约等于)。如果将 limit 设置为 10,你会发现返回结果是空的,但是游标值不为零,意味着遍历还没结束。

key 字典

  redis里面所有的key存在一个很大的字典里面,类似于hashmap,sacn指令返回的游标就是第一维数组的位置索引,我们将这个位置索引称为槽 (slot)。如果不考虑字典的扩容缩容,直接按数组下标挨个遍历就行了。limit 参数就表示需要遍历的槽位数,之所以返回的结果可能多可能少,是因为不是所有的槽位上都会挂接链表,有些槽位可能是空的,还有些槽位上挂接的链表上的元素可能会有多个。每一次遍历都会将 limit 数量的槽位上挂接的所有链表元素进行模式匹配过滤后,一次性返回给客户端。

redis内存策略 LRU,LFU以及安全问题

发表于 2018-09-26

内存策略

  大家知道现代计算机都有虚拟内存这个概念,当redis内存超出物理内存限制的时候,内存数据就会和磁盘发生频繁交换。严重影响性能。我们一般通过配置参数maxmemory来限定内存大小。当内存要超出maxmemory时以下策略来使用。

  noeviction 不会继续服务写请求 (DEL 请求可以继续服务),读请求可以继续进行。这样可以保证不会丢失数据,但是会让线上的业务不能持续进行。这是默认的淘汰策略。

  volatile-lru 尝试淘汰设置了过期时间的 key,最少使用的 key 优先被淘汰。没有设置过期时间的 key 不会被淘汰,这样可以保证需要持久化的数据不会突然丢失。

  volatile-ttl 跟上面一样,除了淘汰的策略不是 LRU,而是 key 的剩余寿命 ttl 的值,ttl 越小越优先被淘汰。

  volatile-random 跟上面一样,不过淘汰的 key 是过期 key 集合中随机的 key。

  allkeys-lru 区别于 volatile-lru,这个策略要淘汰的 key 对象是全体的 key 集合,而不只是过期的 key 集合。这意味着没有设置过期时间的 key 也会被淘汰。

  allkeys-random 跟上面一样,不过淘汰的策略是随机的 key。

  实现 LRU 算法除了需要 key/value 字典外,还需要附加一个链表,链表中的元素按照一定的顺序进行排列。当空间满的时候,会踢掉链表尾部的元素。当字典的某个元素被访问时,它在链表中的位置会被移动到表头。所以链表的元素排列顺序就是元素最近被访问的时间顺序。
  redis为了实现类lru算法,采用了它给每个 key 增加了一个额外的小字段,这个字段的长度是 24 个 bit,也就是最后一次被访问的时间戳。Redis 执行写操作时,发现内存超出 maxmemory,就会执行一次 LRU 淘汰算法。这个算法也很简单,就是随机采样出 5(可以配置) 个 key,然后淘汰掉最旧的 key,如果淘汰后内存还是超出 maxmemory,那就继续随机采样淘汰,直到内存低于 maxmemory 为止。
  在 LRU 模式下,lru 字段存储的是 Redis 时钟server.lruclock,Redis 时钟是一个 24bit 的整数,默认是 Unix 时间戳对 2^24 取模的结果,大约 97 天清零一次。当某个 key 被访问一次,它的对象头的 lru 字段值就会被更新为server.lruclock。

  默认 Redis 时钟值每毫秒更新一次,在定时任务serverCron里主动设置。Redis 的很多定时任务都是在serverCron里面完成的,比如大型 hash 表的渐进式迁移、过期 key 的主动淘汰、触发 bgsave、bgaofrewrite 等等。

  如果server.lruclock没有折返 (对 2^24 取模),它就是一直递增的,这意味着对象的 LRU 字段不会超过server.lruclock的值。如果超过了,说明server.lruclock折返了。通过这个逻辑就可以精准计算出对象多长时间没有被访问——对象的空闲时间。

  LFU策略:表示按最近的访问频率进行淘汰,它比 LRU 更加精准地表示了一个 key 被访问的热度。redis4.0后出来的。
  在 LFU 模式下,lru 字段 24 个 bit 用来存储两个值,分别是ldt(last decrement time)和logc(logistic counter)。
  idt:16bit存储上次idt的跟新时间。内存回收时,自己被随机取到的话,自己更新的时间。
  logc:对象调用次数,存储的是对数次。每次访问更新。
  dt 更新的同时也会一同衰减 logc 的值,衰减也有特定的算法。它将现有的 logc 值减去对象的空闲时间 (分钟数) 除以一个衰减系数,默认这个衰减系数lfu_decay_time是 1。如果这个值大于 1,那么就会衰减的比较慢。如果它等于零,那就表示不衰减,它是可以通过配置参数lfu-decay-time进行配置。

  为何不从系统里面取时间?
  一次系统调用太耗时了。太多更耗时。

安全

  redis有些指令是很危险的,比如flushdb 和 flushall 会让 Redis 的所有数据全部清空。Redis 在配置文件中提供了 rename-command 指令用于将某些危险的指令修改成特别的名称,用来避免人为误操作。比如在配置文件的 security 块增加下面的内容:

rename-command keys abckeysabc

  如果还想执行 keys 方法,那就不能直接敲 keys 命令了,而需要键入abckeysabc。 如果想完全封杀某条指令,可以将指令 rename 成空串,就无法通过任何字符串指令来执行这条指令了。
  我们可以修改端口号,不用默认端口,避免被入侵。
  客户端和服务器的访问时直接暴露在公网上传输,为了安全,我们可以使用ssl代理对通信通道进行加密。

spiped原理

  程序通过公网访问redis时,数据暴露在外面,我们可以用ssl对数据加密。使其安全传输,spiped就是其中一个代理,其原理是在2端各起一个进程,来对消息进行加密和解密。

redis过期删除策略

发表于 2018-09-25

惰性删除

  redis将设置了过期时间的key放到一个独立的字典里面,以后定时遍历这个字典来删除到期的key,同时采用惰性原则,当客户端访问这个key的时候,redis会对其进行检查,如果过期了,那么立即删除。

定时策略

  redis默认每秒10次扫描过期字典的key,不是遍历,而是一种贪心策略。
  1.从过期字典随机20个key。
  2.删除这20个key里面过期的key。
  3.如果过期的key>1/4,那就重复1.
  同时为了避免循环过度,造成线程卡死,算法还设置了一个时间上限,默认不超过25ms。
  
  不要用大量key同一时间过期,会导致系统卡顿。因为卡顿是很多小卡顿积累出来的。

从库的删除策略

  从库不会进行过期扫描,从库的处理是被动的。主库key到期后,会在aof文件里面增加一个del指令,同步到所以的从库。

真删除 del

  redis的单线程是指接受客户端请求的时候是单线程,基于多路io复用,但是redis里面还是有多个异步子线程来进行一些耗时操作的。
  比如:del删除指令会直接释放对象的内存,对于小对象来说,无所谓,秒删,但是对于大的数据,hash,那么就会造成单线程卡顿,redis提供了一个叫unlink的指令,对删除进行懒操作处理。对给后台线程异步回收内存。同时使用这个指令后,主线程就无法访问到了。
  主线程执行这个指令后,就会将这个key的内存回收操作包装为一个任务,塞进异步任务队列,后台线程就会从这个异步队列里面取任务,然后执行。
  Aof sync 操作中一秒一次增量同步,需要频繁磁盘io,耗时,于是也有一个子线程来异步后台处理

redis -stream

发表于 2018-09-20

诞生

  Redis5.0出来的,新的数据结构 Stream 支持多播的可持久化的消息队列。其设计借鉴了kafka的设计。
  这个存储类型是一个数据链表,将所有加入的消息都串起来。每个消息都有唯一的id和对应的内容,消息是持久化的,redis重启后,内容还在,其持久化方式,类似于rdb+aof。
  每个stream都有唯一的一个名称就是其key,我们首次使用xadd指令追加消息时,自动创建,同时每个strean可以同时挂多个消费组,每个消费组都有游标last_delivered_id在Stream数组之上向前移动,表示当前消费组已经消费到哪条消息了,每个消费组都有一个stream内唯一的名称,消费组不会自动创建,需要单独的指令xgroup create进行创建,需要制定从这个stream的某个消息id,表示从这个stream开始消费,这个id用来初始化last_delivered_id变量。
  每个消费组的状态都是独立的。相互不影响,也就是说同一个stream内部的消息会被每个消费组消费到。
  每个消费组可以挂起多个消费者,这些消费者是竞争关系的,一个消息被一个消费者消化了,那就无法被另一个消化。每个消费者组内是唯一的。
  消费者内部有个状态变量pending_ids,它记录了客户端读取的消息,但是没有ack的,如果客户端发了ack,那么这个消息id就会被去掉,它来表示消息是否被至少消费一次。
  消息id是timestampInMillis-sequence,当前时间戳,的第几条消息,同时也可以客户端自己指定,但是形式必须是整数-整数,消息内容是键值对。

操作

  xadd:追加消息。
  xadd codehole * name laoqian age 30
  xdel:删除消息这里的删除仅仅是设置了标志位,不影响消息总长度。
  xrange:获取消息队列,自动过滤被删除消息。
  xrange codehole - +
  xlen:消息长度。
  xlen codehole
  del:删除stream。

独立消费

  我们可以不建立消费组的情况下对stream独立消费,其有一个指令叫做xread,可以将stream当做普通的消息队列(list)来使用,使用xread的时候我们可以忽略消费组的存在,好比stream就是一个普通的列表(list)。xread count 2 streams codehole 0-0表示从头读2个数据,没有数据就一直阻塞。但是我们可以用block来设置阻塞时间,0表示永久阻塞。1000表示1秒。 xread block 1000 count 1 streams codehole $。

创建消费组

  Stream通过xgroup createn 创建消费组。需要传递起始消息id作为参数来初始化last_delivered_id。
  xgroup create codehole cg1 0-0:从头开始消费。
  xgroup create codehole cg2 $:从尾部开始消费,只接受新消息。
   xinfo stream codehole:获取stream信息。

消费

  Stream指令xreadgroup可以进行消费组的组内消费,需要消费组名称,消费者名称,起始消息id,读取到新消息后,对应的消息id就会进入消费者的PEL结构里面,等待ack,来删除。

   xreadgroup GROUP cg1 c1 count 1 streams codehole >
命令 消费组 消费者 读几个 那个stream 表示从这个之后读取

限制长度

  stream消息要是太多怎么办,一个链表太长了,会导致性能下降很多,而且,删除也是逻辑删除,这边我们可以在创建这个stream的时候设置默认长度,新的覆盖旧的。
  xadd codehole maxlen 3 * name xiaorui age 1 长度为3

pel如何避免消息丢失

  当客户端突然断掉了来自服务端的消息,消息丢失了,但是pel里面已经保存了发消息的id,待客户端连上了,我们可以读取pel里面的消息列表,来保证消息不丢失。不过此时 xreadgroup 的起始消息 ID 不能为参数>,而必须是任意有效的消息 ID,一般将参数设为 0-0,表示读取所有的 PEL 消息以及自last_delivered_id之后的新消息。

stream的高可用

  这个的高可用是建立在主从复制的基础上的,他和其他数据的复制机制没区别。

分区 Partition

  Redis不支持分区功能,如果要分区,那就配置多个stream,然后客户端根据一定策略生成消息到不同stream。   

mysql 分库分表

发表于 2018-09-17

为何需要分库分表

​ 根据阿里云上的测试,8/32 支持 1300事务数 25000并发请求数;16/64 2000事务数,40000并发请求数,mysql并发请求数不高。

方案1:数据量不大,读压力大,写压力不大的情况下加缓存。或者读写分离。

方案2:数据量大,并发读写操作超过了单数据处理能力。这个时候需要分库分表了。

垂直切分

  将表按照功能模块、关系密切程度划分出来,部署到不同的库上。也就是分库。适用于表多的情况。

水平切分

  当一个表中的数据量过大时,我们可以把该表的数据按照某种规则,例如userID散列,进行划分,然后存储到多个结构相同的表,这就是分表。

切分的选择

  应该使用哪一种方式来实施数据库分库分表,这要看数据库中数据量的瓶颈所在,并综合项目的业务类型进行考虑。

  如果数据库是因为表太多而造成海量数据,并且项目的各项业务逻辑划分清晰、低耦合,那么规则简单明了、容易实施的垂直切分必是首选。而如果数据库中的表并不多,但单表的数据量很大、或数据热度很高,这种情况之下就应该选择水平切分,水平切分比垂直切分要复杂一些,它将原本逻辑上属于一体的数据进行了物理分割,除了在分割时要对分割的粒度做好评估,考虑数据平均和负载平均,后期也将对项目人员及应用程序产生额外的数据管理负担。

  在现实项目中,往往是这两种情况兼而有之,这就需要做出权衡,甚至既需要垂直切分,又需要水平切分。我们的游戏项目便综合使用了垂直与水平切分,我们首先对数据库进行垂直切分,然后,再针对一部分表,通常是用户数据表,进行水平切分。

问题挑战

  在分片之后的数据库中并不一定能够正确运行。
  跨库事务也是分布式的数据库集群要面对的棘手事情。 合理采用分表,可以在降低单表数据量的情况下,尽量使用本地事务,善于使用同库不同表可有效避免分布式事务带来的麻烦。 在不能避免跨库事务的场景,有些业务仍然需要保持事务的一致性。 而基于XA的分布式事务由于在并发度高的场景中性能无法满足需要,并未被互联网巨头大规模使用,他们大多采用最终一致性的柔性事务代替强一致事务。

分表的2个方式

  1.事先预估好了,直接根据一定规则把表划分为多个。比如用户表。事先建n个这样的表,user1,user2,user3。然后根据用户的ID来判断这个用户的聊天信息放到哪张表里面,你可以用hash的方式来获得,可以用求余的方式来获得,方法很多。
  2.使用merge的方式建立映射。但是要求很多不能是innodb存储引擎。一般不使用。

实践使用sharing-jdbc

redis主从同步和集群

发表于 2018-09-17

什么是主从同步

  主从分布是部署多个redis实例,一主多从,当主实例挂了,那么我们就可以用从实例替换上去,增加了系统的稳定性。

CAP理论

  C:一致性:分布式的系统中同一时刻有一样的值。
  A:可用性:集群故障之后依旧可以相应客户端的读写请求。
  P:分区容错性:系统如果不能在时限内达成数据一致性,就意味着发生了分区的情况,必须就当前操作在C和A之间做出选择。
  而分布式系统往往分布在不同的机器上面。这意味着必然有网络断开的风险,这个网络断开的场景的专业词汇叫网络分区。
  当网络之间无法通信时,节点数据不在统一,那么一致性不满足了,或者我们暂停服务,那么可用性不满足了。
  也就是说当网络分区时,一致性和可用性无法2全。

redis的最终一致性

  redis的主从数据是异步同步的,所以redis系统不满足一致性要求。redis的主节点修改数据后,立即返回,因此就算主从网络断开,主节点依旧可以提供服务。满足可用性,当网络恢复了,从节点会尽可能的同步数据。

增量同步

  这个同步方式是主节点会把对自己状态修改的指令记录在内存buffer中,redis的复制内存buffer是一个定长的环形数组,如果数组满了,将会覆盖这个数组。因此如果长时间无法同步,那么指令可能会被覆盖。

快照同步

  需要将主库的数据全部快照到磁盘文件,然后将快照文件传给子节点。从节点立即进行一次全量加载,加载之前需要将数据清空,加载后,在进行增量同步。(个人理解是和持久化一样的策略,进行快照时,新的操作用增量),但是存在一种情况,就是在快照和同步的时候,增量缓存已经被覆盖,那么只增在进行快照,然后buffer又被覆盖,进入了死循环。因此buffer大小必须设置好。

无盘复制

  快照时会很严重的io操作,对系统很大的影响,我们可以通过服务器直接将套接字将快照内容发到从节点,生成快照是一个遍历的过程,主节点一遍遍历内存一遍序列化内容发送到从节点,从节点接受后也是先缓存到磁盘,然后加载到从节点里面。
  我们可以用wait指令强行同步复制。wait 1 1,1就是一个从库,0就是最多等待时间。如果时间=0,那么将将无限制一致等待。

sentinel 自动化切换

  前面介绍了主从同步的策略实现以及原理。但是主节点出现故障,还是要手动去切换到从节点,为了避免这个问题,redis官方提供了一个方案-redis sentinel 来处理。可以在主节点挂了之后自动切换到从节点。
  我们一般用3~5个sentinel作为集群,来监察redis的集群。客户端访问redis集群的时候,一般先去访问sentinel,从其中获取主节点地址,然后再去访问redis,当redis主节点挂了之后,客户端访问失败后,再次访问sentinel,他会选一个最优的从节点作为主节点,并把地址返回给客户端。而sentinel会持续监控这个坏掉的主节点,等其回复了之后将其作为从节点。
  主节点断掉了,那么以为消息的丢失,因为redis的消息同和和zookeeper不一样是异步,如果延迟过大,可能造成很大的影响。

min-slaves-to-write 1
min-slaves-max-lag 10

  Sentinel 采用上面的命令来尽量减少丢失,第一个参数是,起码必须有1个正在正常复制中,第二个参数是如果超过10秒没有复制,那么就是不正常复制了。

codis 方案

  大数据量下单个redis实例是明显不够的,原因如下:
  1.导致内存过大,这样主从复制的时候全量同步时间过长,其次持久化时间也太长,重启消耗大量时间资源。
  2.云环境下,单个实例内存是收到限制的。
  3.redis是基于多路io复用的单线程,对cpu利用率不高。
  因此redis集群应运而生,多个redis实例可以有效提高cpu使用率,而codis是一个非常好的方案,它是一个代理中间件和redis一样使用redis的协议来对外提供服务。当客户端向codis发送指令时,codis负责将指令转发到后面的redis实例完成,起到一个负载均衡,代理的作用。codis上连接的所有redis实例构成了一个集群。codis对客户端是透明的,客户端就像直接使用redis实例一样。codis是无状态的,我们可以同时启动多个实例,来增加其容错性,以及提高并发量。
  那么问题来了,一个key到底对应哪个redis实例呢?
  codis这样玩的,他对每个key做一个运算,然后对1024取模得到一个值,codis在内存维护的一个映射关系。就是redis实例和1024的对应关系,然后这个值通过这个映射关系,找打对应的redis实例。
  那么问题又来了,我们如何对codis同步数据,同步映射关系,这边采用的是使用zookeeper来持久化映射关系,同时codis提供了一个dashboard来观察修改槽位关系,当槽位关系变化时,codis会监听到变化,从而同步其关系。
  缺点:key分布式了,所以不再支持事务,其次是动态扩容时,key的迁移消耗资源太大。

cluster(亲儿子)

  cluster是redis官方推出的方案,采用去中心化的方案来集群,多个节点通过一个二进制协议交换集群信息。redis将数据的key分为16384 的 slots,其槽点的信息存在每一个节点里面,当客户端获取数据时,先获取到整个槽点的信息,且将其缓存下来。RedisCluster 的每个节点会将集群的配置信息持久化到配置文件中,所以必须确保配置文件是可写的,而且尽量不要依靠人工修改配置文件。具体配置看详细即可。欧了

1…567…13

skydh

skydh

126 日志
© 2020 skydh
本站访客数:
由 Hexo 强力驱动
|
主题 — NexT.Pisces v5.1.3