0%

背景介绍

Redis中list数据结构,也就是队列,使用quicklist作为底层存储结构,quicklist本质上是一个双向链表

数据结构

quicklist本身的结构如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* quicklist is a 40 byte struct (on 64-bit systems) describing a quicklist.
* 'count' is the number of total entries.
* 'len' is the number of quicklist nodes.
* 'compress' is: -1 if compression disabled, otherwise it's the number
* of quicklistNodes to leave uncompressed at ends of quicklist.
* 'fill' is the user-requested (or default) fill factor. */
typedef struct quicklist {
quicklistNode *head;
quicklistNode *tail;
unsigned long count; /* total count of all entries in all ziplists */
unsigned long len; /* number of quicklistNodes */
int fill : 16; /* fill factor for individual nodes */
unsigned int compress : 16; /* depth of end nodes not to compress;0=off */
} quicklist;

可以看到有head和tail,方便从首尾两个方向做push和pop操作,quicklistNode结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/* quicklistNode is a 32 byte struct describing a ziplist for a quicklist.
* We use bit fields keep the quicklistNode at 32 bytes.
* count: 16 bits, max 65536 (max zl bytes is 65k, so max count actually < 32k).
* encoding: 2 bits, RAW=1, LZF=2.
* container: 2 bits, NONE=1, ZIPLIST=2.
* recompress: 1 bit, bool, true if node is temporarry decompressed for usage.
* attempted_compress: 1 bit, boolean, used for verifying during testing.
* extra: 12 bits, free for future use; pads out the remainder of 32 bits */
typedef struct quicklistNode {
struct quicklistNode *prev;
struct quicklistNode *next;
unsigned char *zl;
unsigned int sz; /* ziplist size in bytes */
unsigned int count : 16; /* count of items in ziplist */
unsigned int encoding : 2; /* RAW==1 or LZF==2 */
unsigned int container : 2; /* NONE==1 or ZIPLIST==2 */
unsigned int recompress : 1; /* was this node previous compressed? */
unsigned int attempted_compress : 1; /* node can't compress; too small */
unsigned int extra : 10; /* more bits to steal for future usage */
} quicklistNode;

其中prev和next分别指向之前和之后的Node,而zl表示存储数据的ziplist,关于ziplist前面已经介绍过了,这里要注意的是一个ziplist是存储了多个元素的,在插入数据的时候会根据情况选择增加一个新的Node还是将数据插入到当前已经存在的Node的zl中,这样应该也是一种节约内存的方式

总结

双向链表学过数据结构的都会知道,这里做的优化是使用ziplist,在一个Node里面存储多条数据以节省内存,具体的规则优点复杂这里不介绍了

Reference

  1. quicklist

简介

在内存充足时,redis过期分主动和被动两种,而当redis使用内存超过最大内存限制时,会根据逐出策略来做相应的操作。

主动过期

每次aeMain的时候,也就是每次的NIO Loop的时候,会先调用 activeExpireCycle

1
2
3
4
#0  activeExpireCycle (type=1) at expire.c:97
#1 0x0002fa6c in beforeSleep (eventLoop=<optimized out>) at server.c:1190
#2 0x0002d01c in aeMain (eventLoop=0x40812150) at ae.c:463
#3 0x0002a018 in main (argc=<optimized out>, argv=<optimized out>) at server.c:3844

expire.c:activeExpireCycle 会对expires这个hash表做随机采样,采样20个key,并在 activeExpireCycleTryExpire 函数内判断key是否过期,并对其做删除处理,这里 lazyfree_lazy_expire 控制了是同步删除还是异步删除。这个过程完成之后会判断本次过期的key的比例是否超过25%,也就是5个,如果超过5个则继续这个过程去删除过期key,如果耗时超过了ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC(貌似是25毫秒)设定的时间,也会停止这个过程

另外有定时任务会掉用这个方法做清理定时清理

1
2
3
4
5
6
7
#0  activeExpireCycle (type=0) at expire.c:97
#1 0x00030c70 in databasesCron () at server.c:878
#2 0x00033204 in serverCron (eventLoop=<optimized out>, id=<optimized out>, clientData=0x0) at server.c:1032
#3 0x0002cdc4 in processTimeEvents (eventLoop=0x40812150) at ae.c:323
#4 aeProcessEvents (eventLoop=0x40812150, flags=11) at ae.c:432
#5 0x0002d028 in aeMain (eventLoop=0x40812150) at ae.c:464
#6 0x0002a018 in main (argc=<optimized out>, argv=<optimized out>) at server.c:3844

被动过期

每次执行一些command的时候,例如get set,会先调用 db.c 里面的 expireIfNeeded,检查当前需要访问的key是否已经过期,如果已经过期就会删除它,这个删除会根据 lazyfree_lazy_expire 的设置来决定是同步删除还是异步删除,其实删除主要还是调用free去释放内存,异步删除是让后台线程去删除数据,当需要free的数据比较多的时候。这种情况处理的key较少,依赖被动查询key

内存不足时过期

redis数据结果如下

1
2
3
4
5
6
7
8
9
typedef struct redisDb {
dict *dict; /* The keyspace for this DB */
dict *expires; /* Timeout of keys with a timeout set */
dict *blocking_keys; /* Keys with clients waiting for data (BLPOP)*/
dict *ready_keys; /* Blocked keys that received a PUSH */
dict *watched_keys; /* WATCHED keys for MULTI/EXEC CAS */
int id; /* Database ID */
long long avg_ttl; /* Average TTL, just for stats */
} redisDb;

其中expires存储了带有过期时间key的hash表,dict是存储了所有的key的hash表
先简单谈下LRU和LFU,两个常用的缓存evication算法,分别是Least Recent Usage 和 Least Frequency Usage,当redis中数据超过设置的最大内存,且使用内存超过最大内存,但是又没有能过期的数据的时候,server需要删除一批数据来保证能够继续运行,具体的步骤是
在server.c每次处理命令的时候,会调用 evic.c 里面的 freeMemoryIfNeeded 函数,这个里面会调用 evictionPoolPopulate 函数来释放内存(当内存使用超过 maxmemory 的时候),这个过程也是随机选一些key然后来选出上次更新时间最早的,或者访问次数最少的,来释放内存,知道内存恢复到小于最大内存
注意这个过程有个时间控制,当key的上次访问时间超过 lfu_decay_time 的时候,它的LFU次数是要清零的,也就是那种之前访问了很多次,但是最近没咋访问的,这也很好的在lru字段中展现了出来

1
2
3
4
5
6
7
8
9
typedef struct redisObject {
unsigned type:4;
unsigned encoding:4;
unsigned lru:LRU_BITS; /* LRU time (relative to global lru_clock) or
* LFU data (least significant 8 bits frequency
* and most significant 16 bits access time). */
int refcount;
void *ptr;
} robj;

lru字段共24位,当过期策略是LFU时,其中低8位存LFU的count,高16位存上次读取的时间(分钟);当过期策略是LRU的时候,总共24位就都是LRU时间在里面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
* LFU (Least Frequently Used) implementation.

* We have 24 total bits of space in each object in order to implement
* an LFU (Least Frequently Used) eviction policy, since we re-use the
* LRU field for this purpose.
*
* We split the 24 bits into two fields:
*
* 16 bits 8 bits
* +----------------+--------+
* + Last decr time | LOG_C |
* +----------------+--------+
*
* LOG_C is a logarithmic counter that provides an indication of the access
* frequency. However this field must also be decremented otherwise what used
* to be a frequently accessed key in the past, will remain ranked like that
* forever, while we want the algorithm to adapt to access pattern changes.

过期时间配置的几种情况

1
2
3
4
5
6
7
8
9
10
11
 configEnum maxmemory_policy_enum[] = {
{"volatile-lru", MAXMEMORY_VOLATILE_LRU},
{"volatile-lfu", MAXMEMORY_VOLATILE_LFU},
{"volatile-random",MAXMEMORY_VOLATILE_RANDOM},
{"volatile-ttl",MAXMEMORY_VOLATILE_TTL},
{"allkeys-lru",MAXMEMORY_ALLKEYS_LRU},
{"allkeys-lfu",MAXMEMORY_ALLKEYS_LFU},
{"allkeys-random",MAXMEMORY_ALLKEYS_RANDOM},
{"noeviction",MAXMEMORY_NO_EVICTION},
{NULL, 0}
};

不同过期策略的含义:

  • noeviction: return errors when the memory limit was reached and the client is trying to execute commands that could result in more memory to be used (most write commands, but DEL and a few more exceptions).
  • allkeys-lru: evict keys by trying to remove the less recently used (LRU) keys first, in order to make space for the new data added.
  • volatile-lru: evict keys by trying to remove the less recently used (LRU) keys first, but only among keys that have an expire set, in order to make space for the new data added.
  • allkeys-random: evict keys randomly in order to make space for the new data added.
    volatile-random: evict keys randomly in order to make space for the new data added, but only evict keys with an expire set.
  • volatile-ttl: evict keys with an expire set, and try to evict keys with a shorter time to live (TTL) first, in order to make space for the new data added.

做lru数据过期的堆栈代码如下:

1
2
3
4
5
6
#0  freeMemoryIfNeeded () at evict.c:377
#1 0x0002da18 in processCommand (c=0x16d1d4) at server.c:2368
#2 0x0003d344 in processInputBuffer (c=0x16d1d4) at networking.c:1330
#3 0x00027bd4 in aeProcessEvents (eventLoop=0x134f54, flags=11) at ae.c:421
#4 0x00027f8c in aeMain (eventLoop=0x134f54) at ae.c:464
#5 0x000250c4 in main (argc=<optimized out>, argv=<optimized out>) at server.c:3844

总结

redis既可以用来做存储,又可以做缓存,而memcached只支持做缓存,当内存不够的时候它就会淘汰老的数据

Reference

  1. lru cache

背景介绍

redis内部使用了ziplist这种数据结构,当5大数据结构之hashes的field数量少于512并且每个field长度小于64的时候时会使用这种数据结构存储,超过这个范围就会使用hash table存储,下一节介绍hash table

hash-max-ziplist-entries 512
hash-max-ziplist-value 64

数据结构

ziplist的结构如下:

zlbytes zltail zllen entry1 entry2 entryn zlend

其中:
zlbytes(4 bytes)表示整个ziplist占用的字节数
zltail(4 bytes)表示最后一个entry的idex
zllen(2 bytes)表示entry的个数,低位在左侧
entry表示具体的一个元素
zlend(1 byte)表示结尾字节,恒为 0xFF

每一个entry的结构是这样:

prevlen encoding entry-data

其中:
prevlen表示之前的一个entry的字节数长度
encoding表示本entry的编码
entry-data存放具体的数据

当表示一个小的数字的时候(1 ~ 12),会按照下面的方式直接编码:

prevlen encoding

其中prevlen的含义不变,encoding的高4位为1111,低4位为表示的数字+1,例如 0xF2 表示 1

对于encoding(编码),数字类型都是以0x11pppppp这个格式的,也就是最左边两位是1,如果不是,那么就是字符串编码,
而数字编码又分为下面几种:

1
2
3
4
5
6
7
8
9
10
11
12
13
if (value >= 0 && value <= 12) {
*encoding = ZIP_INT_IMM_MIN+value;
} else if (value >= INT8_MIN && value <= INT8_MAX) {
*encoding = ZIP_INT_8B;
} else if (value >= INT16_MIN && value <= INT16_MAX) {
*encoding = ZIP_INT_16B;
} else if (value >= INT24_MIN && value <= INT24_MAX) {
*encoding = ZIP_INT_24B;
} else if (value >= INT32_MIN && value <= INT32_MAX) {
*encoding = ZIP_INT_32B;
} else {
*encoding = ZIP_INT_64B;
}

从上面可以看到,当数字在0到12之间的时候,会按照前面说的简洁编码方式,将数字和encoding编码到一个字节里面,至于其他情况,会根据数字占用的字节数,分配一个对应的encoding值,对应的值如下:

1
2
3
4
5
#define ZIP_INT_16B (0xc0 | 0<<4)
#define ZIP_INT_32B (0xc0 | 1<<4)
#define ZIP_INT_64B (0xc0 | 2<<4)
#define ZIP_INT_24B (0xc0 | 3<<4)
#define ZIP_INT_8B 0xfe

从上面可以看出,基本的情况是左侧4位有值的时候,都是按照int编码的,而且如果是数字一定是满足左侧两位为1,剩下的就是给字符串用的了

1
2
3
#define ZIP_STR_06B (0 << 6)
#define ZIP_STR_14B (1 << 6)
#define ZIP_STR_32B (2 << 6)

可以看到,这里使用了左侧的 00 01 10 三种情况来表示不同长度的字符串编码,真的是一点都不剩

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
if (ZIP_IS_STR(encoding)) {
/* Although encoding is given it may not be set for strings,
* so we determine it here using the raw length. */
if (rawlen <= 0x3f) {
if (!p) return len;
buf[0] = ZIP_STR_06B | rawlen;
} else if (rawlen <= 0x3fff) {
len += 1;
if (!p) return len;
buf[0] = ZIP_STR_14B | ((rawlen >> 8) & 0x3f);
buf[1] = rawlen & 0xff;
} else {
len += 4;
if (!p) return len;
buf[0] = ZIP_STR_32B;
buf[1] = (rawlen >> 24) & 0xff;
buf[2] = (rawlen >> 16) & 0xff;
buf[3] = (rawlen >> 8) & 0xff;
buf[4] = rawlen & 0xff;
}
}

长度字节编码在右侧的六位,以及右侧的其他字节,这里注意到这个enconding字段是会把字符串的长度也编码进去的,也就是说整个encoding字段最多可能有5个字节

总结

不得不说,为了节省内存,antirez还是下了很多功夫,一个是区分数字和字符串,因为如果直接对数字编码占用的内存会少一些,而且为了处理小数字的情况,还单独弄了个0到12的数字编码,以及充分利用的编码左边的两个位,设计的非常精妙,总之能省一点内存是一点。

Reference

  1. ziplist

背景介绍

众所周知,redis内部使用sds存储字符串,典型的是5大数据类型中的stirng的key和value都使用sds编码,这个数据结构和普通的char[]有一些差异,根据作者 antirez 描述这个数据结构存在一些优势,下面简单介绍其内部实现和优缺点。

数据结构

我们先从 redis 源码中找到 sds.h 文件,最开始引入眼帘的是

1
typedef char *sds;

也就是说sds本身其实是一个char数组,接着我们看到一些不同的定义

1
2
3
4
5
6
7
8
9
10
11
struct __attribute__ ((__packed__)) sdshdr5 {
unsigned char flags; /* 3 lsb of type, and 5 msb of string length */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr8 {
uint8_t len; /* used */
uint8_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
and so on

这些是对不同长度的sds结构体的定义,以 sdshdr8 为例

  • 第一个字节 len 为使用的数据的长度
  • 第二个字节 alloc 为整个 sds 分配的内存的长度
  • 第三个字节 flags,其低3位为类型,高5位为闲置位不使用
  • 后面 N 个字节,准确的说应该是alloc个字节,表示的是存储字符串的数组
    这里注意这些数据的内存地址都是连在一起的

    优缺点总结

    根据 antirez 的介绍,

    优点

  1. 使用的时候,不需要使用结构体那种箭头访问方式(t->p),比较方便,因为sds创建完成后是返回的buf的指针,而不是整个结构体的指针
  2. 访问内部数组的方式和char[]是一样的,都是可以按照index进行索引,原因同上一条
  3. 对比指针方式的字符串wrapper,例子如下,里面存的是指针,相当于结构体和字符串存在不连续的内存上,连续的内存会带来良好的cache效果
    1
    2
    3
    4
    5
    struct yourAverageStringLibrary {
    char *buf;
    size_t len;
    ... possibly more fields here ...
    };

    缺点

  4. sds支持append操作,当append的数据超过了当前的空闲长度的时候,会重新分配创建一个更大的sds内存,这样就需要将这个返回值重新赋值给之前的sds,具体如下,如果不赋值就会导致程序bug
    1
    s = sdscat(s,"Some more data");
  5. 上面说的这个问题,如果当一个sds使用的地方有多个的话,需要在修改的时候同时更新所有的使用点,否则会有bug

总结

优点中第三点可能确实是会带来实质性的性能提升,我水平有限无法验证,其他的优点都是代码使用方便方面的,对整体影响不大

Reference

  1. sds
  2. Redis V4.0

简介

最近在做一个HTTP流量转发的功能,发现POST请求的时候,当请求是 application/x-www-form-urlencoded 类型的时候,HttpServletRequest.getInputStream 是读取不到数据的,虽然ContentLength是不为空的,查了半天才解决。

为什么会出现问题

因为我在获取流之前,调用了 HttpServletRequest.getParameter,这个方法会读取 InputStream,并将form里的数据放到parameterMap里面,方便后面使用,具体调用方式见下面的tomcat代码,源代码见Reference中的1
tomcat read parameters
图中第一个关键点是只有content type是 application/x-www-form-urlencoded 的请求才会继续处理
第二个关键点是readPostBody
tomcat read parameters
这里我们看到流被读取了content length个字节出来,因此后面访问都会是空的

如何解决

在读取InputStream之前不去调用 HttpServletRequest.getParameter

Reference

  1. Tomcat Request(implementation of HttpServletRequest)

简介

众所周知,HTTP是不安全的,因为它采用了明文编码,几年前的时候,大家都会在百度首页看到一些膏药广告,实际上是百度被运营商劫持了,在页面中加了一些广告,搞点收入,还有大家会被提醒不要在公共场所的使用Wi-Fi登陆自己的账号,防止密码被截获。因此,几年前开始,各大网站都步入了全站HTTPS时代。

为什么HTTP是不安全的

HTTP是应用层网络协议,通过TCP包装数据,HTTP请求数据包格式如下:
http protocol
从图中可以看出,http请求由一个请求方法、请求资源的路径、协议和若干请求头组成,如果数据较多,会带一个数据体,数据体的长度由header中的Content-Length标明,值得注意的是基本每一行都是用 CR LF 结尾的,也就是我们常用的换行。
举例来说,我们用一个wireshark抓包看,大概是这样
http wireshark抓包
可以看到,请求头里面的username和password都是明文可见的,而且,对于将数据存储在body的情况下,我们也可以直接看到或者通过解压间接看到,因此http是不安全的。

为什么HTTPS是安全的

HTTPS的引入解决了HTTP的安全问题,HTTPS实质上是在HTTP上增加一个SSL安全传输层,以此来保证数据传输的安全性
https_explain
在讲HTTPS之前,我们先了解下对称加密和非对称加密
简单来说,对称加密就是加密和解密使用一个相同的密钥,而非对称加密就是加密和解密使用不同的密钥,加密使用公钥,解密使用私钥
通过公钥加密的信息,只能被私钥解密,每个客户端保存网站的公钥,而服务器端保存网站的私钥,这样就算数据被拦截,也解密不出来,HTTPS就是建立在非对称加密的基础上的。
常用的对称加密算法有AES,非对称加密的算法有RSA等,这里简单介绍一下RSA,其计算方式如下:

Reference

  1. Hypertext Transfer Protocol
  2. RSA
  3. RFC5246 TSL Specification
  4. Breaking Down the TLS Handshake

简介

2020年初的新冠肺炎由武汉传播到全中国,再到全世界,实体经济受到巨大打击,线上业务大热,主要体现在线上生鲜的销售,人们不得不在网上购买食材以维持生活。但是其他行业也需要借用线上来销售商品,例如线上卖酒店、机票,线上直播参观旅游景点,都是以直播方式进行,这期我们来讨论下直播业务代码的实现方式。

历史

早期的直播,例如体育直播,主要是

基本原理

网络直播系统的整体按照功能可分为两个部分,分别是是视频流、数据流。

a. 视频流部分

这部分主要是将主播的视频流分发到所有观众客户端,其流程如下:
livestream_flowchart
主播通过手机摄像头和麦克风采集音视频数据,经过经过SDK处理,将视频和音频分别按照H264、ACC编码,并推送到RMTP Server端,也就是源站,同时视频流会推送到CDN节点进行加速 ,观众端通过CDN获取这个RMTP流,并在手机端解码从而来观看直播。
由于需要CDN来确保观众端直播的低延迟,一般的互联网公司不会为了业务而单独维护CDN,因此很多公司的直播业务都是直接购买阿里云、腾讯云之类厂商提供的直播直播服务,直接推流云厂商的服务器,然后通过云厂商的CDN来拉流在观众端进行播放。

b. 数据流部分

这部分主要包括刷礼物、评论、点亮/点赞等消息和一些直播间的统计数据例如直播间人数、礼物榜等,其大致流程如下:
livestream_flowchart
数据流按照频次高低分成了两块,一块是频次较低的行为,例如图中左侧的主播开播、观众起播、直播评论、送礼物、点亮等等,这些操作同时也是可靠性要求较高的,因此都通过HTTP/HTTPS传输到服务端;另一块主要是频次较高的例如定时拉取直播间的评论、礼物等数据,以及进入直播间时需要做鉴权和分配房间操作,离开直播间需要将用户移除房间,以及心跳等等,这些主要是需要以较高的频次拉取,如果使用HTTP/HTTPS会存在建连的Overhead,直接使用长连接可直接发送数据,效率会更高,因此在长连接系统中实现。
直播长连接目前使用Netty搭建,从客户端到长连接服务器的上行消息,根据消息类型使用自定义的Handler来处理;从长连接服务器到客户端的下行消息,使用定时任务处理,定时一秒处理一次,处理流程大致是先将每个直播间的消息feed从Redis拉到内存,对应图中Puller的操作。另外会有另一个定时任务,将内存中的消息feed通过长连接发送到观众和主播的客户端,定时一秒执行一次,对应图中的Pusher。
为了保证横向扩容,使用LVS对长连接服务器做了4层的网络负载均衡,结构如下:
livestream_load_banlance
从图中可以看出,每个长连接机器上都保存了一些直播间,直播间里有进入观众的长连接信息在里面,例如Jack在直播间1,而Emily、Lily、John在直播间2,这里注意一个直播间可能会在多个长连接机器上存在,因为负载均衡可能会将不同的用户分配到不同的机器,因此一个直播间的所有观众应该是每个长连接机器上直播间的并集。

Reference

  1. What is RTMP and how it’s used in live-streaming
  2. RTMP(Real-Time Messaging Protocol)
  3. RTMP Specification
  4. 视频推流与拉流

简介

用户token主要用于用户登陆之后,对用户身份鉴权使用。在用户登陆之后,服务端会根据登陆情况下发一个token字段,之后客户端每次请求都带上这个值就能快速识别当前用户。

基本原理

用户token涉及到三个部分,一个是随机生成的token字段,不同用户不一样,存放到客户端或者种到Cookie;另一个是salt,不同的用户salt不同,存放到服务端;最后一个是token hash,是 token 和 salt 的hash值,主要用来校验客户端的token,存放到服务端。

token存储分析

注意客户端的随机token中包含了用户id,主要目的是在校验是查找到对应salt并进行hash

举例(i参考目前我们的业务使用的方式)

目前我们只要使用Java,在用户登陆校验成功之后,首先使用 UUID.randomUUID() 生成原始token数据,并将其和用户id用:符号拼接起来,格式如
8908f665-8464-4852-b1fa-14fe62b5aa76:666666
其中:后面的666666为用户id;之后再使用 UUID.randomUUID() 生成salt,然后将token和salt通过:拼接之后,做SHA256哈希,得到的值为token hash,将用户id和salt、token hash存储到数据库和缓存,然后将token放到返回到客户端的http响应中。之后客户端的请求会在Cookie中带上token,服务端收到后解析出其中的用户id,并从而拿到该用户的salt和token hash,并将token和salt拼接起来做SHA256哈希,并和token hash做比对,从而达到校验token的目的。
整个具体的流程见下面的视频(点击可播放):
如何设计用户登陆系统

优点

  1. 安全性;因为用户端存储的token只有在用户的登陆的http请求里是可以知道的,在极端情况下,例如被拖库,入侵者拿到的也只是salt和token hash,通过这些很难直接逆向出用户的token,因为hash算法一般在单个人的生命周期时间内不能逆向,从而极大程度的保护了用户账号的安全。

简介

9月初印度之行的目的主要是对印度市场进行调研,摸清楚用户的一些特点和习惯,并发现目前存在的问题,解决现有的问题并针对目前用户的情况对产品的规划做调整,作为一名技术,对前者的关注会多一些。

  1. 入境检查的时候被他们奇怪的英语口音惊到,但是他确实没咋为难我。
  2. 班加罗尔的酒店小哥会帮忙把行李从车上拿下来并送到门口,没住过五星级酒店的土包子感觉良好。
  3. 街头访问的时候,几乎所有人都热心接收访问,有问必答,有中年大叔问我是不是会上电视,等公交车的学生回答问题的时候都是笑呵呵的,大部分人回答问题都很认真
  4. 也有会占小便宜的人,飞机上旁边的大叔会让我给他系安全带,而且我给他吃的之后,他觉得好吃会问我还有没有,他还想吃,之后是不会说谢谢的;还有飞机隔壁座位大姐借我手机打电话,虽然她有家人在另一个座位上坐着,都坐飞机了为啥还在乎这些东西呢。
  5. 首都德里大量流浪汉,晚上就睡路上,你在路上走可能踢到他的头。
  6. 去景点玩看到有小孩子迎面走来,他们会主动和你笑着say hi,可能觉得我们是外国人。

  1. 中国品牌很对,手机方面有Oppo、Vivo、小米、1加(One Plus)等,我还看到了特步的店,用这些国内的手机主要是性价比高,如果有钱他们会买三星苹果。
  2. 物价较国内便宜,手机卡套餐1.5G每天,价格是15元RMB左右;点一个牛排只有60元RMB左右,有2块牛排能吃饱;打车一般就十几二十块RMB;
  3. 打车用Uber,地图用Google Map,基本上就没什么障碍了,除了吃饭。
  4. 吃饭问题难以习惯,吃的都是当地带一种香料的饭菜,只能吃土豆、饼、水果啥的,或者去中国菜馆、牛排店等地方吃饭。

现状

  1. WhatsApp、Facebook、Instagram、Youtube、TikTok使用较多,上层人喜欢用Instagram,底层一些的人喜欢用TikTok,但是短视频的App太多,都是相对底层的人在用
  2. 短视频的主要作用是搞笑,另外就是听一些流行的歌,短视频的背景音乐会有。
  3. 短视频平台太多,不同App之间存在差异,大部分都提供了本地化语言内容,对比体验来说,我司的产品处于中下位置。
  4. Youtube在技术上独领风骚,能做到弱网环境不卡,实际上Youtube的使用者也不少,有的喜欢学习的人甚至在上面看TED
  5. 大家TikTok整体的印象是内容有趣,但是广告偏多,有的平民通过TikTok升级成网红了,所以很多人也想当网红。

    机遇与挑战

  6. TikTok用户量已经非常庞大,竞争对手需要找到对方弱点,做差异化,才有可能突围。
  7. TikTok在一些公共场所是遭到抵制的,不让拍,因为之前出现过不好的事情,但是这种负面新闻反而让它名声更响。

感悟

运营其实也是必不可少的一项工作

简介

hppc(High Performance Primitive Collections)是一款能带来性能提升和空间优化的Java原生类型框架,这些都是对比的Java自带的Collection,主要原因是Java自带的Collection使用对象做元素,而对象存在一些Overhead。

问题描述

使用Iterator.partition的时候,出现每次partiton的结果都是相同的一个值,例如对 [1,2,3,4,5]做partition,按照2个一份,最终出来的结果是,[1,1]、[3,3,]、[5]

问题分析

通过debug和代码进行分析,查看Iterators.partition实现代码,发现它每次都会把从Iterator拿到的数据放到一个数组里面,然后转化成List

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
private static <T> UnmodifiableIterator<List<T>> partitionImpl(
final Iterator<T> iterator, final int size, final boolean pad) {
checkNotNull(iterator);
checkArgument(size > 0);
return new UnmodifiableIterator<List<T>>() {
@Override
public boolean hasNext() {
return iterator.hasNext();
}

@Override
public List<T> next() {
if (!hasNext()) {
throw new NoSuchElementException();
}
Object[] array = new Object[size];
int count = 0;
for (; count < size && iterator.hasNext(); count++) {
array[count] = iterator.next();
}
for (int i = count; i < size; i++) {
array[i] = null; // for GWT
}

@SuppressWarnings("unchecked") // we only put Ts in it
List<T> list = Collections.unmodifiableList((List<T>) Arrays.asList(array));
return (pad || count == size) ? list : list.subList(0, count);
}
};
}

查看hppc迭代器代码,以LongHashSet为例子,代码如下,可以看到迭代器的next方法依赖的fetch方法每次都是返回相同的reference,只是把里面的值做了相应的变化(这也许也是它省内存的一部分)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Override
protected LongCursor fetch() {
if (slot < max) {
long existing;
for (slot++; slot < max; slot++) {
if (!((existing = keys[slot]) == 0)) {
cursor.index = slot;
cursor.value = existing;
return cursor;
}
}
}

if (slot == max && hasEmptyKey) {
cursor.index = slot;
cursor.value = 0L;
slot++;
return cursor;
}

return done();
}

问题其实就出在这里,每次partition后的List里面其实只有一个重复的reference,不论List有多大

解决办法

partition之前先用 Iterators.transform 对hppc的类型做转换,转换成基本类型即可