【Redis】(2)功能特性

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 相关命令

  1. SUBSCRIBE channel [channel …]
    订阅一个或多个频道,一旦进入订阅状态,就不能发布消息,只能执行这些命令( P)SUBSCRIBE / ( P )UNSUBSCRIBE / PING / QUIT
  2. PUBLISH channel message
    向频道中发送消息
  3. PSUBSCRIBE pattern [pattern …]
    订阅给定模式匹配的1个或多个频道
  4. PUBSUB CHANNELS [pattern]
    查看当前被订阅的所有频道,可以使用pattern过滤
  5. PUBSUB NUMSUB [channel-1 … channel-N]
    频道的订阅数量
  6. 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 相关命令

  1. MULTI
    - 事务块开始的标记
  2. EXEC
    - 执行进入队列的命令
    - 然后恢复连接状态为Normal
  3. DISCARD
    - 丢弃先前进入队列的命令
    - 然后恢复连接状态为Normal
  4. WATCH key [key …]
    - 监控key更变
  5. 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 好处

  1. 客户端缓存是将数据缓存在客户端,那么获取数据就非常迅速
  2. 减少服务端的数据查询数量,减小服务端压力

6.2 Redis的实现

  • Redis的客户端缓存称为Tracking
  • 两种模式
  1. 默认模式:服务端记录每个客户端访问的key,当key修改时,服务端发送无效信息到指定的客户端。服务端需要花费内存记录信息。
  2. 广播模式:服务端不需要花费额外内存记录信息,每个客户端订阅指定的key(或符合pattern的key),当key更变时,收到通知消息。

6.3 默认模式

  • 服务端内存占用多,CPU占用少
  • 流程:
  1. 客户端启用Tracking
  2. 服务端记录客户端访问的key
  3. 如果key被修改或删除等,发送无效消息到客户端
  4. 客户端收到消息,删除缓存的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占用多
  • 流程
  1. 客户端启动Tracking并设置广播模式,指定接收key无效消息的前缀
  2. 服务端记录前缀表,每个前缀指向一些客户端
  3. 当key更变时,服务端从前缀表中查找满足条件的客户端
  4. 向这些客户端发送无效消息

7. 大量插入数据

  • Redis 提供管道模式,方便数据传输
  • 使用步骤:
  1. 生成命令的文本文件
set key1 value1
set key2 value2
...
set keyN valueN

官方文档上写需要将这个文本文件转换成符合Redis协议的文件,我没转换也插入成功了。

  1. 使用命令
    cat data.txt | redis-cli --pipe
    100万条插入数据,几秒钟就搞定了

8. 数据分区

8.1 为什么需要进行数据分区?

  • 如果我们有大量数据,那么使用1台机器肯定是不够的, 可以使用多台机器的内存来创建更大的数据库
  • 计算能力和网络带宽扩展到多台计算机。

8.2 有哪些分区方式?

  • 例:我们有4个RedisServer(R0,R1,R2,R3),这4个服务器需要存储用户(user1,user2,…userN)信息
  1. 范围分区:可以将user根据id范围分为4部分,如user1到user10000放入R0,依次类推,这样的话,我们还需要维护一张:记录每个实例的存储范围(range->instance)。
  2. 哈希分区:适用于任何key,不需要指定范围,首先对key进行hash运算,然后再服务器数量取模,即Hash(key) Mod 4,这种更灵活,不需要额外的表

8.3 分区的实现

  1. 客户端分区:直接由客户端发送请求到指定的实例节点
  2. 代理分区: 客户端可以指定Twemproxy代理,由代理转发请求到实例节点
  3. 查询路由(Query routing):使用Redis Cluster,客户端的请求发送到RedisCluster的随机一个实例节点,该实例不会直接转发这个请求,而是将客户端重定向到正确的实例节点,这个过程是用户透明的。
  • 分区的首选方案是RedisCluster,其次是Twemproxy

8.4 分区的缺点

  • 如果多个键在不同的实例上,通常不支持多个键的操作和事务
  • 无法使用单个大键(值很大)对数据集进行分区,因此应该注意分区粒度
  • 使用分区时,数据处理会更加复杂,例如,您必须处理多个RDB / AOF文件,并且要备份数据,则需要从多个实例和主机聚合持久性文件。
  • 添加和删​​除容量可能很复杂。

9. 分布式锁

10. Keyspace通知(响应事件回调)

11. 二级索引

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值