文章目录
1. Pipelining 流水线
1.1 使用流水线的好处
- 由于RedisServer是基于TCP协议、Server/Client模型的服务器,在建立TCP连接时会有往返时间(RTT, Round Trip Time)的开销,如果有大批量的命令需要执行,这些RTT将带来大量的时间成本。
- 因此,可以将批量的命令包装成1个请求,发送给Redis服务器,处理后,打包成1个响应,返回给客户端,减少
RTT开销
。 - 另外,RedisServer依次处理单独的命令,将会进行频繁的系统调用,存在
上下文切换开销
,使用流水线可以减少上下文切换开销。
1.2 性能
- 使用流水线比不使用流水线要
快
。 - 使用流水线时要注意,RedisServer将批量命令中每个命令的结果暂存在内存,计算完毕后才一次性返回,因此要注意
RedisServer的内存能否容纳批量命令的结果
。
不使用Pipeline:
使用Pipeline:
性能对比:
1.3 使用
Jedis jedis = new Jedis(URI.create("tcp://localhost:6379"));
Pipeline pipelined = jedis.pipelined();
Response<String> ping1 = pipelined.ping();
Response<String> ping2 = pipelined.ping();
Response<String> ping3 = pipelined.ping();
Thread.sleep(2000);
pipelined.sync();
pipelined.close();
System.out.println(ping1.get());
System.out.println(ping2.get());
System.out.println(ping3.get());
2. 消息发布订阅模型
2.1 基本定义
- 一个频道可以被多个客户端订阅,多个客户端可以向同一个频道发送消息
2.2 相关命令
SUBSCRIBE
channel [channel …]
订阅一个或多个频道,一旦进入订阅状态,就不能发布消息,只能执行这些命令( P)SUBSCRIBE / ( P )UNSUBSCRIBE / PING / QUITPUBLISH
channel message
向频道中发送消息PSUBSCRIBE
pattern [pattern …]
订阅给定模式匹配的1个或多个频道PUBSUB CHANNELS
[pattern]
查看当前被订阅的所有频道,可以使用pattern过滤PUBSUB NUMSUB
[channel-1 … channel-N]
频道的订阅数量PUBSUB NUMPAT
使用PSUBSCRIBE 订阅的数量
2.3 使用
Jedisjedis = new Jedis(URI.create("tcp://localhost:6379"));
JedisPubSub pubSub = new JedisPubSub(){
@Override
public void onMessage(String channel, String message) {
super.onMessage(channel, message);
System.out.println(message);
}
};
ScheduledExecutorService executorService = new ScheduledThreadPoolExecutor(1);
executorService.schedule(() -> {
// jedis.publish("c1", "hello");
pubSub.unsubscribe("c1");
}, 3, TimeUnit.SECONDS);
executorService.shutdown();
jedis.subscribe(pubSub,"c1");
- JedisPubSub是一个抽象类,订阅频道后,会收到接收消息等方法的回调
3. 内存优化方案
3.1 数据结构优化
- Redis将许多数据类型进行了优化,以
减少内存占用
。 - 对于
Hash、List、数字组成的Set、SortedSet
这些数据类型来说,当容量
小于一个值,并且每个元素大小
小于一个值时(conf文件中配置),Redis会将其内部的数据结构转变为一种节省内存(但需要更多计算)的方式,最多可以节省10倍的内存。 - 这是CPU(
时间
)和内存(空间
)的折中方案,相当于用时间换空间,但时间也不会增长太多,因为数据量小 - 这对于用户是不可见的,但可以通过redis.conf配置。
hash/set/zset/list-max-ziplist-entries 512 # 容量
hash/set/zset/list-max-ziplist-value 64 # 元素大小
# 同时满足小于这个容量,并且元素大小于这个值时,使用ziplist优化内存
3.2 支持位操作
3.3 尽可能使用Hash
- 如果我们有N个值为String的键值对需要存入Redis,那么有两种储存方式:
- 第一种:使用N个Key,存入Redis
- 第二种:使用1个Key,将N个键值对存入Hash
-第二种内存效率更高
。 - 问题:这两种方式哪个更好?
第一种方式直接使用了hash表储存,因此取值的时间复杂度为O(1)
第二种方式在N较小时,直接使用线性数组储存,因此内存效率更高
,但时间复杂度来到了O(N),但由于N较小,HGET,HSET的均摊时间复杂度仍然为O(1)。另一个好处是,线性数组可以很好地与CPU缓存
结合使用,进一步减少时间开销。 - Redis允许用户根据自己的
时间或空间需求
,考虑数据存储方案,例如将N个键值对,进行分片,一部分存入Key,一部分存入Hash。
4. 数据淘汰策略
4.1 最大内存配置
- 在redis.conf中可以指定允许redis的数据使用的最大内存
maxmemory 100mb
- maxmemory默认为0,表示没有内存限制(64位系统),32位系统有隐式3GB内存限制
4.2 淘汰策略
- 名词解释
- LRU:Least Recently Used,最近最少使用,关注对象最后一次访问时间
- LFU:Least Frequently Used,使用频率最少,关注对象访问次数
- 执行写入命令没有更多空间时,需要执行策略
- 两个范围:allkeys、volatile
- 3种策略:LRU、LFU、RANDOM
策略 | 解释 |
---|---|
noeviction | 不淘汰,返回错误 |
allkeys-lru | 从全部key中,删除最近最少使用的键值 |
volatile-lru | 在设置过期时间的Key中,删除最近最少使用的键值 |
allkeys-random | 从全部Key中,随机删除键值 |
volatile-random | 在设置过期时间的Key中,随机删除键值 |
volatile-ttl | 删除设置了过期时间的Key,按TTL顺序,小的先删 |
allkeys-lfu | 在全部key中,删除使用频率最少的键值 |
volatile-lfu | 在设置了过期时间的Key中,删除使用频率最少的键值 |
4.3 Redis的LRU和LFU
4.3.1 近似LRU算法
- Redis认为完全的LRU算法消耗内存太多,因此采用近似的LRU算法
- Redis从所有的key中进行采样,然后淘汰最后一次访问时间最久远的key
- 可以通过
maxmemory-samples
配置采样数量,采样数量越多,结果就越接近真正的LRU算法。
4.3.2 LFU算法
- 每个对象仅仅使用几个位的数据,来保存一个
概率计数器
,预估对象访问的概率。 - 该计数器有衰减周期,随着时间而减少
- 该技术器存在饱和值
- 可以通过两个参数配置
lfu-log-factor 10 # 对数因子
lfu-decay-time 1 # 衰减时间
- 衰减时间为1表示每过1分钟,频率衰减一次
- 对数因子表示,计数器的值随着访问次数增长的快慢,对数因子越小,计数器达到饱和所需的访问次数越小,对数因此越大,计数器达到饱和需要更多的访问次数。
5. 事务
5.1 基本定义
- 事务中的命令是
顺序
执行的,当Redis在执行一个事务中时
,不会响应其他客户端的请求,
这保证了事务中的所有命令是一个独立的
,隔离的
操作。 - Redis事务是满足
原子性
,事务中的命令要么全做,要么全不做。只要调用EXEC命令,事务就全做,在调用EXEC命令之前失去连接或没调用EXEC,事务就不做。 - 当使用AOF文件时,Redis将事务写在磁盘上,如果Redis意外退出(系统崩溃或进程直接被杀死),那可能只有部分操作写在了AOF文件中,这种情况下,Redis在下次启动时会报错,可以使用redis-check-aof工具,移除部分事务。
- Redis使用CAS
乐观锁
保证事务的原子性。
5.2 事务中出错
- 事务期间有两种类型的错误:
- 在EXEC调用前,语法错误、内存不足等情况,命令入队列失败
- 在EXEC调用后,对key进行错误的操作 - 对于第一种错误,Redis会记住并累计第一种错误,然后在EXEC期间,
直接丢弃该事务,并返回错误
- 对于第二种错误,Redis会
跳过错误的命令,其他正确的命令依然会被执行
。 - 对于第二种错误,Redis为什么不支持回滚?
1. 仅当使用错误的语法,或针对持有错误数据类型的键调用Redis命令时,该命令才能失败,这实际上意味着失败的命令是`编程错误的结果`,这种错误大部分出现在开发环境中而`不是生产环境中`,即在生产环境中需要解决这些错误。
2. 由于没有实现回滚功能,Redis在内部得到了`简化,速度更快`。
5.3 使用CAS乐观锁
- 在多个客户端访问同一个key时,可能会发生资源争用,从而引发
线程不安全
问题。 - Redis使用
WATCH
命令为key加乐观锁,底层采用CAS
实现 - 例:
WATCH mykey
val = GET mykey
val = val + 1
MULTI
SET mykey $val
EXEC
WATCH
命令监控是否有其他线程(客户端)在WATCH和EXEC期间
修改了myKey的值,如果值被修改,那么该事务失败。然后我们只需要重试该事务即可,直到成功。
- 大部分情况下,不同客户端访问不同的key,因此资源争用很少发生,效率较高。
# 假设a的原始值为1
# 客户端1使用WATCH+事务修改a的值
cli1: WATCH a
# 客户端1执行WATCH后,客户端2修改了a的值
cli2: set a 2
# 然后客户端1继续执行事务
cli1: MULTI
cli1: SET a 10
cli1: EXEC
# 在EXEC调用后,返回nil,说明在WATCH期间,a的值被修改了,事务执行失败,此时我们可以重试整个操作,直到修改成功。
- 当WATCH一个
volatile的key
(带有过期时间的key)时,即使EXEC之前key过期,EXEC仍然会执行
当EXEC调用时
,所有的key都被UNWATCH,不管事务是否执行成功客户端断开连接时
,所有的key也都被UNWATCH
5.2 相关命令
MULTI
- 事务块开始的标记EXEC
- 执行进入队列的命令
- 然后恢复连接状态为NormalDISCARD
- 丢弃先前进入队列的命令
- 然后恢复连接状态为NormalWATCH
key [key …]
- 监控key更变UNWATCH
- 刷新所有被WATCH的key
5.3 使用
Jedis jedis = new Jedis(URI.create("tcp://localhost:6379"));
Transaction transaction = jedis.multi();
transaction.set("a", "2");
transaction.incr("a");
transaction.exec();
//transaction.execGetResponse();
6. 客户端缓存
6.1 好处
- 客户端缓存是将数据缓存在客户端,那么
获取数据就非常迅速
。 - 减少服务端的数据查询数量,
减小服务端压力
。
6.2 Redis的实现
- Redis的客户端缓存称为
Tracking
- 两种模式
- 默认模式:服务端记录
每个客户端
访问的key,当key修改时,服务端发送无效信息到指定的客户端
。服务端需要花费内存
记录信息。 - 广播模式:服务端
不需要花费额外内存
记录信息,每个客户端订阅指定的key(或符合pattern的key),当key更变时,收到通知消息。
6.3 默认模式
- 服务端
内存占用多,CPU占用少
- 流程:
- 客户端启用Tracking
- 服务端记录客户端访问的key
- 如果key被修改或删除等,发送无效消息到客户端
- 客户端收到消息,删除缓存的key
- 例子:
Client 1 -> Server: CLIENT TRACKING ON # 客户端1启用Tracking
Client 1 -> Server: GET foo # 客户端1获取并缓存foo,服务端记录Client1访问了foo
Client 2 -> Server: SET foo SomeOtherValue # 客户端2修改foo
Server -> Client 1: INVALIDATE "foo" # 服务端通知客户端1 foo无效
6.4 广播模式
- 服务端
内存占用少,CPU占用多
- 流程
- 客户端启动Tracking并设置广播模式,指定接收key无效消息的前缀
- 服务端记录
前缀表
,每个前缀指向一些客户端 - 当key更变时,服务端从前缀表中查找满足条件的客户端
- 向这些客户端发送无效消息
7. 大量插入数据
- Redis 提供管道模式,方便数据传输
- 使用步骤:
- 生成命令的文本文件
set key1 value1
set key2 value2
...
set keyN valueN
官方文档上写需要将这个文本文件转换成符合Redis协议的文件,我没转换也插入成功了。
- 使用命令
cat data.txt | redis-cli --pipe
100万条插入数据,几秒钟就搞定了
8. 数据分区
8.1 为什么需要进行数据分区?
- 如果我们有大量数据,那么使用1台机器肯定是不够的,
可以使用多台机器的内存来创建更大的数据库
。 计算能力和网络带宽
扩展到多台计算机。
8.2 有哪些分区方式?
- 例:我们有4个RedisServer(R0,R1,R2,R3),这4个服务器需要存储用户(user1,user2,…userN)信息
范围分区
:可以将user根据id范围分为4部分,如user1到user10000放入R0,依次类推,这样的话,我们还需要维护一张表
:记录每个实例的存储范围(range->instance)。哈希分区
:适用于任何key,不需要指定范围
,首先对key进行hash运算,然后再服务器数量取模,即Hash(key) Mod 4,这种更灵活,不需要额外的表
。
8.3 分区的实现
- 客户端分区:直接由客户端发送请求到指定的实例节点
- 代理分区: 客户端可以指定Twemproxy代理,由代理转发请求到实例节点
- 查询路由(Query routing):使用
Redis Cluster
,客户端的请求发送到RedisCluster的随机一个实例节点,该实例不会直接转发这个请求,而是将客户端重定向到正确的实例节点,这个过程是用户透明的。
分区的首选方案是RedisCluster,其次是Twemproxy
8.4 分区的缺点
- 如果多个键在不同的实例上,通常不支持多个键的操作和事务
- 无法使用单个大键(值很大)对数据集进行分区,因此应该注意分区粒度
- 使用分区时,数据处理会更加复杂,例如,您必须处理多个RDB / AOF文件,并且要备份数据,则需要从多个实例和主机聚合持久性文件。
- 添加和删除容量可能很复杂。