天道酬勤

天下事有难易乎?为之,则难者亦易矣;不为,则易者亦难矣。

0%

(1) 传输方式(Transport)作用

传输方式(Transport)作为rpc框架接收报文的入口,提供各种底层实现如socket创建、读写、接收连接等。
同时实现各种复写传输层包括http、framed、buffered、压缩传输等。

(1.1) 支持的传输方式

thrift支持多种传输方式

传输方式 特点
TSocket 阻塞型 socket,用于客户端,采用系统函数 read 和 write 进行读写数据。
TServerSocket 非阻塞型 socket,用于服务器端,accecpt 到的 socket 类型都是 TSocket(即阻塞型 socket)。
TBufferedTransport
TFramedTransport
TMemoryBuffer
TFileTransport
TFDTransport
TSimpleFileTransport
TZlibTransport
TSSLSocket
TSSLServerSocket
阅读全文 »

Thrift是一个轻量、支持多语言、可扩展、高性能的远程服务调用框架。
提供了数据传输、序列化、应用层处理的清晰抽象。

The Apache Thrift software framework, for scalable cross-language services development, combines a software stack with a code generation engine to build services that work efficiently and seamlessly between C++, Java, Python, PHP, Ruby, Erlang, Perl, Haskell, C#, Cocoa, JavaScript, Node.js, Smalltalk, OCaml and Delphi and other languages.

(1) thrift架构分层

thrift-layers

分层 职责
服务调用层 (客户端/服务端) 客户端、服务端调用。
协议层 消息解析
传输层包装 功能增强,实现各种复写传输层包括http、framed、buffered、压缩传输等
低级传输层 靠近网络层、作为rpc框架接收报文的入口,提供各种底层实现如socket创建、读写、接收连接等。
语言层 thrift采用接口描述语言定义并创建服务,支持可扩展的跨语言服务开发
操作系统层 由编程语言提供各种操作系统的支持
阅读全文 »

 协议大家平时都会遇到,只是没有特别注意。
 像平时大家阅读文章的时候都是从上到下、从左往右 按行阅读,这可以看做一种阅读协议。 ( 备注: 古人在竹简上写的文字则是从上往下、从右往左 按列阅读。)
 更详细的规则比如:作文的第一行是标题,段首要空两格的是一个自然段,遇到一个句号是一句话。

详细的标点符号用法(通信协议)参考教育部的规范 标点符号用法 - 教育部

在计算机远程方法调用时,传输的都是二进制的01,调用方(写数据)和被调用方(读数据)怎么约定通信协议的?

(1) 协议(TProtocol)的作用

协议的作用就类似于文字中的符号,作为应用拆解请求消息的边界,保证二进制数据经过网络传输后,还能被正确地还原语义。

具体点就是从二进制数据中解析出协议版本方法名消息类型序列Id序列化方式消息长度协议体等内容。

thrift-protocol-message

阅读全文 »

Kafka是一个消息队列。被大家用在分布式消息队列或者流数据处理场景。
有几个问题,大家不知道思考过没有?
1、Kafka有哪些组件,作用分别是什么,为什么这么设计?
2、Kafka是怎么保证高性能、高并发、高可用的呢?

阅读全文 »

redis里有 redis.log(程序运行日志)、slowlog(慢查询日志) 等

参考资料

[1] slowlog

  在原来coding pages服务和github pages服务都可以免费试用时,在域名解析时配置 国内访问coding pages部署的博客静态文件,国外访问github pages部署的静态文件。国内国外访问速度都基本都在200ms内。

网站网络设计

  自从 coding.net 停止免费的pages服务后,只有github pages可以免费试用,国内访问博客只能访问github pages服务,导致访问速度很慢。被人吐槽了一次,所以想优化一下。

优化思路比较简单,
1、国内ip访问国内的博客服务 比如gitee pages服务
2、通过缓存加快访问速度 比如CDN缓存

阅读全文 »

整体流程

redis命令执行流程图

acceptTcpHandler -> anetTcpAccept -> acceptCommonHandler
acceptCommonHandler-> createClient -> readQueryFromClient
阅读全文 »

在Feed流的推荐信息里,一般都会有点赞数收藏数评论数转发数 的功能,假设让你设计这套计数系统,你会怎么设计?

(1) 背景

以微信、抖音、快手、微博 为例,推荐的朋友圈、视频或文章里会有点赞数收藏数评论数转发数,假如要设计,一般设计成一个简单的kv查询。

考虑因素如下:
1、预估用户 14亿
2、预估活跃用户数 0.01 ~ 6亿。
3、假设每个用户每天发1~3个信息/动态, 每天大概有 0.01亿 ~ 18亿条信息(动态)。 每条信息(动态)上都有点赞数收藏数评论数转发数

备注: 一条信息(动态)代表一个朋友圈动态或一个抖音视频或一篇微博动态。


(2) 信息Id设计

如果只考虑用户信息的ID查询,只要保证动态Id唯一即可;考虑到要按照用户维度去展示用户的信息列表,信息ID里最好包含用户ID,这样方便查询某个用户发布的所有的信息(动态)。
类似于电商场景查询某个用户的所有订单。

信息有个唯一表示,用rec_id表示
根据推荐信息唯一标识rec_id获取点赞数收藏数评论数转发数 是一个简单查询。

(2.1) 用数字表示信息ID

int类型占用4字节,32位,最多可以表示 2^32个数字,考虑上符号,以及0,正数最多有 2^31-1 = 21 4748 3647 个,大概21亿多。

long类型占用8字节,64位,最多可以表示 2^64个数字,考虑到id都是正数,而且一般不用0,所以最多有 2^63-1 = 922 3372 0368 5477 5807 个,大概922亿亿个。

可以用8字节的Long类型表示信息ID(msg_id),msg_id的设计可以参考雪花算法的思想, 64位可以按照 符号位(1位)、城市(6位)、用户(8-10位)、时间戳()、随机字符()等来设计。


(3) 计数器设计

(3.1) 使用数据库做计数器

在刚开始阶段,用户较少并且用户发布的信息/动态较少时,可以使用数据库来存储。

CREATE TABLE msg_count (
`id` bigint(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键id',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
`msg_id` bigint(11) NOT NULL COMMENT '用户id',
`like_num` int(11) NOT NULL DEFAULT '0' COMMENT '点赞数',
`comments_num` int(11) NOT NULL DEFAULT '0' COMMENT '评论数',
`favor_count` int(11) NOT NULL DEFAULT '0' COMMENT '收藏数',
`forwards_count` int(11) NOT NULL DEFAULT '0' COMMENT '转发数',
PRIMARY KEY (`id`),
UNIQUE KEY `uq_idx_msg_id` (`msg_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='信息(动态)计数表';

按信息ID(msg_id)取模,把数据拆分到N个表(N是2的幂次方)。
如果要扩容,把N个表拆成 N * 2^x,迁移数据时只需要把 1个表拆到 2^x个表即可。

访问量太大怎么办?
使用缓存+数据库,先查缓存,缓存查不到再查数据库。

问题:
1、空数据也得Cache(有一半以上的微博是没有转发也没有评论的,但是依然有大量的访问会查询数据库);
2、Cache频繁失效(由于计数更新非常快,所以经常需要失效Cache再重新缓存,还会导致数据不一致);

更好的硬件解决。 上FusionIO + HandleSocket + 大内存 优化。

总的来说,MySQL分库分表 + Cache加速的方案 对于数据规模和访问量不是特别巨大的情况下,是非常不错的解决方案。


(3.2) 使用Redis做缓存

Redis作为一个简单的内存数据库,提供了多种数据类型,可以使用string类型的 incr 来计数。
具体命令参考 https://redis.io/commands/incr/

简单的来估算一下数据存储量,按照Redis 6.0.0的实现,在64位系统,指针为8字节来估算

假设 key 为8字节,value为 4字节,通过incr存储的话
key value 会存储在 dictEntry里,详细的结构: redisServer -> db0 -> dict -> ht[0] -> dictht -> dictEntry

redis索引模型

放到db->dict->ht[0]->table中存储dictEntry的指针,需要8个字节;
存储一个kv首先需要一个dictEntry,dictEntry里面有3个指针,每个指针占用8字节,占用 3*8字节=24字节
key的指针指向一个RedisObject(16字节),RedisObject的ptr又指向一个SDS,一个Key(msg_id 8字节)通过sds存储,需要 8+

(4) 系统安全问题

(4.1) 系统怎么应对爬虫?

网关+风控处理

参考资料

[1] [WeiDesign]微博计数器的设计(上)
[2] [WeiDesign]微博计数器的设计(下)

  同事在实现一个弹窗需求时遇到一个问题,找大家讨论方案,产品的需求是小程序首页各种弹窗太多了,用户一进来需要点多个弹窗,要求服务端限制在首页一个用户一天只能弹一次。当时考虑了数据库和缓存,从实现简易程度、扩展性、速度综合考虑最后选了redis缓存。

Redis数据类型

五种数据形式的底层实现
string:简单动态字符串
list:双向链表,压缩列表
hash:压缩列表,哈希表
Sorted Set:压缩列表,跳表
set:哈希表,整数数组

(3) redis常用数据类型/对象

上面介绍了 6 种底层数据结构,Redis 并没有直接使用这些数据结构来实现键值数据库,而是基于这些数据结构创建了一个对象系统,这个系统包含字符串对象、列表对象、哈希对象、集合对象和有序集合这五种类型的对象,每个对象都使用到了至少一种前边讲的底层数据结构。

redis常用的数据对象有以下几种

/* The actual Redis Object */
#define OBJ_STRING 0    /* String object. */
#define OBJ_LIST 1      /* List object. */
#define OBJ_SET 2       /* Set object. */
#define OBJ_ZSET 3      /* Sorted set object. */
#define OBJ_HASH 4      /* Hash object. */

#define OBJ_MODULE 5    /* Module object. */
#define OBJ_STREAM 6    /* Stream object. */

(3.1) 字符串对象 OBJ_STRING

(3.2) 列表对象 OBJ_LIST

(3.3) 集合对象 OBJ_SET

(3.4) 有序集合对象 OBJ_ZSET

Sorted Set内部实现数据结构是跳表和压缩列表。
跳表主要服务范围操作,提供O(logN)的复杂度。
zset有个ZSCORE的操作,用于返回单个集合member的分数,它的操作复杂度是O(1),这就是收益于你这看到的hash table。这个hash table保存了集合元素和相应的分数,所以做ZSCORE操作时,直接查这个表就可以,复杂度就降为O(1)了。

(3.5) 哈希对象 OBJ_HASH

(2) redis常用数据结构

/* Objects encoding. Some kind of objects like Strings and Hashes can be
 * internally represented in multiple ways. The 'encoding' field of the object
 * is set to one of this fields for this object. */
#define OBJ_ENCODING_RAW 0     /* Raw representation */
#define OBJ_ENCODING_INT 1     /* Encoded as integer */
#define OBJ_ENCODING_HT 2      /* Encoded as hash table */
#define OBJ_ENCODING_ZIPMAP 3  /* Encoded as zipmap */
#define OBJ_ENCODING_LINKEDLIST 4 /* No longer used: old list encoding. */
#define OBJ_ENCODING_ZIPLIST 5 /* Encoded as ziplist */
#define OBJ_ENCODING_INTSET 6  /* Encoded as intset */
#define OBJ_ENCODING_SKIPLIST 7  /* Encoded as skiplist */
#define OBJ_ENCODING_EMBSTR 8  /* Embedded sds string encoding */
#define OBJ_ENCODING_QUICKLIST 9 /* Encoded as linked list of ziplists */
#define OBJ_ENCODING_STREAM 10 /* Encoded as a radix tree of listpacks */

(2.1) 简单动态字符串SDS

  代码见
  SDS包括 RAW INT EMDSTR

(2.2) 链表

(2.3) 字典

(2.4) 跳跃表

(2.5) 整数集合

(2.6) 压缩列表

思考

Redis为什么会把整数数组和压缩列表作为底层数据结构?

整数数组和压缩列表在查找时间复杂度方面并没有很大的优势,那为什么 Redis 还会把它们作为底层数据结构呢?

1、内存利用率,数组和压缩列表都是非常紧凑的数据结构,它比链表占用的内存要更少。
Redis是内存数据库,大量数据存到内存中,此时需要做尽可能的优化,提高内存的利用率。

2、数组对CPU高速缓存支持更友好,所以Redis在设计时,集合数据元素较少情况下,默认采用内存紧凑排列的方式存储,同时利用CPU高速缓存不会降低访问速度。当数据元素超过设定阈值后,避免查询时间复杂度太高,转为哈希和跳表数据结构存储,保证查询效率。

数组对cpu缓存友好的原因是: cpu预读取一个cache line大小的数据, 数组数据排列紧凑、相同大小空间保存的元素更多, 访问下一个元素时、恰好已经在cpu缓存了. 如果是随机访问、就不能充分利用cpu缓存了, 拿int元素举例: 一个元素4byte, CacheLine 假设64byte, 可以预读取 16个挨着的元素, 如果下次随机访问的元素不在这16个元素里、就需要重新从内存读取了.

(4) 手动测试过程

 1、flushall 清空所有数据
 2、测试用string类型存 key=1000000,value=1
 3、插入100万uid用了55M左右(实际占用操作系统64M)
 4、插入1000万用了587M(实际占用从操作系统622M)

 感觉不太对,key是long类型 8字节,100万*8=8M, 100万key占8M 100万value占8M 多余的55-8-8=39M去哪了?

(4.1) 测试代码

@Slf4j
public class RedisMemoryTest {

    public Jedis jedis = JedisPoolUtil.getJedis();

    /**
     * 1000万 18位用户id
     */
    @Test
    public void testMemory() {
        // 18位用户id
        long start = 123456789012345678L;
        long end = start + 10000000;
        for (long i = 123456789012345678L; i < end; i++) {
            String res = jedis.set("u" + i, "1");
        }
    }

}

(4.2) redis flushall后 info memory信息

# Memory
used_memory:1180064
used_memory_human:1.13M
used_memory_rss:954368
used_memory_rss_human:932.00K
used_memory_peak:1219088
used_memory_peak_human:1.16M
used_memory_peak_perc:96.80%
used_memory_overhead:1132016
used_memory_startup:1079584
used_memory_dataset:48048
used_memory_dataset_perc:47.82%

(4.3) redis 插入100万数据后的info memory信息

 


127.0.0.1:6379> info memory
# Memory
used_memory:57558992
used_memory_human:54.89M
used_memory_rss:66695168
used_memory_rss_human:63.61M
used_memory_peak:58020704
used_memory_peak_human:55.33M
used_memory_peak_perc:99.20%
used_memory_overhead:49520624
used_memory_startup:1079584
used_memory_dataset:8038368
used_memory_dataset_perc:14.23%

(4.4) redis 插入100万数据后的info memory信息

 


# Memory
used_memory:615390352
used_memory_human:586.88M
used_memory_rss:421154816
used_memory_rss_human:401.64M
used_memory_peak:653409248
used_memory_peak_human:623.14M
used_memory_peak_perc:94.18%
used_memory_overhead:535349840
used_memory_startup:1079584
used_memory_dataset:80040512
used_memory_dataset_perc:13.03%

参考资料

[1] redis源码 - github
[2] 十二张图详解Redis的数据结构和对象系统
[3] “万金油”的String,为什么不好用了

  经常会遇到的一个问题是数据库如何保证不丢数据? 同样的假如把Redis当数据库用,如何保证不丢数据?

 MySQL里有 redo log、bin log、undo log,MySQL通过binlog全量备份+增量备份保证数据不丢。通过redo log和bin log保证数据一致性。

 Redis里有没有类似的功能呢?

 Redis包含 rdb logaof log,可以通过RDB全量备份+aof增量备份保证数据几乎不丢。

阅读全文 »