为什么使用Prepared Statement?

  1. 查询执行的基础
    MySQL发送一个请求的时候,MySQL到底做了什么:

    a. 客户端发送一条查询给服务器
    b. 服务器先检查查询缓存,如果命中了缓存,则like返回存储在缓存中的结果。否则进入下一阶段
    c. 服务器端进行SQL解析,预处理,再由优化器生成对应的执行计划
    d. MySQL根据优化器生成的执行计划,调用存储引擎的API来执行查询
    e. 将结果返回给客户端。

    即:SQL执行过程包括以下阶段:查询缓存是否命中->词法解析->语法分析->语义分析->查询优化->执行。

  2. 硬解析

    词法分析->语法分析这两个阶段我们称之为硬解析。词法分析识别SQL中每个词,语法分析解析SQL是否符合SQL语法(如关键字顺序是否正确),并得到一颗语法树。对于只是参数不同,其他均相同的SQL,他们执行时间不同但是硬解析时间是相同的。

  3. 查询优化器
    现在语法树被认为是合法了,并且由优化器将其转化成执行计划。一条查询可以有很多种执行方式,最后返回相同的结果。
    优化器的作用:就是找到这其中最好的执行计划

  4. Prepare的目的
    Prepare的出现就是为了优化硬解析的问题。Prepare在服务器端执行过程如下:

    a. Prepare接受客户端带?的SQL,硬解析得到语法树stmt->lex, 缓存在线程所在的preparestatement cache中。此cache是一个HASH MAP. Key为stmt->id. 然后返回客户端stmt->id等信息。
    b. Execute 接收客户端stmt->id和参数等信息。注意这里客户端不需要再发sql过来。服务器根据stmt->id在preparestatement cache中查找得到硬解析后的stmt, 并设置参数,就可以继续后面的优化和执行了

  5. Prepared Statements的好处:
    a. 安全
    Prepared Statements通过sql逻辑与数据分离来增加安全,sql逻辑与数据的分离能够防止普通类型的SQL注入攻击(sql injection attck)。
    b. 性能
    prepare经过语法解析器和预处理生成了解析树。通过prepare方式,当执行多次时,就不会有额外的负担了。

  6. Prepared 实例

表结构

CREATE TABLE `z` (
    `a` int(11) NOT NULL,
    `b` int(11) DEFAULT NULL,
    PRIMARY KEY (`a`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8

Prepare

prepare stmt1 from 'select * from z where a = ?';
set @a = 20;
execute stmt1 using @a;

MySQL prepare原理

linux定时任务

34.1. Cron

Cron是一个守护进程,根据分时日月周的组合来执行任务。
Cron假设系统是可用的。如果系统不可用,那么定时任务不会执行。为了安排one-time tasks,可以参考 at and batch.

为了执行Cron服务,crond服务必须执行。
可以使用命令rpm -q vixie-cron命令查看是否安装。可以使用命令 service crond status查看状态

34.1.1 配置定时任务

SHELL=/bin/bash 
PATH=/sbin:/bin:/usr/sbin:/usr/bin 
MAILTO=root HOME=/  
# run-parts 
01 * * * * root run-parts /etc/cron.hourly 
02 4 * * * root run-parts /etc/cron.daily 
22 4 * * 0 root run-parts /etc/cron.weekly 
42 4 1 * * root run-parts /etc/cron.monthly

前四行用于配置定时任务执行时的环境变量。SHELL变量告诉系统使用哪个shell,PATH变量定义了执行命令的路径。MAILTO定义了定时任务的输出结果被email给谁,如果MAILTO=””,则不发送邮件。HOME变量用于home目录。

/etc/crontab/文件中任务的格式如下:
minute hour day month dayofweek command

  • minute – 从0到59的任何数字
  • hour – 从0到23的任何数字
  • day – 从0到31的任何一天
  • month – 从1到12的任何数字(或者月份的缩写,如jan或feb)
  • dayofweek – 从0到7的数字,其中0或7代表星期天(或者缩写,如mon或sun)
  • command – 要执行的命令

几种取值情况

  • *:代表任何有效值
  • -:指定一个数字的范围,如1-3代表整数1,2,3。
  • ,: 一个以逗号分隔的列表,如3,4,5,8代表四个特定的数字。
  • /: 用于指定步进的值(step value)。 The value of an integer can be skipped within a range by following the range with /. For example, 0-59/2 can be used to define every other minute in the minute field. Step values can also be used with an asterisk. For instance, the value */3 can be used in the month field to run the task every third month.

正如在/etc/crontab文件中,run-parts脚本执行/etc/cron.hourly/,/etc/cron.daily/, /etc/cron.weekly/, and /etc/cron.monthly/目录下的文件。在这些目录下面必须是shell脚本。

如果定时任务不是每小时,每天,每周,每月,那么定时任务可以加到/ect/cron.d/目录下,这个目录下文件的格式与/etc/crontab是一样的。

除了root以外的用户,可以使用crontab工具配置定时任务。所有用户定义的crontab都存在/var/spool/cron/目录下,并且使用创建他们的用户名来执行。使用crontab -e可以编辑。

cron守护进程每分钟会检查/etc/crontab文件,/ect/cron.d/目录的变化,如果有变化则加载到内存。因此如果crontab文件改变了不需要重启守护进程。

34.1.2 控制访问cron权限

/etc/cron.allow和/etc/cron.deny文件用于严格控制cron进程的访问。文件格式:一行一个用户名,不需要出现空格。如果访问控制文件修改了,crond不需要重启。

root用户什么时候都可以使用cron,而不用考虑访问控制文件中的用户名
如果cron.allow文件存在,只有users列表中的用户允许访问;忽略cron.deny文件
如果cron.allow文件不存在,只有cron.deny中用户不允许访问。

34.1.3 开始和停止服务

使用service crond start 开启。
使用service crond stop 关闭。

34.2 At和Batch

cron用于规划重复的任务,at命令用于规划一次性任务(one-time task),batch用于在系统负载小于0.8的时候执行一次性任务。

参考文章

  1. cron
  2. at and batch

RedisCluster总结

一、RedisCluster基本内容

1. 项目背景

a. 整个项目背景

  • 云盘memcache服务器分散,监控和替换麻烦

    云盘100多个集群,每个集群4个memcache,需要业务方配置和监控这些memcache。一旦memcache超时或者不可用,需要业务方和ops共同操作才能解决memcache的问题。

  • 云盘缓存并发高、空间大

    云盘缓存有36亿,400G空间,每秒钟25万的请求量,因此需要高并发的缓存集群才能满足需求。

b. 现在的替代方案

使用RedisCluster作为缓存集群。其特点(也是其设计目标)是:

  • 水平扩展:最大可以扩展到1000个节点
  • 可以接受的写安全:当出现网络分区(即脑裂)的时候能保证连接到大多数节点分区的写是安全的。
  • 高可用性:当出现网络分区时,大多数节点所在的分区是可用的;并且通过复制转移(replicas migration)保证没有从节点的主节点获得一个从节点。
    (所谓的复制转移:当一个主节点A有多个从节点A1,A2,A3,而另外一个主节点B没有从节点时,RedisCluster集群会从多个从节点A1,A2,A3中选择一个从节点A2作为主节点B的从节点)

目前RedisCluster 目前集群大小是:

  • 一个机房一个大集群
  • bjdt:集群中有24个节点,每个节点一主一从。每个节点18.63G
  • bjcc:集群内有16个节点,每个节点一主一从。每个节点18.63G。

2. 使用方法

a. 业务方使用:

有两种客户端,一种是笨蛋客户端,一种是智能客户端。

笨蛋客户端:本地不缓存节点信息,请求达到任意一个RedisCluster节点后,被告知真正的节点,然后再次请求。比如:redis-cli,telnet

redis-cli -c -h 10.10.10.10 -p 6666
10.10.10.10:6666> get ab
-> Redirected to slot [13567] located at 11.11.11.11:6666
"1"

智能客户端:本地缓存节点信息,通过本地计算出槽位,直接发一次请求即可。

b. phpredis

目前phpredis是智能客户端。

不过在最开始的版本中,phpredis每次new RedisCluster都会发送cluster slots来获取节点信息,其实对于一个业务而言是没有必要的。原因是因为RedisCluster一旦稳定后一般不会调整,那么其节点信息就维持不变,因此没有必要每次都去获取。并且cluster slots非常消耗cluster节点的cpu。这一点吴晓飞同学已经给phpredis提了pullrequest

3. RedisCluster基本概念

a. 集群创建

  1. Redis实例启动,redis.conf中多了一个配置项cluster-enabled yes
  2. Redis节点通过cluster meet形成一个集群,但是集群不可用
  3. 分配槽位:需要将16384个槽位分配到集群中各个节点。如果有的槽位没有分配成功,那么集群就可能不可用(跟cluster-require-full-coverage参数有关系)

b. RedisCluster keys分布模型

一共有16384个hash槽位。集群中每个主节点负责16384个槽位中的一部分。如果没有正在进行中的集群重新配置,那么槽位的分布是稳定的。

key与槽位的映射关系计算算法(如果有hash tags则不使用该计算算法)

HASH_SLOT=CRC(key) mod 16384

c. RedisCluster与Memcache比较

我们知道Memcached是“分布式”缓存服务器,但是服务器端没有分布式功能,需要客户端来实现分布式,也就是需要客户端配置好几台memcached服务器,根据分布式算法(一般采用一致性hash算法)计算哪个key位于哪台memcached机器上。

而RedisCluster没有采用一致性hash算法,而是采用了一种hash 槽位的概念。与Memcached相比,业务方不需要维护memcached服务器的列表。使用智能客户端,可以先获取一份节点信息,然后在本地进行计算hash槽位。

d. RedisCluster HashTags

所谓的hash tags是指key中出现了{"字符串"}或者空的符合{}。 当出现HashTags时,key的hash 槽位不是计算整个key所在的槽位,而是计算{}中间字符串的槽位。
举例:{user1000}.following{user1000}.followers 这两个key在同一个slot里面。因为其hash tags中的字符串是 user1000

e. RedisCluster和Redis比较

  • RedisCluster实现了所有单key的命令,比如get,set
  • RedisCluster中多key操作的命令要求多key必须在同一个节点上,可以通过hash tags强迫这些key存在相同节点上。

f. RedisCluster重定向

基本概念

RedisCluster有两种重定向错误:-MOVED-ASK两种重定向错误

刚刚在RedisCluster keys分布模型中提到如果没有进行中的集群重新配置,那么槽位分布是稳定的。
如果集群正在发生调整呢,那么集群会返回给客户端-MOVED 8 127.0.0.1:8080错误或者 -ASK 8 127.0.0.1:8080这两个重定向错误。

举例

举例:假设集群中节点A(127.0.0.1:8080)负责hash槽位13565,13566,13567。 节点B(127.0.0.2:8080)负责hash槽位100,101,102。

根据crc(ab) mod 16384计算得到ab这个key的slot是13567

a. 如果集群没有调整,那么 向节点B请求get ab ,那么节点B会返回-MOVED 127.0.0.1:8080

b. 如果集群有所调整,将hash槽位13567从节点A迁移到节点B。

在迁移过程中,节点A处于MIGRATING状态,节点B处于IMPORTING状态。

此时向节点A请求get ab,

1) 如果ab这个位于13567的key已经迁移到节点B了,那么将会受到-ASK 127.0.0.2:8080,此时客户端先向节点B发送一个ASKING的命令,然后再get ab就可以获得数据。如果客户端不向节点B发送一个ASKING命令,而是直接向节点B发送get ab则会返回-MOVED错误。
2) 如果ab这个位于13567的key没有迁移到节点B,即依然在节点A上,那么直接返回数据。

c. 如果集群已经调整完毕,即hash槽位13567位于节点B,那么向节点A请求 get ab就会返回-MOVED 127.0.0.2:8080

-MOVED-ASK两个重定向区别:

  1. MOVED代表槽已经完全从一个节点迁移到另外一个节点
  2. ASK是槽位迁移的中间态,代表这个槽位的节点正在迁移。只针对这个槽位的这个key进行重定向,该槽位上其他key依然先到原来的节点

f. RedisCluster节点属性

redis-cli cluster nodes
d1861060fe6a534d42d8a19aeb36600e18785e04 127.0.0.1:6379 myself - 0 1318428930 1 connected 0-1364
3886e65cc906bfd9b1f7e7bde468726a052d1dae 127.0.0.1:6380 master - 1318428930 1318428931 2 connected 1365-2729
d289c575dcbc4bdd2931585fd4339089e461a27d 127.0.0.1:6381 master - 1318428931 1318428931 3 connected 2730-4095

通过cluster nodes命令可以知道RedisCluster集群的很多信息,比如当前槽位分布,当前节点个数及其ip:port等等

二、RedisCluster的高可用原理

1. RedisCluster的CAP

RedisCluster本身是一个分布式NoSQL,因此必然符合CAP定理中的相关内容。

CAP定理

CAP定理基本描述:

给定”一致性Consistency“、”可用性Availability“、”分区耐受性Partition tolerance”这三个属性,我们只能同时满足其中两个属性。

  • 分区耐受性:如果发生通信故障,导致整个集群被分隔成多个无法互相通信的分区时(这种情况也叫“脑裂” split brain),集群依然可用。

    在RedisCluster中有个参数cluster-require-full-coverage
    该参数取值为yes时,出现分区时则不可用;反之则不可用

    脑裂

  • 一致性:在各个节点上数据一样。
    一致性的关键在于:将请求序列化,使之成为原子的(atomic)、相互隔离的(Isolated)”工作单元“(work unit)。

  • 可用性:如果客户可以同集群中的某个节点通信,那么该节点就必须能够处理读取及写入操作。
    CAP定理中将可用性定义为:系统中某个无故障节点所接受的每一条请求,无论成功或失败,都必将得到响应。所以按照这个定义,发生故障且无法响应客户请求的节点,并不会导致系统失去“CAP定理”所定义的那种“可用性”。

这意味着你可以构建一个CA集群,如果出现“分区”现象,那么所有节点必须全部停止工作。

尽管CAP定理经常表述为“三个属性中只能保有两个”,但实际上是在讲:当系统可能遭遇“分区”状况时,我们需要在“一致性”与“可用性”之间进行权衡。通常我们会略微舍弃“一致性”,以获取某种程度的“可用性”。这样产生的系统,既不具备完美的“一致性”,也不具备完美的“可用性”,但是这两种不完美的特性结合起来却能够满足特定需求。

RedisCluster满足了AP,最终一致性

  1. 分区耐受性:大多数节点可用时,RedisCluster可以分区可用。当出现分区时,连接到多数节点分区和连接到少数节点分区有很大不同。
  2. 可用性:通过复制转移(replicas migration),没有从节点的主节点会获得一个从节点,从而提供可用性。
  3. 高性能并且能够线性扩展到1000个节点。
  4. 最终一致性,异步复制,存在写入安全问题。

    最终一致性:eventually consistent:也就是说在任意时刻,节点中都可能存在“复制不一致”问题,然而只要不再继续执行其他更新操作,那么上一次更新操作的结果最终将会反映到全部节点中去。

RedisCluster写入的流程

RedisCluster的写入是异步复制。

  • client先写入主,主回复+OK,此时client认为写入成功
  • 主节点开始异步复制,将写入同步到从节点

RedisCluster写入安全

  • 写入安全情况1:
    先写入主
    再由主传播到从。
    主从式分布模型,存在主写入成功,主挂没有传播
    从升为主,数据丢失。
  • 写入安全情况2: 发生分区时写丢失

    举例:6节点集群,3主3从,节点是A,B,C, A1,B1,C1。还有一个客户端Z1。发生分区时,一个分区是A,C,A1,B1,C1,另外一个分区是B和Z1.此时Z1还可以向B写入,B也可以接收写操作。
    如果在很短时间内,B恢复了那么集群正常;
    如果分区持续太久,那么B1就被推举为新主,此时过了NODE_TIMEOUT时间,节点B也不写,那么分区后写入B的数据就丢失了。
    写操作丢失的最大持续时间是NODE_TIMEOUT+从推举时间

    NoSQL倡导者经常说,与关系型数据库所支持的ACID事务不同,NoSQL系统具备“BASE属性“(基本可用,柔性状态,最终一致性)英文是 Basically Available, Soft state, Eventual consistency.

2. 容错 Fault Tolerance

心跳包和流言消息

心跳包的目的:RedisCluster集群中各个节点会通过发送心跳包(ping包和pong包)来进行通信,更新集群的配置信息。通常节点发送ping包以后,期待对方回复pong包。

RedisCluster一个节点一次只向集群中某些节点发送心跳包(目前一次发送的节点数量是总节点个数的十分之一)。同时考虑到RedisCluster发送对象节点是随机的,所以存在两个节点很久都没有交换消息,为了保证集群状态能够在很多时间内达到一致性,RedisCluster规定当两个节点超过NODE_TIMEOUT一半的时间没有交换消息时,下次发心跳包交换消息。

比如,对于100个节点的集群,NODE_TIMEOUT设置为60秒,那么根据上面的理论,一个节点在30s内要向99个节点发送ping,对于100各节点则每秒发送330个pings。近千节点的Redis Cluster高可用集群案例&version=11020201&pass_ticket=4CBC9RgbSswvChRwX4aHuDwbNTxwAjmPNbOVneP4ac8%2BaS%2BQ8YWN5LJF3ipxB8fR)

通过这一点可以看出,集群间通信占用大量带宽资源。

故障检测

目的:当大多数节点不能访问某个主或从节点时,其从节点就会被推举为主节点。当从推举失败后,集群是error state, 将会停止接收来自客户端的请求。

实现:

  1. PFAIL状态:
    集群中有n个节点,当节点A自己认为节点B不可用了,并不能认为节点B不可用,
  2. FAIL状态:
    必须集群中大多数节点认为节点B不可用了,节点B才是真的不可用。
    当在NODE_TIME * FAIL_REPORT_VALIDITY_MULT时间内超过一半的节点认为B不可达时,节点B才真的是不可达
    (当前实现中该validity参数是2)。
    节点不可达的概念:节点发送的ping包超过NODE_TIMEOUT时间依然收不到pong包。
    工作原理:当发送的ping包,在NODE_TIMEOUT/2时间后依然收不到pong包时,节点会去重连集群中的其他节点。
  3. 广播:
    此时节点A会广播一条FAIL消息,告知大家节点B不可达。所有收到FAIL消息的节点,都被强制设定节点B不可达。
    FAIL标志只是为了安全的触发从推举的算法。

故障转移 failover

  1. 从节点选举

    从已经下线的主节点的所有从节点里面,选中一个从节点。从节点的选举需要得到大多数主节点的授权

  2. 成为主节点

    被选中的从节点会执行SLAVEOF no one命令,成为新的主节点并且负责旧主节点的槽位

  3. 广播

    广播一条pong消息,通知其他节点更新节点映射信息

复制转移 replicas migration

复制转移的定义:如果一个主节点A没有从节点,而集群中有的主节点B有多个从节点s1,s2,s3;那么RedisCluster会从多个从节点中选择一个从节点作为主节点A的从节点。
复制转移的目的:为了提高系统的可用性。

比如一个集群中A,B,C 3个主节点,其从节点分别是A1,B1,C1,C2,其中C的从节点有两个。
当B挂了以后,B1升为主节点,此时B1节点没有从节点。

a. 如果没有复制转移的话,那么B1再挂了的话,这个节点上的槽位都不可用了。
b. 如果有复制转移的话,那么会从C的两个从节点C1和C2中选择一个作为B1的从节点,我们假设选择的是C2。那么现在集群的情况是A-A1,B1-C2,C-C1。如果这时候B节点又可用了,那么他将会作为某个主节点的从节点。

4. 配置操作、传播、故障转移

Cluster currentEpoch

其目的是为了当节点信息发生冲突的时候来解决冲突。解决冲突的方法很简单,epoch高的配置覆盖epoch低的配置,即以epoch高的配置为准。

configuration epoch

  1. 新节点创建时,configEpoch是0
  2. 从节点推举后,生成新的configEpoch,从节点尝试取代失败主节点的epoch,并且获得大多数主节点的授权,那么configEpoch会加1.
  3. configEpoch帮助解决的是当不同节点声明不同配置配置时,用于解决冲突。

RedisCluster配置参数:

cluster-enabled 
cluster-config-file <filename>
cluster-node-timeout <milliseconds>
cluster-slave-validity-factor <factor>
cluster-migration-barrier <count>
cluster-require-full-coverage <yes/no>

三、期间出现问题

  1. 客户端cluster slots没有缓存,造成RedisCluster节点cpu过高。

    phpredis虽然是智能客户端,
    但对于每个RedisCluster类都需要发送cluster slots命令获取节点和slot的对应关系,从而造成节点CPU过高。
    因为cluster slots命令需要执行两层主循环,分别是循环nodes和slot。对于master节点还需要扫描slave,cpu的计算开销就出来了

  2. timewait过高

    由于我们代码中设置100ms超时,当RedisCluster服务器返回过慢时,客户端会主动断开连接,因此出现大量timewait

  3. 为何不使用长连接。

    a. 因为云盘前端机非常多,300台前端机,每台前端机128个进程,那么对于cluster节点而言是38400个长连接,cluster节点所占用的内存和fd开销非常大。
    b. 长连接:cpu明显下降,但是连接数暴涨
    c. 短连接:通过客户端缓存节点信息能够降低部分cpu,但cpu依然偏高

  4. del返回warning

    原因是del对返回值校验严格,要求必须是整形(即:1)这种格式,当出现-MOVED错误或者超时没有读取到数据时,则会报warning
    出现时机:当RedisCluster的CPU过高时,del在100ms(设定的读超时是100ms)内没有响应,那么返回?的值,不是整形,因此会报warning

参考文章

  1. Redis Cluster的FailOver失败案例分析
  2. Redis 百万QPS挑战
  3. Redis Cluster Specification

chapter6:mysql锁

6.1 什么是锁

锁机制用于管理对共享资源的并发访问

MyISAM引擎的锁是表锁。

InnoDB提供一致性的非锁定读、行级锁支持。行级锁没有相关额外开销,并可以同时得到并发性和一致性

6.2 lock与latch

一般我们提到的是lock。lock的对象是事务,用来锁定的是数据库中的对象,如表,页,行。并且一般lock的对象只在事务commit和rollback后进行释放(不同隔离级别有所不同)。

latch的目的是用来保证并发线程操作临界资源的正确性,并且没有死锁检测的机制。

6.3 InnoDB存储引擎中的锁

6.3.1 锁的类型

InnoDB实现了如下两种标准的行级锁:

1. 共享锁(S Lock)允许事务读一行数据
2. 排他锁(X Lock)允许事务删除或更新一行数据

InnoDB支持意向锁,其意向锁即为表级别的锁。

6.3.2 一致性非锁定读 consistent nonlocking read

一致性非锁定读指InnoDB存储引擎通过行多版本控制的方式来读取当前执行时间数据库中行的数据。如果读取的行正在执行DELETE或UPDATE操作,这时读取操作不会因此等待行上锁的释放,而是会读取行的一个快照数据。

快照数据是指该行的之前版本的数据,该实现是通过undo段来完成,undo段是用来在事务中回滚数据因此没有额外的开销。

在不同事务隔离级别下,读取方式的不同,并不是在每个事务隔离级别下都是采用非锁定的一致性读。此外,即使都是使用非锁定的一致性读,但是对于快照数据的定义也不相同。

在事务隔离级别READ COMMITTED和REPEATABLE READ下,InnoDB存储引擎使用非锁定的一致性读,但是快照数据定义不相同。在REPEATABLE READ隔离级别下,非一致性锁定读总是读取事务开始时的行数据版本。在READ COMMITTED下,总是读取被锁定行的最新一份快照数据。

6.3.3 锁定读 locking reads

在某些情况下用户需要显式对数据库读取操作进行加锁以保障数据逻辑的一致性。即使对于SELECT的只读操作,也要支持加锁语句。

支持两种一致性的锁定读(locking read)操作:

SELECT .. FOR UPDATE
SELECT .. LOCK IN SHARE MODE

6.4 锁的算法

6.4.1 行锁的3种算法

InnoDB有3种行锁算法,其分别是:

Record Lock:单个行记录上的锁
Gap Lock:间隙锁,锁定一个范围,但不包含记录本身
Next-Key Lock: Gap Lock + Record Lock 锁定一个范围并且锁定记录本身。

在默认隔离级别REPEATABLE READ下,使用的是Next-Key Lock。对于查询的索引含有唯一属性时,InnoDB会将Next-KeyLock进行优化,降级为RecordLock,即仅锁住索引本身,而不是范围。

而对于辅助索引,其加上Next-KeyLock InnoDB存储引擎还会辅助索引下一个键值加上gapLock,即会锁住这个辅助索引附近的两个范围。

6.4.2 解决幻象问题

幻象是指在同一个事务下,连续执行两次同样的SQL语句可能导致不同的结果,第二次SQL可能会返回之前不存在的行。

在REPEATABLE READ下采用Next-key Lock加锁

在READ COMMITTED下 只是采用RecordLock。

通过InnoDB存储引擎的Next-Key Locking 机制在应用层面实现唯一性检查。

一个场景:想查询一下某个id是否存在,如果不存在则插入,如果存在则不插入。两种做法:外部加锁或者通过数据库。

数据库方式:

select * from table where col = xx LOCK IN SHARE MODE;
if not found any row:
    # unique for insert value 需要是唯一索引,不然也存在多条插入的情况
    INSERT INTO table VALUES (...);

6.5 锁问题

6.5.1 脏读

脏读:指的是在不同事务下,当前事务可以读到另外事务未提交的数据

6.5.2 不可重复读

不可重复读:指的是在一个事务内两次读到的数据不一样,多了或者少了数据。

6.5.3 丢失更新

丢失更新:简单来说,就是一个事务的更新操作会被另一个事务的更新操作所覆盖,从而导致数据的不一致。
解决方法:让事务在这种情况下的操作变成串行化。比如使用select … FOR UPDATE.

几个概念

多版本技术:一个行记录可能有不止一个快照数据,称之为行多版本技术。

由此带来的并发控制,称之为多版本并发控制 multi version concurrency controll mvcc。

6.6 参考文章

  1. 15.5 InnoDBlocking and Transaction Model
  2. 姜承尧 InnoDB存储引擎第二版 6.锁

Redis与memcache作为缓存时的比较

【说明一个大前提】我觉得拿Redis跟Memcache比较时,是把Redis当做缓存来用。不然一个是缓存一个是持久化,那么比较就失去意义了。

【本文目的】这篇文章是总结了大牛们关于Redis和Memcache的论断,加上部分自己的理解。

1. Redis的性能比Memcache好。

Redis的QPS能够达到10w [数据来源]

Memcache的QPS在6w [数据来源

数据来源:

redis作者的声明

memcache,redis性能测试

自己测试的话Redis选择RedisBenchmark,Memcache可以使用mcperf

原因:

  1. Redis没有使用libevent作为事件处理函数库,而是自己造轮子。libevent因为通用牺牲了不少性能,没有epoll性能好参考
  1. cas问题:CAS是Memcached中比较方便的一种防止竞争修改资源的方法。CAS实现需要为每个cache key设置一个隐藏的cas token,cas相当value版本号,每次set会token需要递增,因此带来CPU和内存的双重开销,虽然这些开销很小,但是到单机10G+ cache以及QPS上万之后这些开销就会给双方相对带来一些细微性能差别。

2. 更多元化的方式使用Redis

除了key/value的形式来使用Redis,还可以使用hash等数据形式。使用Hash更加节约空间。实践 理论

3. Redis的内存效率更高

Redis中特殊编码的小型聚合值内存效率非常高内存优化

真正的内存效率必须基于手头的应用案例来评估

4. Redis禁用磁盘IO,纯内存操作

有一种论断是说 Memcached根本没有磁盘IO操作。实际上Redis禁用磁盘IO操作以后,也是纯内存操作。

5. 作为缓存时,redis LRU vs Slab内存分配器

redis的LRU最近优化了很多,现在非常接近真正的LRU。进一步的信息可阅读: lru-cache 。如果我没理解错的话,Memcached的LRU依旧是根据它的slab分配器来判断数据过期的,因此有时其行为与真正的LRU相差甚远,但我希望这方面的专家能够针对这个问题说点什么。如果你想测试Redis的LRU,在最近几个版本的Redis中可以使用redis-cli的LRU测试模式

6. Memcache的多线程对比Redis的单线程

Memcache是多线程的,在多核上能线性扩展到每秒处理100,000个请求。

如果我们将特殊用例下的Redis看做是Memcached的替代品,执行多个Redis进程也能反驳“Memcached多线程更好”这个观点,或者简单地执行一个Redis进程也行。

参考文章

Redis几点认识误区

关于Redis和Memcache的几点澄清

redis内存优化

缓存替换

零 概述

在业务中不可避免的需要使用缓存,当缓存数据很少时,只需要几台缓存服务器即可满足需求。 但是当业务增多时,就需要N个业务M个缓存服务器。缓存服务器不管使用memcache还是redis,都是需要业务方来管理缓存服务器的host和port。

我们业务中使用的memcache服务器接近千台,还需要业务方告知相关人员来处理memcache的问题(memcache机器宕机或者连接数打满)。而实际上,对于我们业务方而言,只需要使用缓存服务器即可,业务方不需要关注缓存服务器的重启和健康检查。当然了,业务方还是需要关注访问缓存服务器的访问时间。

一 我们对缓存服务的要求

  1. 单个请求耗时在2ms以内
  2. 能够抗住30w qps的压力

二 我们的尝试

I. 首先尝试twitter的twemproxy

  1. Twemproxy介绍

    Twemproxy是一个使用C语言编写、以代理的方式实现的、轻量级的代理服务器,通过引入一个代理层,将应用程序后端的多台Redis实例进行统一管理,使应用程序只需要在Twemproxy上进行操作,而不用关心后面具体有多少个真实的Redis或Memcached实例,从而实现了基于Redis和Memcached的集群服务。当某个节点宕掉时,Twemproxy可以自动将它从集群中剔除,而当它恢复服务时,Twemproxy也会自动连接。
    Twemproxy的缺点:1. 无法平滑地扩容/缩容, 2. 运维不友好,没有控制面板(对于业务方而言,这个不是问题)3. 经过一层代理,对性能有损耗。

  1. 我们测试的结果

    a. 压测方式一:

    使用twitter自家的mcperf压测工具来压测twemproxy。

    压测命令:

    /bin/mcperf --timeout=0.2 --conn-rate=1000 --call-rate=5000000 --num-calls=1000 --num-conns=100 --sizes=u32,1024 -s 10.10.10.10 -p 11211
    

    压测结果:

    |case| 直连memcache|连接twemproxy|
    |—|—|—|
    |数据在32字节和1k字节之间|57750 qps, 耗时100ms以内|25589 qps,耗时100ms以上非常多|
    |数据在4k和8k字节之间|18156 qps | 12376 qps|

    b. 压测方式二:使用n个php进程压测,这种压测方法并不能够得到一个mcperf的准确的qps,但是更接近我们的应用场景,也能够看出一些问题。这种方式压测中发现的问题:

    1) twemproxy所在机器TIME_WAIT和CLOSE_WAIT非常高
    2) 单次请求耗时增加,连接twemproxy的qps小于直连memcache

    TIME_WAITCLOSE_WAIT非常高的原因:

    php端设置超时时间是100ms,即server端100ms内没有响应时,php端就会主动断开连接。上面提到mcperf压测时超过100ms的请求非常多,那么100ms没有收到响应时php段就会主动断开连接。由于twemproxy服务器过于繁忙,没有调用close从而导致twemproxy处于CLOSE_WAIT状态。

    对于TIME_WAIT而言,则是twemproxy向memcache服务器发送请求,等待某个时间(twemproxy中设置),如果没有响应,twemproxy就会主动断开连接;当大量从twemproxy到达memcache的请求超时时,就会出现TIME_WAIT过多。

II 可以考虑方案

我们将可以考虑的方案列一下,360的bada,360的pika,豌豆荚的codis

先来说一下豌豆荚的codis,它跟twemproxy一样都是经过一层代理,首先经过一层代理必然会造成一定的性能损耗,其次在内存拷贝上codis并没有进行优化,官方开发者spinlock在答疑中提到:a. twemproxy只有redis性能一半, b. codis比twemproxy慢

也就是说,不管是twemproxy还是codis,这种经过一层代理的方式都会造成一定的性能损耗;并且在高并发的情况下,代理会带来更多的问题;综合考虑,我们不使用这种代理的方式

再来说一下公司的pika,数据存在硬盘并且底层是rocksdb,单个请求和并发都不满足需求。

再来说一下公司的bada,qps在10w以上,但是平均耗时比redis多。

综合考虑,我们决定测试一下RedisCluster,看是否满足我们的需求。

III RedisCluster

  1. 压测环境
    Redis3.0.5版的Redis-benchmark并不支持对RedisCluster的压力测试。看了一下常见的压测工具都没有直接对RedisCluster进行压测的工具。没有办法,只好使用25台测试机,单机进程总数,每个进程请求数量10w个,通过日志记录每个请求的耗时时间,对日志进行分析。
  2. qps粗略计算

    采用php直接压测,qps的粗略计算:

    机器数*每台机器的php进程数*1000(1s=1000ms)/ 单个请求耗时(单位ms)
    

    如果单次请求是5ms,则预计是25*5*1000/5=2.5w

    当然还可以通过redis-trib.rb得到,将数字部分累加即可。

    $./redis-trib.rb call 10.138.113.202:16001 info | grep -iE "instantaneous_ops_per_sec"
    
    instantaneous_ops_per_sec:3963
    instantaneous_ops_per_sec:3842
    instantaneous_ops_per_sec:4219
    instantaneous_ops_per_sec:3590 
    

    对RedisCluster,Memcache,Redis进行压力测试,主要是测试在高并发情况下三者的耗时。耗时位于0-5ms的比例Redis好于Memcache好于Cluster。解释:

    因为RedisCluster在创建对象的时候需要发送cluster slots命令获取一次集群的节点信息,导致耗时比redis和memcache要久。当然这也是其比memcache明智的地方, 因为memcache需要业务方维护一份节点信息,一旦需要更换节点,则需要业务方进行更改;而cluster的节点信息是由cluster的管理员维护。

    对于单个http请求而言,与memcache和Redis相比,只是多了获取节点信息的操作

  3. 压测中发现phpredis驱动的一个问题:

    压测过程中发现单个redis-cluster节点接受cluster-slots命令是4000qps.(该命令获取集群节点映射信息),当大于4000qps时,耗时就会急剧增加。

    对于这个问题,可以通过增加cluster节点来解决。

    当然最好的方法是修改phpredis的cluster驱动,能够缓存cluster节点信息,不用每次创建对象的时候都发送clusterslots命令

我们最后的选择

  1. 基于上面的压测,我们决定使用RedisCluster,原因有二:

    a. 对于单个http请求(里面可能有多个缓存操作)而言,相比直连memcache或者redis,只是多了一步获取节点信息的耗时,耗时在2ms左右,这点性能损耗我们是可以忍受的。

    b. 对于DBA而言,维护简单,扩容方便;对于业务方而言,我只需要获取RedisCluster的一个节点进行连接即可,跟使用普通Redis差别不大。

  2. 使用RedisCluster中存在的问题

    前端机连接RedisCluster使用短连接,因此造成前端机TIMEWAIT过高,从而导致大量的连接超时。这儿要说明两个问题,第一个是为何使用短连接,因为使用长连接的话就不会存在TIMEWAIT过高的问题。第二个是TIMEWAIT过高的解决方式。

    问题1:使用短连接而不使用长连接

    原因是:使用长连接可能造成前端机或cluster机器端口不够用

    先确定采用长连接时Cluster服务器连接数的计算方式:前端机数量*每台前端机fpm进程数量。如果前端机数量非常多,比如300台,每台前端机fpm128个,那么与某台cluster建立的长连接数量是38400,会造成cluster上连接数过多。
    而对于前端机而言,则可能会造成端口不够用。端口范围由net.ipv4.ip_local_port_range决定)。
    为了避免出现这个问题,我们使用短连接而不是长连接

    问题2:TIMEWAIT

    TIMEWAIT出现原因:发送方主动断开连接时,收到接收方发来的FIN以后处于的状态,这是TCP四次挥手的正常状态。但是由于TIMEWAIT状态时,连接依然占有端口和fd,因此可能会出现端口不够用或套接字不够用的情况,从而导致连接失败。
    TIMEWAIT解决方法:我们采用的是修改net.ipv4.tcp_max_tw_buckets = 5000。

参考文章

codis, redis, twemproxy对比

redis cluster浅析与bada对比

Redis集群技术及Codis实践

PhpRedis集群介绍

redis-benchmark cannot work on Redis Cluster

内核参数 tcp_syncookies-- 默认开启tcp_syncookies

提问:
基于下面的信息,是否可以默认开启/proc/sys/net/ipv4/tcp_syncookies。缺点呢?

一种流行的服务攻击是给你的服务器发送许多(可能伪造的)的SYN包,但从不通过三次握手完成TCP请求。这样很快用尽内核中半打开队列(unp中叫 incompelete connection queue), 阻止了合法连接的完成。因为一个连接不需要完成,那么被攻击机器上就不需要资源,那么这就很容易处理和维护了。

如果设置了tcp_syncookies变量(需要内核使用CONFIG_SYNCOOKIES编译),那么在队列满的时候,内核也能够处理TCP SYN包了,这就是SYN cookie功能的关键所在。

SYN cookie完全不使用SYN队列就能工作。正常的情况,内核会发送SYN|ACK作为对SYN包的响应,但是,SYN cookie是发送一个特定的TCP 序号,这个序号是对源地址目的地址源端口目的端口发送包的时间的编码。一个执行SYN 洪水的攻击者拿不到这个包,也就没法响应。一个合法的连接将会包含这个序号来发送三次握手中的第三个包,服务器将会确认这个包是对合法SYN cookie的响应,并允许建立连接,即使在SYN队列中没有对应的位置了。

开启SYN cookie是一种抵御SYN洪水攻击的简单方式,只是多了一点建立cookie和确认cookie的CPU时间。相比拒绝所有到来的连接,开始SYNcookie是一种明智的选择。

参考文章

https://www.redhat.com/archives/rhl-devel-list/2005-January/msg00447.html
https://ckdake.com/content/2007/disadvantages-of-tcp-syn-cookies.html
http://coolshell.cn/articles/11564.html

php的memcache客户端的几点说明

一. 超时时间

  1. 对于php的memcache客户端,当客户端认为memcached服务器端超时的时候,客户端会主动断开与memcached服务器之间的连接。

  2. 对于php的mecache客户端,这个超时时间并不是最低只能是1秒。http://php.net/manual/zh/memcache.addserver.php 中写到timeout的单位是秒,默认是1秒。可是我们可以通过修改变量来调整超时时间。 【http://tech.uc.cn/?p=326 文章中的关于超时时间的论断是错误的】

    ini_set("memcache.default_timeout_ms", 1000);
    

二. 失败重连机制

先提出两个问题,第一个问题:

有n个memcache服务器的时候,某一个server失败了,
那么还会重试下一个server么?
哪些操作会重试下一个server?

第二个问题:

当n个server都失败的时候,
会影响到之后的其他操作么?

这两个问题,稍后会给出详细的解释。

三. 实践前基础知识

  1. tc命令控制网络延时
 tc qdisc add dev eth0 root netem delay 100ms 所有请求都延时100ms

tc qdisc del dev eth0 root netem delay 100ms 删除所有请求都延时100ms的限制
  1. 实践的php代码

    有两台memcache服务器,分别是10.16.57.128和10.16.57.191.

    // test_mc.php
    <?php
    function catchErrHandler($level, $msg, $file, $line)
    {
        switch ($level)
        {
        case E_NOTICE:
            $hhh = __METHOD__.__LINE__.'|pid='.getmypid().'notice'.'|msg='.$msg.microtime(true)."\n";
            echo $hhh;
            error_log($hhh,3,'/tmp/yk.log');
            //exit;
            break;
        default:
            error_log('default'.__METHOD__.__LINE__.'|pid='.getmypid().'notice'.'|msg='.$msg.microtime(true)."\n",3,'/tmp/yk.log');
            break;
        }
    }
    ini_set("memcache.default_timeout_ms", 99);
    ini_set('memcache.allow_failover', 1);
    set_error_handler('catchErrHandler');
    $mcobj = new Memcache();
    $mcobj->addServer('10.16.57.191', 11211);
    $mcobj->addServer('10.16.57.128', 11211);
    $pid = getmypid();
    $max_qid = 1;
    for ($qid = 0; $qid < $max_qid; $qid++)
    {
        $start = microtime(true);
        $key = 'task_lock' . $qid;
        echo $key . "\n";
        $value = $mcobj->set($key, 1, 300);
        $end = microtime(true);
        $consume = $end - $start;
    }
    

四. 实践

  1. 当一个server失败了,会尝试下一个server么?

    memcache客户端关于重试failover有两个设置:

    memcache.allow_failover boolean 是否在发生错误时(对用户)透明的转移到其他服务器。
    memcache.max_failover_attempts integer 定义在写入和获取数据时最多尝试的服务器次数(即:故障转移最大尝试数),仅和 memcache.allow_failover结合使用。
    

    如果设置 ini_set('memcache.allow_failover', 1);那么会重试下一个server。

  2. 哪些操作会去重试下一个呢?

    从源码来看,php的memcache客户端发送set/add/replace指令的时候会先判断server可用不,如果不可用则走failover,如果只有一台memcache服务器,那么这个逻辑就忽略。
    因为server不可用,那么在之后的逻辑就不会发送请求。

    从实践来看,get操作也会走failover,这也就是 http://tech.uc.cn/?p=326 文章中提到的memcache的分布式问题。

    实践步骤:

    a. 两台memcache机器,10.16.57.128,10.16.57.191。
    10.16.57.191这台机器延时100ms 命令:sudo tc qdisc add dev eth0 root netem delay 100ms

    b. 执行test_mc.php代码

    通过wireshark抓包可以看到,10.16.57.191在三次握手的时候,客户端直接发了RST。然后failover,跟10.16.57.128进行三次握手,之后发送set请求,memcache服务器对其进行处理并返回STORED。add, replace, get操作也是一样的会进行failover。

    set
    replace
    get

  1. 当两个memcache服务器都失败的时候呢??

    retry_interval是服务器连接失败时重试的间隔时间,默认值15秒。一旦一个连接失败,他将会被成功重新连接,或者被标记为失败连接并等待retry_interval秒后再践行一次重连。即,当memcache客户端将server的连接标识为失败以后,在retry_interval时间内都不会再次建立连接,而是直接返回false。

    实践步骤:

    a. 两台memcache机器, 10.16.57.128,10.16.57.191。
    10.16.57.191和10.16.57.128两台机器延时100ms 命令:sudo tc qdisc add dev eth0 root netem delay 100ms

    b. 执行test_mc.php代码

    通过wireshark抓包可以看到,客户端分别对两个memcache服务器发送RST,之后并没有请求再发出,而是直接返回了false

    不会进行重试

五. 连接twemproxy和连接memcache

连接twemproxy和直接连接memcache时的failover情况:

  1. php直连memcache时

    三次握手时间超过设置的超时时间,使用pconnect、addserver和connect三种方式都能准确failover

    即直连memcache时,长短连接都能够准确failover。

  2. php连接twemproxy时,

如果只有一台twemproxy(或者通过lvs连接twemproxy集群),即addServer的时候只有一个ip:port,那么不会走failover。

a. PHP设定的timeout 大于 twemproxy的timeout,memcache故障实体机可以及时剔除,connect不出现false,但是pconnect和addserver进程队列里的将继续false很久后才恢复到true;原因:

虽然twemproxy已经剔除故障机,但是只有一个ip:port, 对于php的memcache客户端而言,他就认为这个ip:port的连接不可用,那么需要在retry_interval时间以后才会发送请求。

对于短连接connect而言,则是新建连接,则不存在这个问题。

b. PHP设定的timeout < twemproxy的timeout,mc故障实体机不一定及时剔除,pconnect、addserver和connect都将继续false很久后才恢复到true。

六. 出现问题

  1. 当切换到twemproxy的时候,发现大量的key写入失败,原因是前端机(跑着php的memcache客户端的机器)的TIME_WAIT非常高,twemproxy机器的TIME_WAIT和CLOSE_WAIT都非常高。

  2. twemproxy机器的TIME_WAIT达到18W, CLOSE_WAIT(被动关闭)也非常高。

    TIME_WAIT高的原因是,twemproxy没有收到memcache服务器的响应,主动断开连接。
    CLOSE_WAIT高的原因是,php的memcache客户端没有收到twemproxy服务器的响应,断开连接,造成twemproxy服务器的CLOSE_WAIT过高, 就是客户端主动关闭,服务器端没有快速响应,因此处于CLOSE_WAIT。

七. 解决方案

  1. 调整内核参数解决TIME_WAIT过高的问题

    net.ipv4.tcp_tw_reuse = 1
    net.ipv4.tcp_syncookies = 1
    net.ipv4.tcp_tw_recycle = 0
    net.ipv4.tcp_max_tw_buckets = 5000
    
  2. CLOSE_WAIT过高的问题,没有解决

  3. 对于twemproxy,可能是我们的使用方式有问题,在使用的过程中出现了twemproxy机器的TIME_WAIT和CLOSE_WAIT都过高,影响服务,因此我们没有使用twemproxy。

Index Condition Pushdown 优化

Index Condition Pushdown是一种优化,针对的是使用索引去表中获取记录rows的优化。没有ICP的话,存储引擎遍历索引来定位数据表中的记录(rows),并将其返回给Mysql的server层,在server层对所获取的记录进行WHERE条件的过滤。
开启ICP后,如果WHERE条件中的某些字段刚好是索引中的字段,那么Mysql的server层会将这些字段下放到存储引擎,存储引擎会使用这些WHERE条件进行过滤,只有满足这些字段的条件,存储引擎才会去数据表中读取整行记录。

ICP的优点就是:减少了存储引擎到达数据表的次数,以及MySQL server层到达存储引擎的次数。

能够使用ICP优化的索引:range,ref, eq_ref和ref_or_null。这些索引都需要获取全表的记录。
能够使用ICP优化的引擎:InnoDB和MyISAM。注意 在MySQL5.6的分区表中不支持,MySQL5.7中支持。
注意:只支持辅助索引。因为ICP优化的目标:减少读取整行的次数,从而降低了IO操作。对于InnoDB的聚簇索引,已经读取了整行记录,因此使用ICP并不会降低IO。

优化器没有使用ICP时,数据访问和提取的过程如下:

1) 当storage engine读取下一行时,首先读取一个索引,然后使用索引在数据表中定位和读取整行数据。
2) sever层评估where条件,如果该行数据满足where条件则使用,否则丢弃。
3) 执行1),直到最后一行数据。

no-icp

优化器使用ICP时,server层会将WHERE条件中某些字段(这些字段在索引中)下推到storage engine层。数据访问和提取过程如下:

1) storage engine从索引中读取下一条索引。
2) storage engine使用索引评估下推的索引条件。如果没有满足wehere条件,则回到1)。只有当索引满足下推的索引条件的时候,才会继续去数据表中读取数据。
3) 如果满足WHERE的索引条件,storage engine通过索引定位数据表的行和读取整行数据并返回给server层。
4) server层评估没有被下推到storage engine层的where条件,如果该行数据满足where条件则使用,否则丢弃。

icp

举例来说明:一个表中包含一个人和其地址信息,表中有一个索引(zipcode, lastname, firstname)。如果我们知道一个人的zipcode,但不确定其lastname,因此可以像下面一样进行查询

SELECT * FROM peope WHERE zipcode = '95054' 
AND lastname LIKE '%etrunica%'
AND address LIKE '%Main Street%';

zipcode = ‘95054’可以使用索引,但是第二部分lastname LIKE ‘%etrunica%’,不使用ICP时不能限制需要扫描的记录的行数,需要获取所有满足zipcode = ‘95054’的所有用户记录。

如果使用ICP,在去数据表读取整行记录之前,MYSQL会检查lastname LIKE ‘%etrunica%’部分。这样避免了去读取不满足lastname 这个条件的数据记录。

默认ICP是开启的。可以通过index_condition_pushdown来设置。

set optimizer_switch='index_condition_pushdown=off';

特别注意:

  1. ICP只能用于二级索引,不能用于主索引
  2. 也不是全部where条件都可以用ICP筛选,如果某where条件的字段不在索引中,当然还是要读取整条记录做筛选,在这种情况下,仍然要到server端做where筛选。
  3. ICP的加速效果取决于在存储引擎内通过ICP筛选掉的数据的比例
  4. 概括之:对于完全命中索引的查询,不需要ICP。比如索引是idx_abc(a,b,c)。查询是where a = 1 and b = 2 则不需要ICP。如果查询是 where a = 1 and b > 3 则会使用ICP.

参考文章

  1. http://dev.mysql.com/doc/refman/5.6/en/index-condition-pushdown-optimization.html
  2. http://ourmysql.com/archives/1351
  3. http://mdba.cn/?p=315
  4. https://mariadb.com/kb/en/mariadb/index-condition-pushdown/

Redis内存优化之小的聚合数据类型的特殊编码

Redis内存优化之小的聚合数据类型的特殊编码

从Redis2.2开始,很多数据类型被优化为使用更小的空间。对于由数字组成的Hashes,Lists, Sets,Sorted Sets 在某些情况下可以使用使用更加有效的内存方式来进行编码,这种更有效的内存方式可以节约最少5倍最大10倍的内存。那么在什么情况这些数字组成的类型可以使用这种更有效的内存方式呢?

当元素个数小于给定的数量时,
当到了一个最大的元素大小时,

从用户和API角度来看,这完全是透明的。因为这是一种CPU/内存的平衡。可以使用redis.conf中的指令来调整元素的最大个数和最大的元素大小。

hash-max-zipmap-entries 512 (hash-max-ziplist-entries for Redis >= 2.6)
hash-max-zipmap-value 64  (hash-max-ziplist-value for Redis >= 2.6)
list-max-ziplist-entries 512
list-max-ziplist-value 64
zset-max-ziplist-entries 128
zset-max-ziplist-value 64
set-max-intset-entries 512

如果一个特定编码值超过了设置的最大值,Redis被自动将其转为普通编码。这个转换过程,对于小的取值非常快,但如果你为了使用特殊编码值而改变了配置,那么就需要允许压力测试来测试这个转换时间了。

实践

进行三个实践。

第一个使用key/value的形式存储1w个数据,占用空间608175字节。

第二个使用hash的形式存储1w个数据,占用空间79424字节

  1. 采用k/v形式
#! /bin/bash

redis-cli info|grep used_memory:

for (( start = 10000; start <20000 ; start++ ))
do
    redis-cli set $start 100 > /dev/null
done

redis-cli info|grep used_memory:
  1. 采用hash形式
#! /bin/bash

redis-cli info|grep used_memory:

for (( start = 10000; start < 20000 ; start++ ))
do
    hash=$((start % 100))
    redis-cli hset $hash $start 100 > /dev/null
done

redis-cli info|grep used_memory:

参考文章

redis内存优化