看redis makefile,重新学习makefile

曾经我以为我懂makefile,看着网上的一步一步教你写makefile读完,感觉自己啥都会了。makefile不就是个语法诡异的perl吗,都有cmake了谁还有make。那个时候,我既不懂编译原理,也不懂perl,更不懂cmake,现在我也不太懂。

简单概括流程

  • 顶层makefile直接 引导到src目录$(MAKE) $@ 转发到子目录

  • 调用mkrelease.sh 生成release.sh
    • 这个脚本会手动pull更新代码和submodule
    • 注意权限,否则无法生成release.h,这个文件记录最新的git commit和sha1
  • 走%.o: %.c .make-prerequisites,判断依赖make-prerequisites是否存在,然后找persisi-setting, 然后找到make-setting,如果已经存在了,就会直接编译o文件&链接了。
  • 生成.make-settings,缓存编译配置。注意该配置不make distclean会一直生效
  • make deps 注意有些有文件生成,需要脚本权限(jemalloc说的就是你

    chmod 777 deps/jemalloc/configure
    chmod 777 src/mkreleasehdr.sh
    chmod 777 deps/jemalloc/include/jemalloc/*
    chmod 777 deps/jemalloc/scripts/*
    
  • 子模块编译注意
    • 同redis编译逻辑,会生成make-setting文件,注意清理要make distclean,不过redis目录也得清理,不然走不进这个分支流程
    • redis编译flag和子模块不一样,比如hiredis子模块是gnu, 本体是c99, 就会重编,按理说这个场景会判断出来。但是有的模块没有判定,比如jemalloc。这里感觉会有坑。已经遇到了一个连接错误的问题,发现jemalloc不知怎么的改成gnu11编译了。保险起见deps目录make distclean, redis src目录 make distclean
  • 按照设定和生成的deps编译,然后根据设定链接,然后走all的规则内容,逐个链接
  • 最后提示make test 运行测试

这里面好多规则跳跃,有点迷糊。

make流程日志

[root@redis]# make
cd src && make all
sh: ./mkreleasehdr.sh: Permission denied #注意这里,需要权限
make[1]: Entering directory `/root/redis/src'
    CC Makefile.dep
make[1]: Leaving directory `/root/redis/src'
sh: ./mkreleasehdr.sh: Permission denied
make[1]: Entering directory `/root/redis/src'
rm -rf redis-server redis-sentinel redis-cli redis-benchmark redis-check-rdb redis-check-aof *.o *.gcda *.gcno *.gcov redis.info lcov-html Makefile.dep dict-benchmark
(cd ../deps && make distclean)
make[2]: Entering directory `/root/redis/deps'
(cd hiredis && make clean) > /dev/null || true
(cd linenoise && make clean) > /dev/null || true
(cd lua && make clean) > /dev/null || true
(cd jemalloc && [ -f Makefile ] && make distclean) > /dev/null || true
(rm -f .make-*)
make[2]: Leaving directory `/root/redis/deps'
(rm -f .make-*)
echo STD=-std=c99 -pedantic -Dredis_STATIC='' >> .make-settings
echo WARN=-Wall -W -Wno-missing-field-initializers >> .make-settings
echo OPT=-O2 >> .make-settings
echo MALLOC=jemalloc >> .make-settings
echo CFLAGS= >> .make-settings
echo LDFLAGS= >> .make-settings
echo redis_CFLAGS= >> .make-settings
echo redis_LDFLAGS= >> .make-settings
echo PREV_FINAL_CFLAGS=-std=c99 -pedantic -Dredis_STATIC='' -Wall -W -Wno-missing-field-initializers -O2 -g -ggdb   -I../deps/hiredis -I../deps/linenoise -I../deps/lua/src -DUSE_JEMALLOC -I../deps/jemalloc/include >> .make-settings
echo PREV_FINAL_LDFLAGS=  -g -ggdb -rdynamic >> .make-settings
(cd ../deps && make hiredis linenoise lua jemalloc)
make[2]: Entering directory `/root/redis/deps'
(cd hiredis && make clean) > /dev/null || true
(cd linenoise && make clean) > /dev/null || true
(cd lua && make clean) > /dev/null || true
(cd jemalloc && [ -f Makefile ] && make distclean) > /dev/null || true
(rm -f .make-*)
(echo "" > .make-cflags)
(echo "" > .make-ldflags)
MAKE hiredis
cd hiredis && make static
make[3]: Entering directory `/root/redis/deps/hiredis'
ar rcs libhiredis.a net.o hiredis.o sds.o async.o read.o
make[3]: Leaving directory `/root/redis/deps/hiredis'
MAKE linenoise
cd linenoise && make
make[3]: Entering directory `/root/redis/deps/linenoise'
cc  -Wall -Os -g  -c linenoise.c
make[3]: Leaving directory `/root/redis/deps/linenoise'
MAKE lua
cd lua/src && make all CFLAGS="-O2 -Wall -DLUA_ANSI -DENABLE_CJSON_GLOBAL -Dredis_STATIC='' " MYLDFLAGS="" AR="ar rcu"
make[3]: Entering directory `/root/redis/deps/lua/src'
cc -o luac  luac.o print.o liblua.a -lm
make[3]: Leaving directory `/root/redis/deps/lua/src'
MAKE jemalloc
cd jemalloc && ./configure --with-version=5.1.0-0-g0 --with-lg-quantum=3 --with-jemalloc-prefix=je_ --enable-cc-silence CFLAGS="-std=gnu99 -Wall -pipe -g3 -O3 -funroll-loops " LDFLAGS=""
///bin/sh: ./configure: Permission denied   注意这里,也需要权限
make[2]: *** [jemalloc] Error 126
make[2]: Leaving directory `/root/redis/deps'
make[1]: [persist-settings] Error 2 (ignored)
# redis Makefile
# Copyright (C) 2009 Salvatore Sanfilippo <antirez at gmail dot com>
# This file is released under the BSD license, see the COPYING file
#
# The Makefile composes the final FINAL_CFLAGS and FINAL_LDFLAGS using
# what is needed for redis plus the standard CFLAGS and LDFLAGS passed.
# However when building the dependencies (Jemalloc, Lua, Hiredis, ...)
# CFLAGS and LDFLAGS are propagated to the dependencies, so to pass
# flags only to be used when compiling / linking redis itself redis_CFLAGS
# and redis_LDFLAGS are used instead (this is the case of 'make gcov').
#
# Dependencies are stored in the Makefile.dep file. To rebuild this file
# Just use 'make dep', but this is only needed by developers.

#生成 release信息
release_hdr := $(shell sh -c './mkreleasehdr.sh')
#提取Linux
uname_S := $(shell sh -c 'uname -s 2>/dev/null || echo not')
#提取 x85_64
uname_M := $(shell sh -c 'uname -m 2>/dev/null || echo not')
# 优化等级
OPTIMIZATION?=-O2
# 子模块,注意,这里的内容会被转发给make,进入deps层编译,如果加子模块需要改动这里
DEPENDENCY_TARGETS=hiredis linenoise lua
NODEPS:=clean distclean

# Default settings
# 注意这个std=c99
STD=-std=c99 -pedantic -Dredis_STATIC=''
ifneq (,$(findstring clang,$(CC)))
ifneq (,$(findstring FreeBSD,$(uname_S)))
  #不用c11拓展?为了兼容性吧,本身都 c99 l 
  STD+=-Wno-c11-extensions
endif
endif
WARN=-Wall -W -Wno-missing-field-initializers
OPT=$(OPTIMIZATION)

PREFIX?=/usr/local
INSTALL_BIN=$(PREFIX)/bin
INSTALL=install

#设置内存分配器,默认jemalloc
# Default allocator defaults to Jemalloc if it's not an ARM
MALLOC=libc
ifneq ($(uname_M),armv6l)
ifneq ($(uname_M),armv7l)
ifeq ($(uname_S),Linux)
	MALLOC=jemalloc
endif
endif
endif

# To get ARM stack traces if redis crashes we need a special C flag.
ifneq (,$(filter aarch64 armv,$(uname_M)))
        CFLAGS+=-funwind-tables
else
ifneq (,$(findstring armv,$(uname_M)))
        CFLAGS+=-funwind-tables
endif
endif

# Backwards compatibility for selecting an allocator
ifeq ($(USE_TCMALLOC),yes)
	MALLOC=tcmalloc
endif

ifeq ($(USE_TCMALLOC_MINIMAL),yes)
	MALLOC=tcmalloc_minimal
endif

ifeq ($(USE_JEMALLOC),yes)
	MALLOC=jemalloc
endif

ifeq ($(USE_JEMALLOC),no)
	MALLOC=libc
endif

# Override default settings if possible
#注意这行 默认是会生成一个 .make-settings文件的,如果有这个文件
#就不会再生成了,所有编译选线都缓存了,所以光改makefile没用,还得把这个文件删掉(make distclean)
-include .make-settings

#下面是调试符号支持和pthread支持,平台各异
FINAL_CFLAGS=$(STD) $(WARN) $(OPT) $(DEBUG) $(CFLAGS) $(redis_CFLAGS)
FINAL_LDFLAGS=$(LDFLAGS) $(redis_LDFLAGS) $(DEBUG)
FINAL_LIBS=-lm
DEBUG=-g -ggdb

ifeq ($(uname_S),SunOS)
	# SunOS
        ifneq ($(@@),32bit)
		CFLAGS+= -m64
		LDFLAGS+= -m64
	endif
	DEBUG=-g
	DEBUG_FLAGS=-g
	export CFLAGS LDFLAGS DEBUG DEBUG_FLAGS
	INSTALL=cp -pf
	FINAL_CFLAGS+= -D__EXTENSIONS__ -D_XPG6
	FINAL_LIBS+= -ldl -lnsl -lsocket -lresolv -lpthread -lrt
else
ifeq ($(uname_S),Darwin)
	# Darwin
	FINAL_LIBS+= -ldl
else
ifeq ($(uname_S),AIX)
        # AIX
        FINAL_LDFLAGS+= -Wl,-bexpall
        FINAL_LIBS+=-ldl -pthread -lcrypt -lbsd
else
ifeq ($(uname_S),OpenBSD)
	# OpenBSD
	FINAL_LIBS+= -lpthread
	ifeq ($(USE_BACKTRACE),yes)
	    FINAL_CFLAGS+= -DUSE_BACKTRACE -I/usr/local/include
	    FINAL_LDFLAGS+= -L/usr/local/lib
	    FINAL_LIBS+= -lexecinfo
    	endif

else
ifeq ($(uname_S),FreeBSD)
	# FreeBSD
	FINAL_LIBS+= -lpthread -lexecinfo
else
ifeq ($(uname_S),DragonFly)
	# FreeBSD
	FINAL_LIBS+= -lpthread -lexecinfo
else
	# All the other OSes (notably Linux)
	FINAL_LDFLAGS+= -rdynamic
	FINAL_LIBS+=-ldl -pthread -lrt
endif
endif
endif
endif
endif
endif

#这里是deps目录,指定目录放在这
# Include paths to dependencies
FINAL_CFLAGS+= -I../deps/hiredis -I../deps/linenoise -I../deps/lua/src 

ifeq ($(MALLOC),tcmalloc)
	FINAL_CFLAGS+= -DUSE_TCMALLOC
	FINAL_LIBS+= -ltcmalloc
endif

ifeq ($(MALLOC),tcmalloc_minimal)
	FINAL_CFLAGS+= -DUSE_TCMALLOC
	FINAL_LIBS+= -ltcmalloc_minimal
endif

ifeq ($(MALLOC),jemalloc)
	DEPENDENCY_TARGETS+= jemalloc
	FINAL_CFLAGS+= -DUSE_JEMALLOC -I../deps/jemalloc/include
	FINAL_LIBS := ../deps/jemalloc/lib/libjemalloc.a $(FINAL_LIBS)
endif

redis_CC=$(QUIET_CC)$(CC) $(FINAL_CFLAGS)
redis_LD=$(QUIET_LINK)$(CC) $(FINAL_LDFLAGS)
redis_INSTALL=$(QUIET_INSTALL)$(INSTALL)

CCCOLOR="\033[34m"
LINKCOLOR="\033[34;1m"
SRCCOLOR="\033[33m"
BINCOLOR="\033[37;1m"
MAKECOLOR="\033[32;1m"
ENDCOLOR="\033[0m"

ifndef V
QUIET_CC = @printf '    %b %b\n' $(CCCOLOR)CC$(ENDCOLOR) $(SRCCOLOR)$@$(ENDCOLOR) 1>&2;
QUIET_LINK = @printf '    %b %b\n' $(LINKCOLOR)LINK$(ENDCOLOR) $(BINCOLOR)$@$(ENDCOLOR) 1>&2;
QUIET_INSTALL = @printf '    %b %b\n' $(LINKCOLOR)INSTALL$(ENDCOLOR) $(BINCOLOR)$@$(ENDCOLOR) 1>&2;
endif

#所有生成文件的obj文件依赖,手写。见all哪里,指定生成文件,调到相应规则,然后找这里的依赖
redis_SERVER_NAME=redis-server
redis_SENTINEL_NAME=redis-sentinel
redis_SERVER_OBJ=adlist.o quicklist.o ae.o anet.o dict.o server.o sds.o zmalloc.o lzf_c.o lzf_d.o pqsort.o zipmap.o sha1.o ziplist.o release.o networking.o util.o object.o db.o replication.o rdb.o t_string.o t_list.o t_set.o t_zset.o t_hash.o config.o aof.o pubsub.o multi.o debug.o sort.o intset.o syncio.o cluster.o crc16.o endianconv.o slowlog.o scripting.o bio.o rio.o rand.o memtest.o crc64.o bitops.o sentinel.o notify.o setproctitle.o blocked.o hyperloglog.o latency.o sparkline.o redis-check-rdb.o redis-check-aof.o geo.o lazyfree.o module.o evict.o expire.o geohash.o geohash_helper.o childinfo.o defrag.o siphash.o rax.o t_stream.o listpack.o localtime.o lolwut.o lolwut5.o swapdata.o thpool.o sp_queue.o
redis_CLI_NAME=redis-cli
redis_CLI_OBJ=anet.o adlist.o dict.o redis-cli.o zmalloc.o release.o anet.o ae.o crc64.o siphash.o crc16.o
redis_BENCHMARK_NAME=redis-benchmark
redis_BENCHMARK_OBJ=ae.o anet.o redis-benchmark.o adlist.o zmalloc.o redis-benchmark.o
redis_CHECK_RDB_NAME=redis-check-rdb
redis_CHECK_AOF_NAME=redis-check-aof

#这是所有编译入口,逐个编译。之前会生成dep,生成依赖。这里的流程没有搞清楚
all: $(redis_SERVER_NAME) $(redis_SENTINEL_NAME) $(redis_CLI_NAME) $(redis_BENCHMARK_NAME) $(redis_CHECK_RDB_NAME) $(redis_CHECK_AOF_NAME)
	@echo ""
	@echo "Hint: It's a good idea to run 'make test' ;)"
	@echo ""

Makefile.dep:
	-$(redis_CC) -MM *.c > Makefile.dep 2> /dev/null || true

ifeq (0, $(words $(findstring $(MAKECMDGOALS), $(NODEPS))))
-include Makefile.dep
endif

.PHONY: all
#生成 .make-setting,这里都是编译配置
persist-settings: distclean
	echo STD=$(STD) >> .make-settings
	echo WARN=$(WARN) >> .make-settings
	echo OPT=$(OPT) >> .make-settings
	echo MALLOC=$(MALLOC) >> .make-settings
	echo CFLAGS=$(CFLAGS) >> .make-settings
	echo LDFLAGS=$(LDFLAGS) >> .make-settings
	echo redis_CFLAGS=$(redis_CFLAGS) >> .make-settings
	echo redis_LDFLAGS=$(redis_LDFLAGS) >> .make-settings
	echo PREV_FINAL_CFLAGS=$(FINAL_CFLAGS) >> .make-settings
	echo PREV_FINAL_LDFLAGS=$(FINAL_LDFLAGS) >> .make-settings
	#注意这里,生成编译配置后会进入deps编译子模块
	-(cd ../deps && $(MAKE) $(DEPENDENCY_TARGETS))

.PHONY: persist-settings

#检测是否生成make-setting的一个占位符
# Prerequisites target
.make-prerequisites:
	@touch $@

# Clean everything, persist settings and build dependencies if anything changed
ifneq ($(strip $(PREV_FINAL_CFLAGS)), $(strip $(FINAL_CFLAGS)))
.make-prerequisites: persist-settings
endif

ifneq ($(strip $(PREV_FINAL_LDFLAGS)), $(strip $(FINAL_LDFLAGS)))
.make-prerequisites: persist-settings
endif

#所有二进制依赖
# redis-server
$(redis_SERVER_NAME): $(redis_SERVER_OBJ)
	$(redis_LD) -o $@ $^ ../deps/hiredis/libhiredis.a ../deps/lua/src/liblua.a  $(FINAL_LIBS)

# redis-sentinel
$(redis_SENTINEL_NAME): $(redis_SERVER_NAME)
	$(redis_INSTALL) $(redis_SERVER_NAME) $(redis_SENTINEL_NAME)

# redis-check-rdb
$(redis_CHECK_RDB_NAME): $(redis_SERVER_NAME)
	$(redis_INSTALL) $(redis_SERVER_NAME) $(redis_CHECK_RDB_NAME)

# redis-check-aof
$(redis_CHECK_AOF_NAME): $(redis_SERVER_NAME)
	$(redis_INSTALL) $(redis_SERVER_NAME) $(redis_CHECK_AOF_NAME)

# redis-cli
$(redis_CLI_NAME): $(redis_CLI_OBJ)
	$(redis_LD) -o $@ $^ ../deps/hiredis/libhiredis.a ../deps/linenoise/linenoise.o $(FINAL_LIBS)

# redis-benchmark
$(redis_BENCHMARK_NAME): $(redis_BENCHMARK_OBJ)
	$(redis_LD) -o $@ $^ ../deps/hiredis/libhiredis.a $(FINAL_LIBS)

dict-benchmark: dict.c zmalloc.c sds.c siphash.c
	$(redis_CC) $(FINAL_CFLAGS) $^ -D DICT_BENCHMARK_MAIN -o $@ $(FINAL_LIBS)

# Because the jemalloc.h header is generated as a part of the jemalloc build,
# building it should complete before building any other object. Instead of
# depending on a single artifact, build all dependencies first.

#最开始执行这个,编译,检查prerequisites,不满足会跳到
# persis-setting ,跳到到make-setting, 然后执行编译生成obj文件,最后执行all。
%.o: %.c .make-prerequisites 
	$(redis_CC) -c $<

#注意这个清理是不清理 .make*文件的。对于普通用户来说,这加速编译了一下,不用反复编译deps模块,对于开发者要注意。
clean:
	rm -rf $(redis_SERVER_NAME) $(redis_SENTINEL_NAME) $(redis_CLI_NAME) $(redis_BENCHMARK_NAME) $(redis_CHECK_RDB_NAME) $(redis_CHECK_AOF_NAME) *.o *.gcda *.gcno *.gcov redis.info lcov-html Makefile.dep dict-benchmark

.PHONY: clean

distclean: clean
	-(cd ../deps && $(MAKE) distclean)
	-(rm -f .make-*)

.PHONY: distclean

test: $(redis_SERVER_NAME) $(redis_CHECK_AOF_NAME)
	@(cd ..; ./runtest)

test-sentinel: $(redis_SENTINEL_NAME)
	@(cd ..; ./runtest-sentinel)

check: test

lcov:
	$(MAKE) gcov
	@(set -e; cd ..; ./runtest --clients 1)
	@geninfo -o redis.info .
	@genhtml --legend -o lcov-html redis.info

test-sds: sds.c sds.h
	$(redis_CC) sds.c zmalloc.c -DSDS_TEST_MAIN $(FINAL_LIBS) -o /tmp/sds_test
	/tmp/sds_test

.PHONY: lcov

bench: $(redis_BENCHMARK_NAME)
	./$(redis_BENCHMARK_NAME)

32bit:
	@echo ""
	@echo "WARNING: if it fails under Linux you probably need to install libc6-dev-i386"
	@echo ""
	$(MAKE) CFLAGS="-m32" LDFLAGS="-m32"

gcov:
	$(MAKE) redis_CFLAGS="-fprofile-arcs -ftest-coverage -DCOVERAGE_TEST" redis_LDFLAGS="-fprofile-arcs -ftest-coverage"

noopt:
	$(MAKE) OPTIMIZATION="-O0"

valgrind:
	$(MAKE) OPTIMIZATION="-O0" MALLOC="libc"

helgrind:
	$(MAKE) OPTIMIZATION="-O0" MALLOC="libc" CFLAGS="-D__ATOMIC_VAR_FORCE_SYNC_MACROS"

src/help.h:
	@../utils/generate-command-help.rb > help.h

install: all
	@mkdir -p $(INSTALL_BIN)
	$(redis_INSTALL) $(redis_SERVER_NAME) $(INSTALL_BIN)
	$(redis_INSTALL) $(redis_BENCHMARK_NAME) $(INSTALL_BIN)
	$(redis_INSTALL) $(redis_CLI_NAME) $(INSTALL_BIN)
	$(redis_INSTALL) $(redis_CHECK_RDB_NAME) $(INSTALL_BIN)
	$(redis_INSTALL) $(redis_CHECK_AOF_NAME) $(INSTALL_BIN)
	@ln -sf $(redis_SERVER_NAME) $(INSTALL_BIN)/$(redis_SENTINEL_NAME)

参考

  • make 参考文档 https://www.gnu.org/software/make/manual/make.pdf

  • 跟我一起写makefile https://seisman.github.io/how-to-write-makefile

Read More

redis 代码走读

[toc]

数据结构

set

  • 实现 intset / hashtable(dict)实际上是一样的,编码不同
命令 intset 编码的实现方法 hashtable 编码的实现方法
SADD 调用 intsetAdd 函数, 将所有新元素添加到整数集合里面。 调用 dictAdd , 以新元素为键, NULL 为值, 将键值对添加到字典里面。
SCARD 调用 intsetLen 函数, 返回整数集合所包含的元素数量, 这个数量就是集合对象所包含的元素数量。 调用 dictSize 函数, 返回字典所包含的键值对数量, 这个数量就是集合对象所包含的元素数量。
SISMEMBER 调用 intsetFind 函数, 在整数集合中查找给定的元素, 如果找到了说明元素存在于集合, 没找到则说明元素不存在于集合。 调用 dictFind 函数, 在字典的键中查找给定的元素, 如果找到了说明元素存在于集合, 没找到则说明元素不存在于集合。
SMEMBERS 遍历整个整数集合, 使用 intsetGet 函数返回集合元素。 遍历整个字典, 使用 dictGetKey 函数返回字典的键作为集合元素。
SRANDMEMBER 调用 intsetRandom 函数, 从整数集合中随机返回一个元素。 调用 dictGetRandomKey 函数, 从字典中随机返回一个字典键。
SPOP 调用 intsetRandom 函数, 从整数集合中随机取出一个元素, 在将这个随机元素返回给客户端之后, 调用 intsetRemove 函数, 将随机元素从整数集合中删除掉。 调用 dictGetRandomKey 函数, 从字典中随机取出一个字典键, 在将这个随机字典键的值返回给客户端之后, 调用 dictDelete 函数, 从字典中删除随机字典键所对应的键值对。
SREM 调用 intsetRemove 函数, 从整数集合中删除所有给定的元素。 调用 dictDelete 函数, 从字典中删除所有键为给定元素的键值对。

zset

  • 为什么ZSCORE是O(1)的 因为是组合存储的,hashtable+skiplist

  • 内部实现skiplist/ziplist

skiplist

img

命令 ziplist 编码的实现方法 zset 编码的实现方法
ZADD 调用 ziplistInsert 函数, 将成员和分值作为两个节点分别插入到压缩列表。 先调用 zslInsert 函数, 将新元素添加到跳跃表, 然后调用 dictAdd 函数, 将新元素关联到字典。
ZCARD 调用 ziplistLen 函数, 获得压缩列表包含节点的数量, 将这个数量除以 2 得出集合元素的数量。 访问跳跃表数据结构的 length 属性, 直接返回集合元素的数量。
ZCOUNT 遍历压缩列表, 统计分值在给定范围内的节点的数量。 遍历跳跃表, 统计分值在给定范围内的节点的数量。
ZRANGE 从表头向表尾遍历压缩列表, 返回给定索引范围内的所有元素。 从表头向表尾遍历跳跃表, 返回给定索引范围内的所有元素。
ZREVRANGE 从表尾向表头遍历压缩列表, 返回给定索引范围内的所有元素。 从表尾向表头遍历跳跃表, 返回给定索引范围内的所有元素。
ZRANK 从表头向表尾遍历压缩列表, 查找给定的成员, 沿途记录经过节点的数量, 当找到给定成员之后, 途经节点的数量就是该成员所对应元素的排名。 从表头向表尾遍历跳跃表, 查找给定的成员, 沿途记录经过节点的数量, 当找到给定成员之后, 途经节点的数量就是该成员所对应元素的排名。
ZREVRANK 从表尾向表头遍历压缩列表, 查找给定的成员, 沿途记录经过节点的数量, 当找到给定成员之后, 途经节点的数量就是该成员所对应元素的排名。 从表尾向表头遍历跳跃表, 查找给定的成员, 沿途记录经过节点的数量, 当找到给定成员之后, 途经节点的数量就是该成员所对应元素的排名。
ZREM 遍历压缩列表, 删除所有包含给定成员的节点, 以及被删除成员节点旁边的分值节点。 遍历跳跃表, 删除所有包含了给定成员的跳跃表节点。 并在字典中解除被删除元素的成员和分值的关联。
ZSCORE 遍历压缩列表, 查找包含了给定成员的节点, 然后取出成员节点旁边的分值节点保存的元素分值。 直接从字典中取出给定成员的分值。

list

  • 内部编码 quicklist,代替linkedlist,两者区别?
  • 编码的区别,api完全一致

  • 这是3.0版本,新版本就是把api换成quicklist-api,接口完全一致,原来的编码方案废除
#define OBJ_ENCODING_QUICKLIST 9 /* Encoded as linked list of ziplists */
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;
命令 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 函数, 定位到双端链表指定索引上的节点, 然后通过赋值操作更新节点的值。

string

  • kv 存在hashtable(dict), 有不同的编码方式
命令 int 编码的实现方法 embstr 编码的实现方法 raw 编码的实现方法
SET 使用 int 编码保存值。 使用 embstr 编码保存值。 使用 raw 编码保存值。
GET 拷贝对象所保存的整数值, 将这个拷贝转换成字符串值, 然后向客户端返回这个字符串值。 直接向客户端返回字符串值。 直接向客户端返回字符串值。
APPEND 将对象转换成 raw 编码, 然后按 raw编码的方式执行此操作。 将对象转换成 raw 编码, 然后按 raw编码的方式执行此操作。 调用 sdscatlen 函数, 将给定字符串追加到现有字符串的末尾。
INCRBYFLOAT 取出整数值并将其转换成 longdouble 类型的浮点数, 对这个浮点数进行加法计算, 然后将得出的浮点数结果保存起来。 取出字符串值并尝试将其转换成long double 类型的浮点数, 对这个浮点数进行加法计算, 然后将得出的浮点数结果保存起来。 如果字符串值不能被转换成浮点数, 那么向客户端返回一个错误。 取出字符串值并尝试将其转换成 longdouble 类型的浮点数, 对这个浮点数进行加法计算, 然后将得出的浮点数结果保存起来。 如果字符串值不能被转换成浮点数, 那么向客户端返回一个错误。
INCRBY 对整数值进行加法计算, 得出的计算结果会作为整数被保存起来。 embstr 编码不能执行此命令, 向客户端返回一个错误。 raw 编码不能执行此命令, 向客户端返回一个错误。
DECRBY 对整数值进行减法计算, 得出的计算结果会作为整数被保存起来。 embstr 编码不能执行此命令, 向客户端返回一个错误。 raw 编码不能执行此命令, 向客户端返回一个错误。
STRLEN 拷贝对象所保存的整数值, 将这个拷贝转换成字符串值, 计算并返回这个字符串值的长度。 调用 sdslen 函数, 返回字符串的长度。 调用 sdslen 函数, 返回字符串的长度。
SETRANGE 将对象转换成 raw 编码, 然后按 raw编码的方式执行此命令。 将对象转换成 raw 编码, 然后按 raw编码的方式执行此命令。 将字符串特定索引上的值设置为给定的字符。
GETRANGE 拷贝对象所保存的整数值, 将这个拷贝转换成字符串值, 然后取出并返回字符串指定索引上的字符。 直接取出并返回字符串指定索引上的字符。 直接取出并返回字符串指定索引上的字符。

tzset

time zone set的意思。初始化全局的时间记录

API文档

如果前面有setenv设置了timezone setenv("TZ", "GMT-8", 1);,这里就会更新。

主要更新

全局变量 说明 缺省值
__daylight 如果在TZ设置中指定夏令时时区 1则为非0值;否则为0
__timezone UTC和本地时间之间的时差,单位为秒 28800(28800秒等于8小时)
__tzname[0] TZ环境变量的时区名称的字符串值 如果TZ未设置则为空 PST
__tzname[1] 夏令时时区的字符串值; 如果TZ环境变量中忽略夏令时时区则为空PDT在上表中daylight和tzname数组的缺省值对应于”PST8PDT

在glibc中代码 time/tzset.c

void 
__tzset (void)
{
  __libc_lock_lock (tzset_lock);
  tzset_internal (1);
  if (!__use_tzfile)// 注意这里没有设置过TZ就不会进来。
    {
      /* Set `tzname'.  */ 
      __tzname[0] = (char *) tz_rules[0].name;
      __tzname[1] = (char *) tz_rules[1].name;
    }
  __libc_lock_unlock (tzset_lock);
}

实现在tzset_internal中

static void
tzset_internal (int always)
{
  static int is_initialized;
  const char *tz;

  if (is_initialized && !always)//tzset只会执行一次,初始化过就不会再执行
    return;
  is_initialized = 1;
  /* Examine the TZ environment variable.  */
  tz = getenv ("TZ");
  if (tz && *tz == '\0')
    /* User specified the empty string; use UTC explicitly.  */
    tz = "Universal";
  	
    // 处理tz 字符串,过滤掉:...
	// 检查tz字符串是不是和上次相同,相同就直接返回...
    // 新的tz是NULL 就设定成默认的TZDEFAULT "/etc/localtime"...
    //保存到old_tz...

  //  去读tzfile ,获取daylight和timezone 这里十分繁琐,也有几处好玩的
  // 1. 打开文件校验是硬编码的,We must not allow to read an arbitrary file in a  `setuid` program
  // 2. FD_CLOSEXEC
       /* Note the file is opened with cancellation in the I/O functions
       disabled and if available FD_CLOEXEC set.  */
      //f = fopen (file, "rce"); 具体解释看api文档
       //这个mode参数是glibc扩展https://www.gnu.org/software/libc/manual/html_node/Opening-Streams.html
  __tzfile_read (tz, 0, NULL);
  //后续是没读到文件的默认复制动作...  
}

setlocale

getRandomHexChars

getRandomHexChars -> getRandomBytes

生成随机数直接用

FILE *fp = fopen("/dev/urandom","r");
if (fp == NULL || fread(seed,sizeof(seed),1,fp) != 1){
    /* Revert to a weaker seed, and in this case reseed again
     * at every call.*/
     for (unsigned int j = 0; j < sizeof(seed); j++) {
            struct timeval tv;
            gettimeofday(&tv,NULL);
            pid_t pid = getpid();
            seed[j] = tv.tv_sec ^ tv.tv_usec ^ pid ^ (long)fp;
     }
 }

保证安全,如果失败就先用时间戳和pid生成一个,但是还是不安全,还是会用fopen重新生成

有个局部静态counter ,每次hash都不一样

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;

基本上看字段名字就明白啥意思了。就是指针。实现多态用的。

redis所有对外呈现的数据类型,都是dict对象来保存。这个dictType就是用来实例化各个类型对象,具体的类型在通过这个类型对象来初始化。举例,redisdb有个expire,这个dict是用来存有设定过期时间的key,

  expire= dictCreate(&expireDictType,NULL);

这样就绑定了类型,内部构造析构都用同一个函数指针就行了。

要让dict发挥多态的效果,就要增加一个类型字段,也就是dictType,通过绑定指针来实现,这就相当于元数据。或者说c++中的构造语义,编译器帮你搞or你自己手动搞,手动搞就要自己设计字段搞定


typedef struct dict {
	dictType *type;
	void *privdata;
	dictht ht[2];
	int rehashidx; /* rehashing not in progress if rehashidx == -1 */
	int iterators; /* number of iterators currently running */
} dict;

说到这,不如考虑一下hashtable的实现

_redisPanic

void _serverPanic(const char *file, int line, const char *msg, ...) {
...log...
#ifdef HAVE_BACKTRACE
    serverLog(LL_WARNING,"(forcing SIGSEGV in order to print the stack trace)");
#endif
    serverLog(LL_WARNING,"------------------------------------------------");
    *((char*)-1) = 'x';
}

指针访问-1 故意段错误,这里也有讨论

overcommit_memory

int linuxOvercommitMemoryValue(void) {
    FILE *fp = fopen("/proc/sys/vm/overcommit_memory","r");
    char buf[64];
if (!fp) return -1;
if (fgets(buf,64,fp) == NULL) {
    fclose(fp);
    return -1;
}
fclose(fp);

return atoi(buf);
}

关于这个参数,见http://linuxperf.com/?p=102

如果/proc/sys/vm/overcommit_memory被设置为0,并且配置了rdb重新功能,如果内存不足,则frok的时候会失败,如果在往redis中塞数据, 会失败,打印 MISCONF Redis is configured to save RDB snapshots, but is currently not able to persist on disk 如果/proc/sys/vm/overcommit_memory被设置为1,则不管内存够不够都会fork失败,这样会引发OOM,最终redis实例会被杀掉。

anetNonBlock(char *err, int fd )

if ((flags = fcntl(fd, F_GETFL)) == -1) 
if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) == -1)
就是这个
::fcntl(sock, F_SETFL, O_NONBLOCK | O_RDWR);

ustime

/* Return the UNIX time in microseconds */
long long ustime(void) {
    struct timeval tv;
    long long ust;
    gettimeofday(&tv, NULL);
    ust = ((long long)tv.tv_sec)*1000000;
    ust += tv.tv_usec;
    return ust;
}

/* Return the UNIX time in milliseconds */
mstime_t mstime(void) {
    return ustime()/1000;
}

写了个c++里类似的

#include <chrono>
auto [] (){ return std::chrono::duration_cast<std::chrono::microseconds>(
                   std::chrono::system_clock::now().time_since_epoch()).count();
};

redisObject

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;

一个对象就24字节了。注意type和encoding,就是redis数据结构和实际内部编码,构造也是先type对象->encoding对象初始化,析构也是判断type,判断encoding,然后删encoding对象删type对象。c++就是把这个流程隐藏了。

client

这个结构就比较复杂了。思考:阻塞命令是怎么阻塞的,为什么影响不到服务端 ->转移到客户端头上了。

阻塞的pop命令,每个客户端都会存个字典,blocking_keys 记录阻塞的key- >客户端链表

  • 如果有push变化,就会遍历一遍找到,然后发送命令,解除阻塞,将这个key放到ready_keys链表中。

  • 如果是连续的命令,怎么办 - >记录当前客户端连接的状态,阻塞就不执行了
  • 如果有事务,那不能阻塞事务,所以直接回复空。事务中用阻塞命令是错误的。

还有很多命令的细节放到命令里面讲比较合适。

client还有很多数据结构 看上去很轻巧,复杂的很。

异步事件框架

ae.c,networking这几个文件把epoll 和select kqueue封了一起。用法没差别。epoll用的是LT模式。

  • 从客户端读到大数据怎么处理 ->收到事件触发,注册在creatClient -> readQueryFromClient中 如果读出错,EAGAIN就结束,下次EPOLLIN继续处理
  • 写大数据到缓冲写不完怎么处理 -> 事件注册在addReply -> prepareClientToWrite sendReplyToClient 中, 循环写缓冲区,如果写出错 EAGAIN就结束,下次EPOLLOUT继续处理,正式结束后删掉写事件(or异常,踢掉客户端流程中会删所有事件)

仔细顺了一遍ET,LT,感觉这个用法有点像ET,没有修改事件, 仔细发现在add/delevent里。。

static int aeApiAddEvent(aeEventLoop *eventLoop, int fd, int mask) {
    aeApiState *state = eventLoop->apidata;
    struct epoll_event ee = {0}; /* avoid valgrind warning */
    /* If the fd was already monitored for some event, we need a MOD
     * operation. Otherwise we need an ADD operation. */
    int op = eventLoop->events[fd].mask == AE_NONE ?
            EPOLL_CTL_ADD : EPOLL_CTL_MOD;

    ee.events = 0;
    mask |= eventLoop->events[fd].mask; /* Merge old events */
    if (mask & AE_READABLE) ee.events |= EPOLLIN;
    if (mask & AE_WRITABLE) ee.events |= EPOLLOUT;
    ee.data.fd = fd;
    if (epoll_ctl(state->epfd,op,fd,&ee) == -1) return -1;
    return 0;
}

static void aeApiDelEvent(aeEventLoop *eventLoop, int fd, int delmask) {
    aeApiState *state = eventLoop->apidata;
    struct epoll_event ee = {0}; /* avoid valgrind warning */
    int mask = eventLoop->events[fd].mask & (~delmask);

    ee.events = 0;
    if (mask & AE_READABLE) ee.events |= EPOLLIN;
    if (mask & AE_WRITABLE) ee.events |= EPOLLOUT;
    ee.data.fd = fd;
    if (mask != AE_NONE) {
        epoll_ctl(state->epfd,EPOLL_CTL_MOD,fd,&ee);
    } else {
        /* Note, Kernel < 2.6.9 requires a non null event pointer even for
         * EPOLL_CTL_DEL. */
        epoll_ctl(state->epfd,EPOLL_CTL_DEL,fd,&ee);
    }
}

操作客户端的fd

epoll LT ET 主要区别在于LT针对EPOLLOUT事件的处理,

首先EPOLLOUT, 缓冲区可写 调用异步写 → 写满了,EAGAIN→继续等EPOLLOUT事件,这时需要在addevent中修改,加上(MOD)EPOLLOUT事件(可写)

如果没写满,结束了,修改fd,去掉EPOLLOUT事件 ,这时在delevent中删掉(OR 屏蔽掉,MOD),不然这个EPOLLOUT事件会一直触发,就得加屏蔽措施

在比如写一个echo server,或者长连接传文件 ,针对EPOLLOUT事件,写不完,就得手动epoll_ctl MOD一下,暂时屏蔽掉EPOLLOUT事件,然后如果又有了EPOLLOUT事件需要添加就在家上。针对fd得改来改去。如果是ET就没有这么麻烦设定好就行了,要么使劲读到缓冲区读完, or使劲写到缓冲区写满,事件处理完毕,等下一次事件就行。

ET LT是电子信息 信号处理的概念,触发是电平(一直触发)还是毛刺(触发一次),如果用ET,当前框架会不会丢消息?

看了一天epoll 脑袋快炸了

注意新的版本,回复事件全部放在beforeSleep中注册了,上面的分析是3.0版本。

clientsCron

定时任务,处理客户端相关

  • 踢掉超时客户端
  • 处理内存?

freememIfNeeded

策略 说明
volatile-lru 从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰
volatile-ttl 从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰
volatile-random 从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰
allkeys-lru 从数据集(server.db[i].dict)中挑选最近最少使用的数据淘汰
allkeys-random 从数据集(server.db[i].dict)中任意选择数据淘汰
no-enviction(驱逐) 禁止驱逐数据

server.maxmemory这个字段用来判断,这个字段有没有推荐设定值?

ProcessCommand

前期处理,各种条件限制,提前返回。标记上下文涉及到的flag,处理正常会调用call

call

所有的命令都走它,通过它来执行具体的命令。processCommand ....-> call -> c->cmd->proc(c)

call针对不同的客户端连接,处理不同的flag

命令表

把redis源码注释抄过来了

标识 意义
w write command (may modify the key space).
r read command (will never modify the key space).
m may increase memory usage once called. Don’t allow if out of memory.
a admin command, like SAVE or SHUTDOWN.
p Pub/Sub related command.
f force replication of this command, regardless of server.dirty.
s command not allowed in scripts.
R R: random command. SPOP
l Allow command while loading the database.
t Allow command while a slave has stale data but is not allowed to server this data. Normally no command is accepted in this condition but just a few.
S Sort command output array if called from script, so that the output is deterministic.
M Do not automatically propagate the command on MONITOR.
k Perform an implicit ASKING for this command, so the command will be accepted in cluster mode if the slot is marked as ‘importing’.
F Fast command: O(1) or O(log(N)) command that should never delay its execution as long as the kernel scheduler is giving us time. Note that commands that may trigger a DEL as a side effect (like SET) are not fast commands.

initServerConfig

初始化server参数

有几个有意思的点:

  1. cachedtime 保存一份时间,优化,因为很多对象用这个,就存一份,比直接调用time要快

  2. server.tcp_backlog设置成了521.

  3. 默认的客户端连接空闲时间是0,无限

  4. tcpkeepalive设置成了300

    …参数太多

  5. server.commands 用hashtable存了起来,在一开始已经用数组存好了。

    1. 优化点,经常使用的命令单独又存了一遍,也有可能后续读配置文件被改。 /* Fast pointers to often looked up command */
  6. 检查backlog FILE *fp = fopen(“/proc/sys/net/core/somaxconn”,”r”);

  7. daemonize 不说了,很常见的操作,fork +exit父进程退出,setsid 改权限,重定向标准fd,打开垃圾桶open(“/dev/null”, O_RDWR)

    1. 如果是守护进程得建立个pidfile,这个目录写死,在var/run/redis.pid
  8. initServer 忽略SIGHUP SIGPIPE,SIGPIPE比较常见

    1. catch TERM,INT,kill
    2. segment fault 信号,会直接死掉,OR,打印堆栈和寄存器,在sigsegvhandler中,这个实现值得一看了解怎么获取堆栈
  9. 下面就是listen和处理socket ipc了。

  10. 然后是各种初始化,LRU初始化

adjustOpenFilesLimit()

调整打开文件大小,如果小,就设置成1024

serverCron

一个定时任务,每秒执行server.hz次

里面有run_with_period宏,相当于除,降低次数

clientsCron

  • 遍历client链表删除超时的客户端
    • 大于BIG_ARG (宏,32k)以及querybuf_peak(远大于,代码写的是二倍)
    • 大于1k且不活跃 idletime>2
  • 遍历client链表缩小查询缓冲区大小
    • 如果缓冲越来越大客户端消费不过来redis就oom了

freeClient(redisClient *c)

  • 判断是不是主备要求断开,这里会有同步问题
  • querybuf
  • blockey watchkey pubsubkey
  • delete event, close event fd
  • reply buf
  • 从client链表删掉,从unblocked_clients 链表删掉
  • 再次清理主备
  • 释放字段内存,释放自己

整体交互流程

beforeSleep

  • 执行一次快速的主动过期检查,检查是否有过期的key
  • 当有客户端阻塞时,向所有从库发送ACK请求
  • unblock 在同步复制时候被阻塞的客户端
  • 尝试执行之前被阻塞客户端的命令
  • 将AOF缓冲区的内容写入到AOF文件中
  • 如果是集群,将会根据需要执行故障迁移、更新节点状态、保存node.conf 配置文件

Client发起socket连接

  • 获取客户端参数,如端口、ip地址、dbnum、socket等

  • 根据用户指定参数确定客户端处于哪种模式

  • 进入上图中step1的cliConnect 方法,cliConnect主要包含redisConnect、redisConnectUnix方法。这两个方法分别用于TCP Socket连接以及Unix Socket连接,Unix Socket用于同一主机进程间的通信。
  • 进入redisContextInit方法,redisContextInit方法用于创建一个Context结构体保存在内存中,主要用于保存客户端的一些东西,最重要的就是 write buffer和redisReader,write buffer 用于保存客户端的写入,redisReader用于保存协议解析器的一些状态。

  • 进入redisContextConnectTcp 方法,开始获取IP地址和端口用于建立连接

server接收socket连接

  • 服务器初始化建立socket监听
  • 服务器初始化创建相关连接应答处理器,通过epoll_ctl注册事件
  • 客户端初始化创建socket connect 请求
  • 服务器接受到请求,用epoll_wait方法取出事件
  • 服务器执行事件中的方法(acceptTcpHandler/acceptUnixHandler)并接受socket连接

至此客户端和服务器端的socket连接已经建立,但是此时服务器端还继续做了2件事:

  • 采用createClient方法在服务器端为客户端创建一个client,因为I/O复用所以需要为每个客户端维持一个状态。这里的client也在内存中分配了一块区域,用于保存它的一些信息,如套接字描述符、默认数据库、查询缓冲区、命令参数、认证状态、回复缓冲区等。这里提醒一下DBA同学关于client-output-buffer-limit设置,设置不恰当将会引起客户端中断。
  • 采用aeCreateFileEvent方法在服务器端创建一个文件读事件并且绑定readQueryFromClient方法。可以从图中得知,aeCreateFileEvent 调用aeApiAddEvent方法最终通过epoll_ctl 方法进行注册事件。

server接收写入

服务器端依然在进行事件循环,在客户端发来内容的时候触发,对应的文件读取事件。这就是之前创建socket连接的时候建立的事件,该事件绑定的方法是readQueryFromClient 。

  • 在readQueryFromClient方法中从服务器端套接字描述符中读取客户端的内容到服务器端初始化client的查询缓冲中,主要方法如下:

  • 交给processInputBuffer处理,processInputBuffer 主要包含两个方法,processInlineBuffer和processCommand。processInlineBuffer方法用于采用redis协议解析客户端内容并生成对应的命令并传给processCommand 方法,processCommand方法则用于执行该命令

  • processCommand方法会以下操作:
    • 处理是否为quit命令。
    • 对命令语法及参数会进行检查。
    • 这里如果采取认证也会检查认证信息。
    • 如果Redis为集群模式,这里将进行hash计算key所属slot并进行转向操作。
    • 如果设置最大内存,那么检查内存是否超过限制,如果超过限制会根据相应的内存策略删除符合条件的键来释放内存
    • 如果这是一个主服务器,并且这个服务器之前执行bgsave发生了错误,那么不执行命令
    • 如果min-slaves-to-write开启,如果没有足够多的从服务器将不会执行命令 注:所以DBA在此的设置非常重要,建议不是特殊场景不要设置。
    • 如果这个服务器是一个只读从库的话,拒绝写入命令。
    • 在订阅于发布模式的上下文中,只能执行订阅和退订相关的命令
    • 当这个服务器是从库,master_link down 并且slave-serve-stale-data 为 no 只允许info 和slaveof命令
    • 如果服务器正在载入数据到数据库,那么只执行带有REDIS_CMD_LOADING标识的命令
    • lua脚本超时,只允许执行限定的操作,比如shutdown、script kill 等
  • 最后进入call方法, 决定调用具体的命令

  • setCommand方法,setCommand方法会调用setGenericCommand方法,该方法首先会判断该key是否已经过期,最后调用setKey方法。

    这里需要说明一点的是,通过以上的分析。redis的key过期包括主动检测以及被动监测

    主动监测
    • 在beforeSleep方法中执行key快速过期检查,检查模式为ACTIVE_EXPIRE_CYCLE_FAST。周期为每个事件执行完成时间到下一次事件循环开始
    • 在serverCron方法中执行key过期检查,这是key过期检查主要的地方,检查模式为ACTIVE_EXPIRE_CYCLE_SLOW,* serverCron方法执行周期为1秒钟执行server.hz 次,hz默认为10,所以约100ms执行一次。hz设置越大过期键删除就越精准,但是cpu使用率会越高,这里我们线上redis采用的默认值。redis主要是在这个方法里删除大部分的过期键。
    被动监测
    • 使用内存超过最大内存被迫根据相应的内存策略删除符合条件的key。
    • 在key写入之前进行被动检查,检查key是否过期,过期就进行删除。
    • 还有一种不友好的方式,就是randomkey命令,该命令随机从redis获取键,每次获取到键的时候会检查该键是否过期。 以上主要是让运维的同学更加清楚redis的key过期删除机制。
  • 进入setKey方法,setKey方法最终会调用dbAdd方法,其实最终就是将该键值对存入服务器端维护的一个字典中,该字典是在服务器初始化的时候创建,用于存储服务器的相关信息,其中包括各种数据类型的键值存储。完成了写入方法时候,此时服务器端会给客户端返回结果。

  • 进入prepareClientToWrite方法然后通过调用_addReplyToBuffer方法将返回结果写入到outbuf中(客户端连接时创建的client)

  • 通过aeCreateFileEvent方法注册文件写事件并绑定sendReplyToClient方法

server返回写入结果

checkTcpBacklogSettings

/* Check that server.tcp_backlog can be actually enforced in Linux according to the value of /proc/sys/net/core/somaxconn, or warn about it. */

listenToPort 是直接调用net接口了。后面再说吧

prepareForShutdown

  • 通知system fd关机
  • 干掉lua debugger
  • 干掉rdb子进程。
    • 同步生成rdb,最后的快照,安息
  • 干掉module子进程
  • 干掉aof子进程
    • flush
  • unlink pid文件
  • 关闭socket

info信息构造

bytesToHuman

/* Convert an amount of bytes into a human readable string in the form
 * of 100B, 2G, 100M, 4K, and so forth. */
void bytesToHuman(char *s, unsigned long long n) {
    double d;

    if (n < 1024) {
        /* Bytes */
        sprintf(s,"%lluB",n);
    } else if (n < (1024*1024)) {
        d = (double)n/(1024);
        sprintf(s,"%.2fK",d);
    } else if (n < (1024LL*1024*1024)) {
        d = (double)n/(1024*1024);
        sprintf(s,"%.2fM",d);
    } else if (n < (1024LL*1024*1024*1024)) {
        d = (double)n/(1024LL*1024*1024);
        sprintf(s,"%.2fG",d);
    } else if (n < (1024LL*1024*1024*1024*1024)) {
        d = (double)n/(1024LL*1024*1024*1024);
        sprintf(s,"%.2fT",d);
    } else if (n < (1024LL*1024*1024*1024*1024*1024)) {
        d = (double)n/(1024LL*1024*1024*1024*1024);
        sprintf(s,"%.2fP",d);
    } else {
        /* Let's hope we never need this */
        sprintf(s,"%lluB",n);
    }
}

rdb文件是内存的一份快照

rdbLoad

代码很短

if ((fp = fopen(filename,"r")) == NULL) return C_ERR;
startLoadingFile(fp, filename,rdbflags);
rioInitWithFile(&rdb,fp);//绑定write read flush 等系统api
retval = rdbLoadRio(&rdb,rdbflags,rsi); //正式读
fclose(fp);
stopLoading(retval==C_OK);

rdbLoadRio 处理一些不同类型的数据,像文件头,selectdb号,moduleID等等,处理完之后会进入真正的读string object。kv都帮到一起了,rdb加载就能直接解出来,主要逻辑

//while(1) 
        if ((key = rdbLoadStringObject(rdb)) == NULL) goto eoferr;
        /* Read value */
        if ((val = rdbLoadObject(type,rdb,key)) == NULL) {
            decrRefCount(key);
            goto eoferr;
        }

rdbLoadObject解出来每一个object,拿到value。这个函数会根据每种类型来解。如果是list之类的还要在内部继续遍历继续解。代码500行,不抄了

具体的格式见参考链接,我直接抄过来

----------------------------# RDB文件是二进制的,所以并不存在回车换行来分隔一行一行.
52 45 44 49 53              # 以字符串 "REDIS" 开头
30 30 30 33                 # RDB 的版本号,大端存储,比如左边这个表示版本号为0003
----------------------------
FE 00                       # FE = FE表示数据库编号,Redis支持多个库,以数字编号,这里00表示第0个数据库
----------------------------# Key-Value 对存储开始了
FD $length-encoding         # FD 表示过期时间,过期时间是用 length encoding 编码存储的,后面会讲到
$value-type                 # 1 个字节用于表示value的类型,比如set,hash,list,zset等
$string-encoded-key         # Key 值,通过string encoding 编码,同样后面会讲到
$encoded-value              # Value值,根据不同的Value类型采用不同的编码方式
----------------------------
FC $length-encoding         # FC 表示毫秒级的过期时间,后面的具体时间用length encoding编码存储
$value-type                 # 同上,也是一个字节的value类型
$string-encoded-key         # 同样是以 string encoding 编码的 Key值
$encoded-value              # 同样是以对应的数据类型编码的 Value 值
----------------------------
$value-type                 # 下面是没有过期时间设置的 Key-Value对,为防止冲突,数据类型不会以 FD, FC, FE, FF 开头
$string-encoded-key
$encoded-value
----------------------------
FE $length-encoding         # 下一个库开始,库的编号用 length encoding 编码
----------------------------
...                         # 继续存储这个数据库的 Key-Value 对
FF                          ## FF:RDB文件结束的标志

Magic Number

第一行就不用讲了,REDIS字符串用于标识是Redis的RDB文件

版本号

用了4个字节存储版本号,以大端(big endian)方式存储和读取

数据库编号

以一个字节的0xFE开头,后面存储数据库的具体编号,数据库的编号是一个数字,通过 “Length Encoding” 方式编码存储,“Length Encoding” 我们后面会讲到。

Key-Value值对

值对包括下面四个部分 1. Key 过期时间,这一项是可有可无的 2. 一个字节表示value的类型 3. Key的值,Key都是字符串,通过 “Redis String Encoding” 来保存 4. Value的值,通过 “Redis Value Encoding” 来根据不同的数据类型做不同的存储

Key过期时间

过期时间由 0xFD 或 0xFC开头用于标识,分别表示秒级的过期时间和毫秒级的过期时间,后面的具体时间是一个UNIX时间戳,秒级或毫秒级的。具体时间戳的值通过“Redis Length Encoding” 编码存储。在导入RDB文件的过程中,会通过过期时间判断是否已过期并需要忽略。

Value类型

Value类型用一个字节进行存储,目前包括以下一些值:

  • 0 = “String Encoding”
  • 1 = “List Encoding”
  • 2 = “Set Encoding”
  • 3 = “Sorted Set Encoding”
  • 4 = “Hash Encoding”
  • 9 = “Zipmap Encoding”
  • 10 = “Ziplist Encoding”
  • 11 = “Intset Encoding”
  • 12 = “Sorted Set in Ziplist Encoding”

Key

Key值就是简单的 “String Encoding” 编码,具体可以看后面的描述

Value

上面列举了Value的9种类型,实际上可以分为三大类

  • type = 0, 简单字符串
  • type 为 9, 10, 11 或 12, value字符串在读取出来后需要先解压
  • type 为 1, 2, 3 或 4, value是字符串序列,这一系列的字符串用于构建list,set,hash 和 zset 结构

Length Encoding

上面说了很多 Length Encoding ,现在就为大家讲解。可能你会说,长度用一个int存储不就行了吗?但是,通常我们使用到的长度可能都并不大,一个int 4个字节是否有点浪费呢。所以Redis采用了变长编码的方法,将不同大小的数字编码成不同的长度。

  1. 首先在读取长度时,会读一个字节的数据,其中前两位用于进行变长编码的判断
  2. 如果前两位是 0 0,那么下面剩下的 6位就表示具体长度
  3. 如果前两位是 0 1,那么会再读取一个字节的数据,加上前面剩下的6位,共14位用于表示具体长度
  4. 如果前两位是 1 0,那么剩下的 6位就被废弃了,取而代之的是再读取后面的4 个字节用于表示具体长度
  5. 如果前两位是 1 1,那么下面的应该是一个特殊编码,剩下的 6位用于标识特殊编码的种类。特殊编码主要用于将数字存成字符串,或者编码后的字符串。具体见 “String Encoding”

这样做有什么好处呢,实际就是节约空间:

  1. 0 – 63的数字只需要一个字节进行存储
  2. 而64 – 16383 的数字只需要两个字节进行存储
  3. 16383 - 2^32 -1 的数字只需要用5个字节(1个字节的标识加4个字节的值)进行存储

String Encoding

Redis的 String Encoding 是二进制安全的,也就是说他没有任何特殊分隔符用于分隔各个值,你可以在里面存储任何东西。它就是一串字节码。 下面是 String Encoding 的三种类型

  1. 长度编码的字符串
  2. 数字替代字符串:8位,16位或者32位的数字
  3. LZF 压缩的字符串

长度编码字符串

长度编码字符串是最简单的一种类型,它由两部分组成,一部分是用 “Length Encoding” 编码的字符串长度,第二部分是具体的字节码。

数字替代字符串

上面说到过 Length Encoding 的特殊编码,就在这里用上了。所以数字替代字符串是以 1 1 开头的,然后读取这个字节剩下的6 位,根据不同的值标识不同的数字类型:

  • 0 表示下面是一个8 位的数字
  • 1 表示下面是一个16 位的数字
  • 2 表示下面是一个32 位的数字

LZF压缩字符串

和数据替代字符串一样,它也是以1 1 开头的,然后剩下的6 位如果值为4,那么就表示它是一个压缩字符串。压缩字符串解析规则如下:

  1. 首先按 Length Encoding 规则读取压缩长度 clen
  2. 然后按 Length Encoding 规则读取非压缩长度
  3. 再读取第二个 clen
  4. 获取到上面的三个信息后,再通过LZF算法解码后面clen长度的字节码

List Encoding

Redis List 结构在RDB文件中的存储,是依次存储List中的各个元素的。其结构如下:

  1. 首先按 Length Encoding 读取这个List 的长度 size
  2. 然后读取 size个 String Encoding的值
  3. 然后再用这些读到的 size 个值重新构建 List就完成了

Set Encoding

Set结构和List结构一样,也是依次存储各个元素的

Sorted Set Encoding

也是和list类似的,注意double有两种保存方法,做了优化,所以读取也要做区分

Hash Encoding

  1. 首先按 Length Encoding 读出hash 结构的大小 size
  2. 然后读取2×size 个 String Encoding的字符串(因为一个hash项包括key和value两项)
  3. 将上面读取到的2×size 个字符串解析为hash 和key 和 value
  4. 然后将上面的key value对存储到hash结构中

注意这个整理,现在6.0版本是不准的,多了module和stream。

其中stream的处理方法是类似的,也是遍历

rdbSaveRio

逐个保存,所有的kv对都变成string

核心逻辑

for(all db)
  for(each type)
      rdbSaveKeyValuePair()

rdb文件整体都是大端的。这也算方便跨平台吧

6.0带来的一大改动就是多线程IO了。

IOThreadMain

多线程IO读。提高并发。核心代码

        listRewind(io_threads_list[id],&li);
        while((ln = listNext(&li))) {
            client *c = listNodeValue(ln);
            if (io_threads_op == IO_THREADS_OP_WRITE) {
                writeToClient(c,0);
            } else if (io_threads_op == IO_THREADS_OP_READ) {
                readQueryFromClient(c->conn);
            } else {
                serverPanic("io_threads_op value is unknown");
            }
        }

readQueryFromClient

核心代码没什么好说的

    nread = connRead(c->conn, c->querybuf+qblen, readlen);
    if (nread == -1) {
        if (connGetState(conn) == CONN_STATE_CONNECTED) {
            return;
        } else {
            serverLog(LL_VERBOSE, "Reading from client: %s",connGetLastError(c->conn));
            freeClientAsync(c);
            return;
        }
    } else if (nread == 0) {
        serverLog(LL_VERBOSE, "Client closed connection");
        freeClientAsync(c);
        return;
    } else if (c->flags & CLIENT_MASTER) {
        /* Append the query buffer to the pending (not applied) buffer
         * of the master. We'll use this buffer later in order to have a
         * copy of the string applied by the last command executed. */
        c->pending_querybuf = sdscatlen(c->pending_querybuf,
                                        c->querybuf+qblen,nread);
    }

这里读完,后面是processInputBufferAndReplicate->processInputBuffer 解析完命令等执行

从客户链接读数据,几个优化点

  • 内存预分配,和redis业务相关。不讲
  • postponeClientRead 如果IO线程没读完,接着读,别处理

createClient

  • 绑定handler等等
    • noblock, tcpnodelay, keepalive
  • 上线文设定,buf,db,cmd,auth等等
  • 对应freeclient
    • 各种释放缓存,unwatch unpubsub
    • unlinkClient 处理handler,关掉fd,如果有pending,扔掉

prepareClientToWrite

  • 如果不能写,需要把client标记成pending_write,等调度

各种accept略过

processInputBufferAndReplicate

各种buffer处理总入口,processInputBuffer的一层封装

redis支持两种协议,redis protocol,或者inline,按行读

processInputBuffer在检查各种flag之后,根据字符串开头是不是array来判断是processMultibulkBuffer还是processInlineBuffer

Client相关的帮助函数这里省略

stream

整体架构

  • 每个消息都有一个唯一的ID和对应的内容
  • 每个Stream都有唯一的名称,Redis key

  • 每个Stream都可以挂多个消费组,每个消费组会有个游标last_delivered_id在Stream数组之上往前移动,表示当前消费组已经消费到哪条消息了。每个消费组都有一个Stream内唯一的名称,消费组不会自动创建,它需要单独的指令xgroup create进行创建,需要指定从Stream的某个消息ID开始消费,这个ID用来初始化last_delivered_id变量。

  • 每个消费组(Consumer Group)的状态都是独立的,相互不受影响。也就是说同一份Stream内部的消息会被每个消费组都消费到。

  • 同一个消费组(Consumer Group)可以挂接多个消费者(Consumer),这些消费者之间是竞争关系,任意一个消费者读取了消息都会使游标last_delivered_id往前移动。每个消费者者有一个组内唯一名称。

  • 消费者(Consumer)内部会有个状态变量pending_ids,它记录了当前已经被客户端读取的消息,但是还没有ack。如果客户端没有ack,这个变量里面的消息ID会越来越多,一旦某个消息被ack,它就开始减少。这个pending_ids变量在Redis官方被称之为PEL,也就是Pending Entries List,这是一个很核心的数据结构,它用来确保客户端至少消费了消息一次,而不会在网络传输的中途丢失了没处理。

简单说

stream key 每条消息存储并生成streamid-seq,然后id和消费组挂钩,消费组内部有游标和消费者,消费者和消费组挂钩。是串行消费游标信息。

如果把stream编码成kv需要怎么做?

首先,stream key本身维护一组信息,需要知道最后一个streamid,需要知道stream个数,本身就算是一个元数据

其次,stream key需要和子字段,streamid编码,方便区分是谁的key的streamID下的字段

消费组,这也算是一个元数据,需要保存最后一个消费到的id,为了快速定位消费组,可以保存到stream key的value里。

然后就是消费者了,这个就是PEL,消费者应该没有必要单独存一个表 。


ref

  • redis设计与实现试读内容,基本上一大半。还有源码注释做的不错。我基本上照着注释写的。http://redisbook.com/ redisbook 讲的太详细了,huangz还给了个阅读建议。我重写主要是落实一下脑海中的概念,便于后续翻阅。redis代码走读的东西太多了,我的方向偏向于改动源码需要了解的东西。
  • huangz给的建议,如何阅读redis代码http://blog.huangz.me/diary/2014/how-to-read-redis-source-code.html
  • 大部分都抄自这里 http://www.zbdba.com/2018/06/23/深入浅出-redis-client-server交互流程/
  • https://ningyu1.github.io/site/post/34-redis-rdb/
  • https://mp.weixin.qq.com/s?__biz=MzAwMDU1MTE1OQ==&mid=2653549949&idx=1&sn=7f6c4cf8642478574718ed0f8cf61409&chksm=813a64e5b64dedf357cef4e2894e33a75e3ae51575a4e3211c1da23008ef79173962e9a83c73&mpshare=1&scene=1&srcid=0717FcpVc16q9rNa0yfF78FU#rd
  • http://chenzhenianqing.com/articles/1622.html
  • http://chenzhenianqing.com/articles/1649.html
Read More

如何从c++导出c接口

最近一周做一个第三方c++库糊c wrapper的工作,干的太慢了,一周啊没搞定。我原本的计划是打算这一周就把这个活搞定,结果连10%都没做完。高估自己能力了。

前两天主要是搞定原来的库,去掉对其他库的依赖(锁用的还是pthread api,我给去掉了),剩下的时间,思考怎么把cpp导出c接口。。这也太闹心了。学习了一下rocksdb导出的实现。

结构体封装

类的字段全都要导出接口,天哪。

参数处理

首先说下我个人的暴力替换法

  • std::vector<T> 改成 T*

  • std::vector<std::string>(&) 直接改成 char**

  • std::vector<std::string>* 直接变成char *** 感觉已经招架不住了

返回值

  • 返回指针还是传入指针or传入buffer指针?buffer指针咋设置合适,or 传入指针在内部分配?不如直接返回指针

  • 返回的话管理内存就得交给调用方?

Rocksdb做法

char* rocksdb_get(
    rocksdb_t* db,
    const rocksdb_readoptions_t* options,
    const char* key, size_t keylen,
    size_t* vallen,
    char** errptr) {
  char* result = nullptr;
  std::string tmp;
  Status s = db->rep->Get(options->rep, Slice(key, keylen), &tmp);
  if (s.ok()) {
    *vallen = tmp.size();
    result = CopyString(tmp);
  } else {
    *vallen = 0;
    if (!s.IsNotFound()) {
      SaveError(errptr, s);
    }
  }
  return result;
}

返回指针,传入长度,这只是一个kv,如果是多个呢,如果是复杂的数据结构呢?

错误处理

Rocksdb 是怎么把错误导出的。传入错误指针,saveerror

static bool SaveError(char** errptr, const Status& s) {
  assert(errptr != nullptr);
  if (s.ok()) {
    return false;
  } else if (*errptr == nullptr) {
    *errptr = strdup(s.ToString().c_str());
  } else {
    // This is a bug if *errptr is not created by malloc()
    free(*errptr);
    *errptr = strdup(s.ToString().c_str());
  }
  return true;
}

char* err = NULL; 传入&err (为啥二级指针?) err本身是个字符串,传字符串地址。

编译问题

gcc编译c程序,链接c++的静态库,需要-lstdc++,不然会有连接错误。或者用g++编译,默认带stdc++ runtime 其实这背后有更恶心的问题,如果你链接一个静态库,这个静态库依赖其他静态库,这里头的依赖关系就很恶心了。 最近看pika,主程序依赖rocksdb和blackwidow,blackwidow也依赖rocksdb,链接都是静态。就很混乱。

一个示例

[root@host]# tree
|-- libp.so
|-- libstaticp.a
|-- p.cpp
|-- p.h
|-- p.o
|-- p_test.c
|-- ptest_d
`-- ptest_s

p.cpp

#include "p.h"
#include <iostream>
using namespace std;
extern "C"{
void print_int(int a){
    cout<<a<<endl;
}
}

p.h

#ifndef _P_
#define _P_
#ifdef __cplusplus 
extern "C" {
#endif
void print_int(int a);
#ifdef __cplusplus
}
#endif
#endif

p_test.c

#include "p.h"
int main(){
    print_int(111);
    return 0;
}
  • 需要导入当前目录到环境中,方便ld
    export LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH
    
  • 编译动态库
    g++ p.cpp -fPIC -shared -o libp.so
    
  • 编译静态库
    g++ -c p.cpp
    ar cqs libstaticp.a p.o
    
  • 编译程序
    gcc -o ptest_d p_test.c -L. -lp					#ok
    g++ -o ptest_s p_test.c -L. -lstaticp			#ok
    gcc -o ptest_s p_test.c -L. -lstaticp -lstdc++	#ok
    gcc -o ptest_s p_test.c -L. -lstaticp			#not ok
    ./libstaticp.a(p.o): In function `print_int':
    p.cpp:(.text+0x11): undefined reference to `std::cout'
    p.cpp:(.text+0x16): undefined reference to `std::ostream::operator<<(int)'
    p.cpp:(.text+0x1b): undefined reference to `std::basic_ostream<char, std::char_traits<char> >& std::endl<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&)'
    p.cpp:(.text+0x23): undefined reference to `std::ostream::operator<<(std::ostream& (*)(std::ostream&))'
    ./libstaticp.a(p.o): In function `__static_initialization_and_destruction_0(int, int)':
    p.cpp:(.text+0x4c): undefined reference to `std::ios_base::Init::Init()'
    p.cpp:(.text+0x5b): undefined reference to `std::ios_base::Init::~Init()'
    collect2: error: ld returned 1 exit status
    

    reference

  • 参考这个 写的例子 https://blog.csdn.net/surgewong/article/details/39236707
  • 注意p.h中需要有__cplusplus marco guard, 因为extern “C” 不是c的内容,会报错 https://stackoverflow.com/questions/10307762/error-expected-before-string-constant
  • https://arne-mertz.de/2018/10/calling-cpp-code-from-c-with-extern-c/ 这个链接说了extern “C”在c++中的风格干净的用法,注意,在c中还是用不了。
Read More

基本的Linux内核参数的优化

Linux内核参数的优化

摘自深入理解Nginx 第一章

由于默认的Linux内核参数考虑的是最通用的场景,这明显不符合用于支持高并发访问 的Web服务器的定义,所以需要修改Linux内核参数,使得Nginx可以拥有更高的性能。 在优化内核时,可以做的事情很多,不过,我们通常会根据业务特点来进行调整,当 Nginx作为静态Web内容服务器、反向代理服务器或是提供图片缩略图功能(实时压缩图片) 的服务器时,其内核参数的调整都是不同的。这里只针对最通用的、使Nginx支持更多并发 请求的TCP网络参数做简单说明。 首先,需要修改/etc/sysctl.conf来更改内核参数。例如,最常用的配置:

fs.file-max = 999999
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_keepalive_time = 600
net.ipv4.tcp_fin_timeout = 30
net.ipv4.tcp_max_tw_buckets = 5000
net.ipv4.ip_local_port_range = 1024 61000
net.ipv4.tcp_rmem = 4096 32768 262142
net.ipv4.tcp_wmem = 4096 32768 262142
net.core.netdev_max_backlog = 8096
net.core.rmem_default = 262144
net.core.wmem_default = 262144
net.core.rmem_max = 2097152
net.core.wmem_max = 2097152
net.ipv4.tcp_syncookies = 1
net.ipv4.tcp_max_syn.backlog=1024

然后执行sysctl-p命令,使上述修改生效。 上面的参数意义解释如下:

  • file-max:这个参数表示进程(比如一个worker进程)可以同时打开的最大句柄数,这 个参数直接限制最大并发连接数,需根据实际情况配置。
  • tcp_tw_reuse:这个参数设置为1,表示允许将TIME-WAIT状态的socket重新用于新的 TCP连接,这对于服务器来说很有意义,因为服务器上总会有大量TIME-WAIT状态的连接。
  • tcp_keepalive_time:这个参数表示当keepalive启用时,TCP发送keepalive消息的频度。 默认是2小时,若将其设置得小一些,可以更快地清理无效的连接。
  • tcp_fin_timeout:这个参数表示当服务器主动关闭连接时,socket保持在FIN-WAIT-2状 态的最大时间。
  • tcp_max_tw_buckets:这个参数表示操作系统允许TIME_WAIT套接字数量的最大值, 如果超过这个数字,TIME_WAIT套接字将立刻被清除并打印警告信息。该参数默认为 180000,过多的TIME_WAIT套接字会使Web服务器变慢。
  • tcp_max_syn_backlog:这个参数表示TCP三次握手建立阶段接收SYN请求队列的最大 长度,默认为1024,将其设置得大一些可以使出现Nginx繁忙来不及accept新连接的情况时, Linux不至于丢失客户端发起的连接请求。
  • ip_local_port_range:这个参数定义了在UDP和TCP连接中本地(不包括连接的远端) 端口的取值范围。
  • net.ipv4.tcp_rmem:这个参数定义了TCP接收缓存(用于TCP接收滑动窗口)的最小 值、默认值、最大值。
  • net.ipv4.tcp_wmem:这个参数定义了TCP发送缓存(用于TCP发送滑动窗口)的最小 值、默认值、最大值。
  • netdev_max_backlog:当网卡接收数据包的速度大于内核处理的速度时,会有一个队列 保存这些数据包。这个参数表示该队列的最大值。
  • rmem_default:这个参数表示内核套接字接收缓存区默认的大小。
  • wmem_default:这个参数表示内核套接字发送缓存区默认的大小。
  • rmem_max:这个参数表示内核套接字接收缓存区的最大大小。
  • wmem_max:这个参数表示内核套接字发送缓存区的最大大小。
    • 滑动窗口的大小与套接字缓存区会在一定程度上影响并发连接的数目。每个 TCP连接都会为维护TCP滑动窗口而消耗内存,这个窗口会根据服务器的处理速度收缩或扩 张。 参数wmem_max的设置,需要平衡物理内存的总大小、Nginx并发处理的最大连接数量 (由nginx.conf中的worker_processes和worker_connections参数决定)而确定。当然,如果仅仅 为了提高并发量使服务器不出现Out Of Memory问题而去降低滑动窗口大小,那么并不合 适,因为滑动窗口过小会影响大数据量的传输速度。
    • rmem_default、wmem_default、rmem_max、wmem_max这4个参数的设置需要根据我们的业务特性以及实际的硬件成本来综合考虑。
  • tcp_syncookies:该参数与性能无关,用于解决TCP的SYN攻击。
Read More


codewars刷题常用代码片

数学

快速算一个int有几位数

int sizeofint(int x) {
  const static int table[] = { 9, 99, 999, 9999, 99999, 999999, 9999999,    
            99999999, 999999999, std::numeric_limits<int>::max()};
  for (int i = 0; i<10; i++)    
    if (x <= table[i])    
      return i + 1;  
}

字符串操作

拆分

std::vector<std::string> split(const std::string& s, char delimiter)
{
   std::vector<std::string> tokens;
   std::string token;
   std::istringstream tokenStream(s);
   while (std::getline(tokenStream, token, delimiter))
   {
      tokens.push_back(token);
   }
   return tokens;
}

判断标点符号 std::ispunct,一可以自己写个数组暴力查表

Range for循环判定结尾, 这种写法注意可能vector有重复导致判断错误

for (auto v : vec) {
		if (v != vec.back()) {//...}
}

参考

  • geekforgeeks 问题列表 https://g4g.apachecn.org/#/docs/a-boolean-matrix-question

Read More

链表以及TAILQ

#链表以及TAILQ

最近重新看c语言的东西,对数据结构还是不熟悉

从链表说起,一个基本的链表,就是由一个个node组成

typedef struct node{
	node *next;
	node *prev;
	void* data;
}node;

而操作链表表头,引用整个链表,就有很多方法

比如,表头就是node,

typedef struct node linklist;

相关的操作函数比如构造函数就都以node做入参

linklist* createList();
void listAdd(linklist* l, node* v);

虽然写法不同但实际上是同一个类型,这种链表的缺点在于,比如合并两个链表,或者对整个链表排序都是leetcode上的题目

对于头结点,同时也是提领引用整个链表的入口,操作会很麻烦,这时候,就需要一个dummyhead,作为head的前节点。那么如何避免这个问题呢?

对linklist结构进行改造,通常就是封一层,比如

typedef struct linklist {
    node *head;
    ...其他函数指针或者记录信息的字段
}linklist;

这基本上就是c++list封装的办法,也是c中常见的封装方法。就是内存分配需要两次比较麻烦,c++为了避免手动malloc,加上了构造析构的语义。(我好像在Bjarne Stroustrup 那本自传书籍里看到过)

然后说到tailq

typedef struct linklist {
    node *head;
    node *tail;
} linklist;

就是这样了。增加一个记录结尾的字段。这个结构在libevent redis中都有(redis基本上把libevent组件抄了一遍,抽出来组装的),最早追述应该是内核中的TAILQ吧。整体就比原来的双端列表多了一个指针的开销,说是队列,实际上双端队列本身也可以实现队列,就是访问最后一个节点需要间接的操作一下,而这可能缓存不友好?不然为啥会有这么个数据结构。

数据结构还是不直观,上个图来说明一下,图用的libevent

img

Read More


pika 简单分析

[toc]

why

梳理思路,省着忘了

0. trivial

  • 全局PikaServer对象,所有变量都用读写锁保护了。真的有必要吗。
  • 模块拆分 (pika主程序,pink网络模块,blackwidow编码模块,slash公共组件(这个公共组件模块很坑爹),glog),底层模块又互相依赖。挺头疼的(需要画个图)
  • 利用多线程优势,类似memcache用libevent哪种用法。主事件循环处理io事件,写pipe通知子线程搞定

  • redis协议分析我以为得放在pika主程序中,结果没想到在pink里。糊一起了。之前还好奇,翻pika代码没发现redis代码,难道解析redis居然没用到redis源码自己搞的,结果在pink里。

1. 整体架构

一图胜千言 很多redis over rocksdb的实现都是在编码上有各种异同,比如ssdb ardb之类的,pika怎么做的?上图

img

复杂数据结构 set zset hash 是分成元数据和实体数据来做的。(大家都抄linux)

pika编译踩坑

  1. 运行测试,二进制文件运行几次测试用例就挂了,应该是不支持wait直接崩了

下面是log记录。我个人猜测是运行环境的问题,准备从头编译

bash pikatests.sh wait

ERROR:path : ./tests/tmp/redis.conf.29436.2
-----------Pika server 3.0.5 ----------
-----------Pika config list----------
1 thread-num 1
2 sync-thread-num 6
3 sync-buffer-size 10
4 log-path ./log/
5 loglevel info
6 db-path ./db/
7 write-buffer-size 268435456
8 timeout 60
9 requirepass
10 masterauth
11 userpass
12 userblacklist
13 dump-prefix
14 dump-path ./dump/
15 dump-expire 0
16 pidfile ./pika.pid
17 maxclients 20000
18 target-file-size-base 20971520
19 expire-logs-days 7
20 expire-logs-nums 10
21 root-connection-num 2
22 slowlog-write-errorlog no
23 slowlog-log-slower-than 10000
24 slowlog-max-len 128
25 slave-read-only yes
26 db-sync-path ./dbsync/
27 db-sync-speed -1
28 slave-priority 100
29 server-id 1
30 double-master-ip
31 double-master-port
32 double-master-server-id
33 write-binlog yes
34 binlog-file-size 104857600
35 identify-binlog-type new
36 max-cache-statistic-keys 0
37 small-compaction-threshold 5000
38 compression snappy
39 max-background-flushes 1
40 max-background-compactions 2
41 max-cache-files 5000
42 max-bytes-for-level-multiplier 10
-----------Pika config end----------
WARNING: Logging before InitGoogleLogging() is written to STDERR
W1217 17:22:48.644249 29438 pika.cc:167] your 'limit -n ' of 1024 is not enough for Redis to start. pika have successfully reconfig it to 25000
I1217 17:22:48.644497 29438 pika.cc:184] Server at: ./tests/tmp/redis.conf.29436.2
I1217 17:22:48.644691 29438 pika_server.cc:192] Using Networker Interface: eth0
I1217 17:22:48.644783 29438 pika_server.cc:235] host: 192.168.1.104 port: 0
I1217 17:22:48.644820 29438 pika_server.cc:68] Prepare Blackwidow DB...
I1217 17:22:48.776921 29438 pika_server.cc:73] DB Success
I1217 17:22:48.776942 29438 pika_server.cc:89] Worker queue limit is 20100
I1217 17:22:48.777190 29438 pika_binlog.cc:103] Binlog: Manifest file not exist, we create a new one.
I1217 17:22:48.777328 29438 pika_server.cc:109] double recv info: filenum 0 offset 0
F1217 17:22:48.779913 29438 pika_server.cc:284] Start BinlogReceiver Error: 1: bind port 1000 conflict, Listen on this port to handle the data sent by the Binlog Sender
*** Check failure stack trace: ***
@ 0x83c5aa google::LogMessage::Fail()
@ 0x83e2ff google::LogMessage::SendToLog()
@ 0x83c1f8 google::LogMessage::Flush()
@ 0x83ec2e google::LogMessageFatal::~LogMessageFatal()
@ 0x5f5725 PikaServer::Start()
@ 0x423db4 main
@ 0x7fe733418bb5 __libc_start_main
@ 0x58ac71 (unknown)
[1/1 done]: unit/wait (5 seconds)

The End

Execution time of different units:
5 seconds - unit/wait

\2. 源码编译由于我的服务器限制不能访问外网,自己手动git clone然后导回服务器(这里又有一个坑)(win7没有wsl,只能用服务器搞)

然后需要依赖子模块

git submodule init
git submodule update

重新在windows上更新后传回

分别编译,

子模块编译失败,其中有文件格式不对的问题 手动转dos2unix

然后才发现tortoisegit有个自动添加换行符的选项,默认打勾。所以基本上涉及到的shell文件都得手动dos2unix

img

我的服务器只有4.8.3,所以编译rocksdb需要c++11 需要修改Makefile(其实是detect脚本没加权限的问题)

img

编译glog失败

./configure make失败 提示没有什么repo

搜到了类似的问题,https://stackoverflow.com/questions/18839857/deps-po-no-such-file-or-directory-compiler-error

按照提示 (需要安装libtool)

autoreconf -if

提示

.ibtoolize: AC_CONFIG_MACRO_DIR([m4]) conflicts with ACLOCAL_AMFLAGS=-I m4 

结果还是gitwindows加换行符导致的。 That darn “libtoolize: AC_CONFIG_MACRO_DIR([m4]) conflicts with ACLOCAL_AMFLAGS=-I m4” error Is caused by using CRLFs in Makefile.am. “m4" != "m4" and thus the libtoolize script will produce an error. If you're using git, I strongly advise adding a .gitattributes file with the following: *.sh -crlf *.ac -crlf *.am -crlf http://pete.akeo.ie/2010/12/that-darn-libtoolize-acconfigmacrodirm4.html

最终编译过了,运行提示找不到glog 还要手动ldconfig一下

echo "/usr/local/lib" >> /etc/ld.so.conf
ldconfig

还有一种解决方案,export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/lib

1552045590285

总算能运行测试了。

redis 中runtest脚本不能直接用,但是测试用例可以直接拿过来用,support/server.tcl文件改下start_server函数就行(pika已经改过了,直接用那个文件就好了)

测试,类似下面,改改应该就行了

#!/bin/bash
for f in ${PWD}/tests/unit/*;do
    filename=${f##*/}
    tclsh tests/test_helper.tcl --clients 1 --single unit/${filename%.*}
done

for f in ${PWD}/tests/unit/type/*;do
    filename=${f##*/}
    tclsh tests/test_helper.tcl --clients 5 --single  unit/type/${filename%.*}
    #tclsh tests/test_helper.tcl --clients 1 --single unit/type/${filename%.*}
done

附调用图一张。组件太多,确实找不到在哪。

遇到的问题

  • valgrind不能attach到daemon进程上

只能停掉daemon从命令行执行

  • vallgrind提示非法指令
vex amd64->IR: unhandled instruction bytes: 0x62 0xF1 0x7D 0x48 0xEF 0xC0 0x48 0x8D
vex amd64->IR:   REX=0 REX.W=0 REX.R=0 REX.X=0 REX.B=0
vex amd64->IR:   VEX=0 VEX.L=0 VEX.nVVVV=0x0 ESC=NONE
vex amd64->IR:   PFX.66=0 PFX.F2=0 PFX.F3=0
==17407== valgrind: Unrecognised instruction at address 0x815dc7.
==17407==    at 0x815DC7: rocksdb::BlockBasedTableFactory::BlockBasedTableFactory(rocksdb::BlockBasedTableOptions const&) (block_based_table_factory.cc:44)
==17407==    by 0x7FC64D: rocksdb::ColumnFamilyOptions::ColumnFamilyOptions() (options.cc:99)
==17407==    by 0x4762EF: __static_initialization_and_destruction_0(int, int) [clone .constprop.1298] (options_helper.h:387)
==17407==    by 0x8E011C: __libc_csu_init (in /home/vdb/pika/output/pika)
==17407==    by 0x65F2B44: (below main) (in /usr/lib64/libc-2.17.so)
==17407== Your program just tried to execute an instruction that Valgrind
==17407== did not recognise.  There are two possible reasons for this.
==17407== 1. Your program has a bug and erroneously jumped to a non-code
==17407==    location.  If you are running Memcheck and you just saw a
==17407==    warning about a bad jump, it's probably your program's fault.
==17407== 2. The instruction is legitimate but Valgrind doesn't handle it,
==17407==    i.e. it's Valgrind's fault.  If you think this is the case or
==17407==    you are not sure, please let us know and we'll try to fix it.
==17407== Either way, Valgrind will now raise a SIGILL signal which will
==17407== probably kill your program.
==17407==
==17407== Process terminating with default action of signal 4 (SIGILL)
==17407==  Illegal opcode at address 0x815DC7
==17407==    at 0x815DC7: rocksdb::BlockBasedTableFactory::BlockBasedTableFactory(rocksdb::BlockBasedTableOptions const&) (block_based_table_factory.cc:44)
==17407==    by 0x7FC64D: rocksdb::ColumnFamilyOptions::ColumnFamilyOptions() (options.cc:99)
==17407==    by 0x4762EF: __static_initialization_and_destruction_0(int, int) [clone .constprop.1298] (options_helper.h:387)
==17407==    by 0x8E011C: __libc_csu_init (in /home/vdb/pika/output/pika)
==17407==    by 0x65F2B44: (below main) (in /usr/lib64/libc-2.17.so)
==17407==
==17407== Events    : Ir
==17407== Collected : 11565385

rocksdb 检测脚本 加上 PORTABLE=1 主要还是-march=native这条导致的

  • valgrind执行权限不足?子进程pika 无法修改sockte个数

14:17:17.436130 16158 pika.cc:173] your ‘limit -n ‘ of 1024 is not enough for Redis to start. pika can not reconfig it(Operation not permitted), do it by yourself

ulimit -n 102400

  • 无法启动,原因是目录下有个叫log的文件
I0527 14:47:45.087769 18132 pika_binlog.cc:87] Binlog: Manifest file not x
exist, we create a new one.                                              x
Could not create logging file: Not a directory                           x
COULD NOT CREATE A LOGGINGFILE!F0527 14:47:45.087787 18132 pika_binlog.ccx
:92] Binlog: new ./log/log_db0/write2file IO error: ./log/log_db0/write2fx
ile0: Not a directory                                                    x
*** Check failure stack trace: ***                                       x
    @     0x7fc9dd5d1b5d  google::LogMessage::Fail()                     x
    @     0x7fc9dd5d37dd  google::LogMessage::SendToLog()                x
    @     0x7fc9dd5d1773  google::LogMessage::Flush()                    x
    @     0x7fc9dd5d41fe  google::LogMessageFatal::~LogMessageFatal()    x
    @           0x6032e6  Binlog::Binlog()                               x
    @           0x615519  Partition::Partition()                         x
    @           0x5f5f83  Table::Table()                                 x
    @           0x621525  PikaServer::InitTableStruct()                  x
    @           0x625d0c  PikaServer::Start()                            x
    @           0x429a54  main                                           x
    @     0x7fc9dbaa9bb5  __libc_start_main                              x
    @           0x54ea41  (unknown)                                      x
Aborted 

测试select命令路径的脚本 需要安装redis-tools

#!/bin/bash
for i in {0..10000}
do
    echo select $(expr $i % 8 ) |redis-cli -p 9221
done

spop命令不支持count参数,需要拓展

zadd命令不支持nx xx ch incr 参数,比较复杂

pika跑单测

  1 #!/bin/bash
  2 ./runtest --list-tests |while read line
  3 do
  4 ./runtest --clients 1 --single $line
  5 done

  • 无法生成core文件
ulimit -c unlimited

core 目录

cat /proc/sys/kernel/core_pattern

也可以改core格式, 见参考链接

echo "core-%e-%p-%t" > /proc/sys/kernel/core_pattern

这样默认生成在程序所在目录

  • 原生redis test适配,tcl脚本修改

    • 需要把pika改成redis-server pika需要指定配置文件 -c,tests/support/server.tcl

      set pid [exec src/redis-server -c $config_file > $stdout 2> $stderr &]
      
    • 配置文件和redis不一样,a : b格式的,redis是a b格式,脚本用dict保存,前面第一个字段和剩下的组成dict,为了生成独一无二的端口,需要重新设置,这可能就丢掉了:,需要手动加上

      • port前加上list
       set config {}
       foreach line $data {
            if {[string length $line] > 0 && [string index $line 0] ne "#"} {
                set elements [split $line " "]
                set directive [lrange $elements 0 0]
                set arguments [lrange $elements 1 end]
                dict set config $directive $arguments
             }
        }
        # use a different directory every time a server is started
        dict set config dir [tmpdir server]
        dict set config db-path [list ":" [tmpdir server]]
          
        # start every server on a different port
        set ::port [find_available_port [expr {$::port+1}]]
        dict set config port [list ":" $::port]
        #    dict set config port $::port
      
    • 文件目录,pika是db-path, redis是dir,需要加上个db-path定义,同上,需要加上:

    • 大量测试不支持,需要注释掉,tcl注释见参考链接

  • python driver修改
    • conftest redis_version 改成pika_version #添加redis_version
    • conftest 注意db数,改成7 0~7 原生是0~9 #添加数据库
  • redigo 测试修改 redisx/db_test.go : DialTest函数 不然会报 out of range

    _, err = c.Do("SELECT", "7")
    //_, err = c.Do("SELECT", "9")
    
  • c driver 测试

    • 大量assert失败,简单hook

      /* The assert() calls below have side effects, so we need assert()
       * even if we are compiling without asserts (-DNDEBUG). */
      #ifdef NDEBUG
      #undef assert
      #define assert(e) (void)(e)
      #else
      #define assert(expr)     \
          if(expr) \
            do{ \
                fprintf(stderr, "Assertion failed:%s, file %s, line %d\n", #expr, __FILE__,__LINE__); \
               }while(0)
      #endif
      
    • 注意有redis_version 获取,要改成pika_version

    • unix sock,(/tmp/redis.sock)文件没有处理,相关测试屏蔽掉了,基本和上面没差别

redis release note

只列举功能相关部分,增强优化点不列出

redis版本 功能点
2.6 Lua脚本支持
2.6 新增PEXIRE、PTTL、PSETEX过期设置命令,key过期时间可以设置为毫秒级
2.6 新增位操作命令:BITCOUNT、BITOP
2.6 新增命令:dump、restore,即序列化与反序列化操作
2.6 新增命令:INCRBYFLOAT、HINCRBYFLOAT,用于对值进行浮点数的加减操作
2.6 新增命令:MIGRATE,用于将key原子性地从当前实例传送到目标实例的指定数据库上
2.6 SHUTDOWN命令添加SAVE和NOSAVE两个参数,分别用于指定SHUTDOWN时用不用执行写RDB的操作
2.6 sort命令会拒绝无法转换成数字的数据模型元素进行排序
2.6 不同客户端输出缓冲区分级,比如普通客户端、slave机器、pubsub客户端,可以分别控制对它们的输出缓冲区大小
2.8 引入PSYNC,主从可以增量同步,这样当主从链接短时间中断恢复后,无需做完整的RDB完全同步
2.8 新增命令:SCAN、SSCAN、HSCAN和ZSCAN
2.8 crash的时候自动内存检查
2.8 新增键空间通知功能,客户端可以通过订阅/发布机制,接收改动了redis指定数据集的事件
2.8 可通过CONFIGSET设置客户端最大连接数
2.8 新增CONFIGREWRITE命令,可以直接把CONFIGSET的配置修改到redis.conf里
2.8 新增pubsub命令,可查看pub/sub相关状态
2.8 支持引用字符串,如set ‘foo bar’ “hello world\n”
2.8 新增redis master-slave集群高可用解决方案(Redis-Sentinel)
2.8 当使用SLAVEOF命令时日志会记录下新的主机
3.0 实现了分布式的Redis即Redis Cluster,从而做到了对集群的支持
3.0 大幅优化LRU近似算法的性能
3.0 新增CLIENT PAUSE命令,可以在指定时间内停止处理客户端请求
3.0 新增WAIT命令,可以阻塞当前客户端,直到所有以前的写命令都成功传输并和指定的slaves确认
3.0 AOF重写过程中的”last write”操作降低了AOF child -> parent数据传输的延迟
3.0 实现了对MIGRATE连接缓存的支持,从而大幅提升key迁移的性能
3.0 为MIGRATE命令新增参数:copy和replace,copy不移除源实例上的key,replace替换目标实例上已存在的key
3.2 新增对GEO(地理位置)功能的支持
3.2 SPOP命令新增count参数,可控制随机删除元素的个数
3.2 新增HSTRLEN命令,返回hash数据类型的value长度
3.2 提供了一个基于流水线的MIGRATE命令,极大提升了命令执行速度
4.0 加入模块系统,用户可以自己编写代码来扩展和实现redis本身不具备的功能,它与redis内核完全分离,互不干扰
4.0 优化了PSYNC主从复制策略,使之效率更高
4.0 为DEL、FLUSHDB、FLUSHALL命令提供非阻塞选项,可以将这些删除操作放在单独线程中执行,从而尽可能地避免服务器阻塞
4.0 新增SWAPDB命令,可以将同一redis实例指定的两个数据库互换
4.0 新增RDB-AOF持久化格式,开启后,AOF重写产生的文件将同时包含RDB格式的内容和AOF格式的内容,其中 RDB格式的内容用于记录已有的数据,而AOF格式的内存则用于记录最近发生了变化的数据
4.0 新增MEMORY内存命令,可以用于查看某个key的内存使用、查看整体内存使用细节、申请释放内存、深入查看内存分配器内部状态等功能
5.0 新的流数据类型(Stream data type) https://redis.io/topics/streams-intro
5.0 新的 Redis 模块 API:定时器、集群和字典 API(Timers, Cluster and Dictionary APIs)
5.0 RDB 现在可存储 LFU 和 LRU 信息
5.0 新的有序集合(sorted set)命令:ZPOPMIN/MAX 和阻塞变体(blocking variants)
5.0 更好的内存统计报告
5.0 许多包含子命令的命令现在都有一个 HELP 子命令
5.0 引入 CLIENT UNBLOCK 和 CLIENT ID

兼容性对比

命令 版本 复杂度 pika
string      
SET 1.0 O1 支持
SETNX 1.0 O(1) 支持
SETEX 2.0 O(1) 支持
PSETEX 2.6 O(1) 支持
GET 1.0 O(1) 支持
GETSET 1.0 O(1) 支持
STRLEN 2.2 O(1) 支持
APPEND 2.0 平摊O(1) 支持
SETRANGE 2.2 如果本身字符串短。平摊O(1),否则为O(len(value)) 支持
GETRANGE 2.4 O(N)N为返回字符串的长度 支持
INCR 1.0 O(1) 支持
INCRBY 1.0 O(1) 支持
INCRBYFLOAT 2.6 O(1) 支持
DECR 1.0 O(1) 支持
DECRBY 1.0 O(1) 支持
MSET 1.0.1 O(N) N为键个数 支持
MSETNX 1.0.1 O(N) N为键个数 支持
MGET 1.0.0 O(N) N为键个数 支持
SETBIT 2.2 O(1) 部分支持
GETBIT 2.2 O(1) 部分支持
BITCOUNT 2.6 O(N) 部分支持
BITPOS 2.8.7 O(N) 支持
BITOP 2.6 O(N) 部分支持
BITFIELD 3.2 O(1) 不支持
hash      
HSET 2.0 O(1) 支持
HSETNX 2.0 O(1) 支持
HGET 2.0 O(1) 支持
HEXISTS 2.0 O(1) 支持
HDEL 2.0 O(N) N为删除的字段个数 支持
HLEN 2.0 O(1) 支持
HSTRLEN 3.2 O(1) 支持
HINCRBY 2.0 O(1) 支持
HINCRBYFLOAT 2.6 O(1)支持 支持
HMSET 2.0 O(N) N为filed-value数量 支持
HMGET 2.0 O(N) 支持
HKEYS 2.0 O(N)N为哈希表大小? 支持
HVALS 2.0 O(N) 支持
HGETALL 2.0 O(N) 支持
HSCAN 2.0 O(N)? 支持
list      
LPUSH 1.0 O(1) 支持
LPUSHX 2.2 O(1) 支持
RPUSH 1.0 O(1) 支持
RPUSHX 2.2 O(1) 支持
LPOP 1.0 O(1) 支持
RPOP 1.0 O(1) 支持
RPOPLPUSH 1.2 O(1) 支持
LREM 1.0 O(N) 支持
LLEN 1.0 O(1) 支持
LINDEX 1.0 O(N) N为遍历到index经过的数量 支持
LINSERT 2.2 O(N) N 为寻找目标值经过的值 支持
LSET 1.0 O(N) N为遍历到index处的元素个数 支持
LRANGE 1.0 O(S+N) S为start偏移量,N为区间stop-start 支持
LTRIM 1.0 O(N) N为被移除元素的数量 支持
BLPOP 2.0 O(1) 不支持
BRPOP 2.0 O(1) 不支持
BRPOPLPUSH 2.2 O(1) 不支持
set      
SADD 1.0 O(N) N是元素个数 支持
SISMEMBER 1.0 O(1) 支持
SPOP 1.0 O(1)支持 支持
SRANDMEMBER 1.0 O(N) N为返回值的个数 支持 行为可能不一致 复杂度ON
SREM 1.0 O(N) N为移除的元素个数支持 支持
SMOVE 1.0 O(1) 支持
SCARD 1.0 O(1) 支持
SMEMBERS 1.0 O(N) N为集合基数 支持
SSCAN     支持
SINTER 1.0 O(NxM) N是集合最小基数,M是集合个数 支持
SINTERSTORE 1.0 O(N*M) 支持
SUION 1.0 O(N) N是所有给定元素之和 支持
SUIONSTORE 1.0 O(N) 支持
SDIFF 1.0 O(N) 支持
SDIFFSTORE 1.0 O(N) 支持
Sorted Set      
ZADD 1.2 O(M*logN) N 是基数M是添加新成员个数 支持
ZSCORE 1.2 O(1) 支持
ZINCRBY 1.2 O(logN) 支持
ZCARD 1.2 O(1) 支持
ZCOUNT 2.0 O(logN) 支持
ZRANGE 1.2 O(logN+M) M结果集基数 N有序集基数 支持
ZREVRANGE 1.2 O(logN+M) M结果集基数 N有序集基数 支持
ZRANGEBYSCORE 1.05 O(logN+M) M结果集基数 N有序集基数 支持
ZREVRANGEBYSCORE 1.05 O(logN+M) M结果集基数 N有序集基数 支持
ZRANK 2.0 O(logN) 支持
ZREVRANK 2.0 O(logN) 支持
ZREM 1.2 O(logN*M) N基数M被移除的个数 支持
ZREMRANGEBYRANK 2.0 O(logN+M) N基数 M被移除数量 支持
ZREMRANGEBYSCORE 1.2 O(logN+M) N基数 M被移除数量 支持
ZRANGEBYLEX 2.8.9 O(logN+M) N基数 M返回元素数量 支持
ZLEXCOUNT 2.8.9 O(logN) N为元素个数 支持
ZREMRANGEBYLEX 2.8.9 O(logN+M) N基数 M被移除数量 支持
ZSCAN     支持
ZUNIONSTORE 2.0 时间复杂度: O(N)+O(M log(M)), N 为给定有序集基数的总和, M 为结果集的基数。 支持
ZINTERSTORE 2.0 O(NK)+O(Mlog(M)), N 为给定 key 中基数最小的有序集, K 为给定有序集的数量, M 为结果集的基数。 支持
ZPOPMAX 5.0 O(log(N)*M) M最大值个数 不支持
ZPOPMIN 5.0 O(log(N)*M) 不支持
BZPOPMAX 5.0 O(log(N)) 不支持
BZPOPMIN 5.0 O(log(N)) 不支持
HyperLogLog      
PFADD 2.8.9 O(1) 支持
PFCOUNT 2.8.9 O(1),多个keyO(N) 支持
PFMERGE 2.8.9 O(N) 支持
GEO      
GEOADD 3.2 O(logN) 支持
GEOPOS 3.2 O(logN) 支持
GEODIST 3.2 O(logN) 支持
GEORADIUS 3.2 O(N+logM)N元素个数M被返回的个数 支持
GEORADIUSBYMEMBER 3.2 O(N+logM) 支持
GEOHASH 3.2 O(logN) 支持
Stream      
XADD 5.0 O(1) 不支持
XACK 5.0 O(1) 不支持
XCLAIM 5.0 O(log N) 不支持
XDEL 5.0 O(1) 不支持
XGROUP 5.0 O(1) 不支持
XINFO 5.0 O(N) 不支持
XLEN 5.0 O(1) 不支持
XPENDING 5.0 O(N) 可以退化为O(1) 不支持
XRANGE 5.0 O(N) 不支持
XREAD 5.0 O(N) 可以退化为O(1) 不支持
XREADGROUP 5.0 O(M) 可以退化为O(1) 不支持
XREVRANGE 5.0 O(N) 可以退化为O(1) 不支持
XTRIM 5.0 O(N) 不支持
Keys      
EXISTS 1.0 O(1) 支持
TYPE 1.0 O(1) 支持 行为不一致但是pika允许重名,有类型输出顺序
RENAME 1.0 O(1) 不支持
RENAMENX 1.0 O(1) 不支持
MOVE 1.0 O(1) 不支持
DEL 1.0 O(N) N为key个数 支持
RANDOMKEY 1.0 O(1) 不支持
DBSIZE 1.0 O(1) 支持 行为不一致
EXPIRE 1.0 O(1) 支持
EXPIREAT 1.2 O(1) 支持
TTL 1.0 O(1) 支持
PERSIST 2.2 O(1) 支持
PEXPIRE 2.6 O(1) 支持 行为不一致,单位秒
PEXPIREAT 2.6 O(1) 支持 行为不一致,单位秒
PTTL 2.6 O(1) 支持
MULTI 1.2 O(1) 不支持
EXEC 1.2 事务块内执行的命令复杂度和 不支持
DISCARD 2.2 O(1) 不支持
WATCH 2.2 O(1) 不支持
UNWATCH 2.2 O(1) 不支持
EVAL 2.6 O(1) 找到脚本。其余复杂度取决于脚本本身 不支持
EVALSHA 2.6 根据脚本的复杂度而定 不支持
SCRIPT LOAD 2.6 O(N) N为脚本长度 不支持
SCRIPT EXISTS 2.6 O(N) N为判断的sha个数 不支持
SCRIPT FLUSH 2.6 O(N) N为缓存中脚本个数 不支持
SCRIPT KILL 2.6 O(1) 不支持
SAVE 1.0 O(N) N为key个数 不支持
BGSAVE 1.0 O(N) 支持 行为不一致
BGREWRITEAOF 1.0 O(N) N为追加到AOF文件中的数据数量 不支持
LASTSAVE 1.0 O(1) 不支持
Pub/Sub      
PUBLISH 2.0 O(M+N) channel订阅者数量+模式订阅客户端数量 支持
SUBSCRIBE 2.0 O(N) N是channel个数 支持
PSUBSCRIBE 2.0 O(N) N是模式的个数 支持
UNSUBSCRIBE 2.0 O(N) N是channel个数 支持
PUNSUBSCRIBE 2.0 O(N) N是channel个数 支持
PUBSUB CHANNELS
PUBSUB NUMSUB
PUBSUB NUMPAT
2.8 O(N) N是频道个数 支持
管理命令      
SLAVEOF 1.0 O(1) 支持
ROLE 2.8.12 O(1) 不支持
AUTH 1.0 O(1) 支持
QUIT 1.0 O(1) 支持
INFO 1.0 O(1) 支持 行为不一致
SHUTDOWN 1.0 O(1) 支持
TIME 2.6 O(1) 支持
CLIENT GETNAME CLIENT KILL CLIENT LIST SETNAME PAUSE REPLY ID 2.6.9
2.4
O(1)
O(N
O(N))
O(1)
部分支持
CONFIG SET CONFIG GET CONFIG RESETSTAT CONFIG REWRITE 2.0…2.8 O(1)O(N)O(1)O(N) 部分支持
PING 1.0 O(1) 支持
ECHO 1.0 O(1) 支持
OBJECT 2.2.3 O(1) 不支持
SLOWLOG 2.2.12 O(1) 支持
MONITOR 1.0 O(N) 支持
DEBUG OBJECT
DEBUG SEGFAULT
1.0 O(1) 不支持
MIGRATE 2.6 O(N) 不支持
DUMP 2.6 O(1)查找O(N*size)序列化 不支持
RESTORE 2.6 O(1)查找O(N*size)反序列化,有序集合还要再乘logN,插入排序的代价 不支持
SYNC 1.0 O(N) 支持,行为不一致
PSYNC 2.8 NA 支持 行为不一致
SORT     不支持
SELECT 1.0   支持 实现了一部分
  • bit操作部分支持主要是精度问题

  • stream 不支持,未实现,5.0之后的命令都没有实现

  • lua脚本相关命令都没有实现 这个依赖lua虚拟机。改动较大

  • module相关命令都没有实现 这个涉及api移植,改动较大

  • bit操作部分支持主要是精度问题

  • 集群相关命令都没有实现

  • 阻塞命令都没有实现

  • rename没有实现 这个涉及到pika的元数据。由于跨db,改名也稍稍麻烦

  • select数据库,pika只支持0-7 原生支持0-9 已经解决

  • client命令只支持list和kill

  • config命令 原生配置字段和pika配置字段不一致

  • info命令显式的字段和redis原生不一致

  • pexpire命令精度有限

  • spop不支持count指令

  • zadd不支持xxnxincr ch指令

  • ping不支持参数

  • 不支持quit

  • 不支持flushdb和randomkey

  • zset range 还有zincrby命令,double判断inf有问题,回复也不标准

  • lpushx rpushx不支持多参数

  • setrange 回复不标准

  • ping在pubsub中的回复不标准

  • expire ttl精度有问题,改时间戳解决

  • zadd 加入元素最后的会更新,pika没有这个逻辑,主要是实现insert_or_assign这种东西

  • smove行为也不太一样

  • incr incrbyfloat行为也不一致

  • zpopmax zpopmin命令没实现

ref

Read More


^