Redis 客户端使用
Java 客户端:Jedis
Jedis 是 Redis 官方首选的 Java 客户端开发包。集成了 redis 的一些命令操作,封装了 redis 的 java 客户端。提供了连接池管理。
Jedis Maven 依赖包
<dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>2.9.0</version> <type>jar</type> <scope>compile</scope> </dependency>
简单使用
/** * @author 又坏又迷人 * 公众号: Java菜鸟程序员 * @date 2020/12/29 * @Description: Redis简单实用 */ public class RedisTest { public static void main(String[] args) { // 1.生成一个Jedis对象,这个对象负责和指定Redis节点进行通信 Jedis jedis = new Jedis("127.0.0.1", 6379); // 2.执行string操作 jedis.set("hello", "world"); String hello = jedis.get("hello"); System.out.println(hello); // world jedis.set("count", "1"); // 自增 jedis.incr("count"); System.out.println(jedis.get("count")); // 2 //3.执行hash操作 jedis.hset("myHash", "f1", "v1"); jedis.hset("myHash", "f2", "v2"); System.out.println(jedis.hgetAll("myHash").toString()); // {f2=v2, f1=v1} //4. list jedis.rpush("myList", "1", "2", "3"); System.out.println(jedis.lrange("myList", 0, -1)); // [1, 2, 3] //5. set jedis.sadd("mySet", "a", "b", "c"); System.out.println(jedis.smembers("mySet")); // [a, c, b] //6. zset jedis.zadd("myzset", 10, "Jack"); jedis.zadd("myzset", 20, "Rose"); jedis.zadd("myzset", 30, "Michelle"); System.out.println(jedis.zrange("myzset", 0, -1)); //[Jack, Rose, Michelle] } }
Jedis 连接池使用
Jedis 直连
Jedis 连接池
方案 | 优点 | 缺点 |
直连 | 简单方便,适用于少量长期连接的场景。 | 存在每次新建/关闭 TCP 开销,资源无法控制,存在泄露的可能。Jedis 对象线程不安全。 |
连接池 | Jedis 预先生成,减低开销使用。连接池的形式保护和控制资源的使用 | 相对于直连,使用相对麻烦,尤其在资源的管理上需要很多参数来保证。一旦规划不合理就会出现问题。 |
/** * @author 又坏又迷人 * 公众号: Java菜鸟程序员 * @date 2020/12/29 * @Description: Redis连接池使用 */ public class RedisPoolTest { // 初始化Jedis连接池,通常来讲JedisPool是单例的. private final static GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig(); private final static JedisPool jedisPool = new JedisPool(poolConfig, "127.0.0.1", 6379); public static void main(String[] args) { Jedis jedis = null; try { jedis = jedisPool.getResource(); jedis.set("hello", "world"); String hello = jedis.get("hello"); System.out.println(hello); // world jedis.set("count", "1"); // 自增 jedis.incr("count"); System.out.println(jedis.get("count")); // 2 } catch (Exception e) { e.printStackTrace(); } finally { if(jedis != null){ //归还资源 jedis.close(); } } } }
Redis 其他功能
慢查询
生命周期
生命周期两点说明:
- 慢查询只发生在第 3 阶段。
- 客户端超时不一定是慢查询,但慢查询是客户端超时的一个可能因素。
两个配置
slowlog-max-len
- 此参数表示慢查询最大保存个数,慢查询日志都是保存在队列中。
- 先进先出。
- 固定长度。
- 保存在内存中(服务器断电会导致慢查询数据丢失)。
slowlog-log-slower-than
- 此参数表示慢查询阈值(单位:微妙),默认配置 10000 微妙。
slowlog-log-slower-than=0
, 表示记录所有命令。slowlog-log-slower-than<0
,表示不记录任何命令。
动态配置:
127.0.0.1:6379> config get slowlog-max-len #查看默认配置 1) "slowlog-max-len" 2) "128" 127.0.0.1:6379> config set slowlog-max-len 1000 #动态修改配置 OK 127.0.0.1:6379> config get slowlog-max-len 1) "slowlog-max-len" 2) "1000" 127.0.0.1:6379> config get slowlog-log-slower-than #查看默认配置 1) "slowlog-log-slower-than" 2) "10000" 127.0.0.1:6379> config set slowlog-log-slower-than 1200 #动态修改配置 OK 127.0.0.1:6379> config get slowlog-log-slower-than 1) "slowlog-log-slower-than" 2) "1200"
三个命令
slowlog get [n]
:获取慢查询队列slowlog len
:获取慢查询队列长度slowlog reset
: 清空慢查询队列
127.0.0.1:6379> slowlog get 10 (empty list or set) 127.0.0.1:6379> slowlog len (integer) 0 127.0.0.1:6379> slowlog reset OK
运维经验
slowlog-max-len
不要设置的过大,默认 10ms,通常设置 1ms。slowlog-log-slower-than
不要设置过小,通常设置 1000 左右。- 理解命令生命周期。
- 定期持久化慢查询。
Pipeline
什么是流水线
1 次网络命令通信模型
批量网络命令通信模型
什么是流水线
流水线的作用
命令 | N 个命令操作 | 1 次 pipeline(N 个命令) |
时间 | N 次网络+N 次命令 | 1 次网络+N 次命令 |
数据量 | 1 条命令 | N 条命令 |
注意
- Redis 命令执行时间是微妙级别的。
- pipeline 每次批量命令条数需要控制(注意网络传输)。
Pipeline-Jedis 客户端实现
没有使用 Pipeline用时:29707
/** * @author 又坏又迷人 * 公众号: Java菜鸟程序员 * @date 2020/12/29 * @Description: */ public class PipelineRedisTest { // 初始化Jedis连接池,通常来讲JedisPool是单例的. private final static GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig(); private final static JedisPool jedisPool = new JedisPool(poolConfig, "47.110.41.15", 6379); public static void main(String[] args) { long start = System.currentTimeMillis(); Jedis jedis = null; try { jedis = jedisPool.getResource(); for (int i = 0; i < 1000; i++) { jedis.hset("hashkey", "field_" + i, "value_" + i); } } catch (Exception e) { e.printStackTrace(); } finally { if (jedis != null) { //归还资源 jedis.close(); } } long end = System.currentTimeMillis(); System.out.println(end - start); //29707 } }
使用 Pipeline用时:3161
/** * @author 又坏又迷人 * 公众号: Java菜鸟程序员 * @date 2020/12/29 * @Description: */ public class PipelineRedisTest { // 初始化Jedis连接池,通常来讲JedisPool是单例的. private final static GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig(); private final static JedisPool jedisPool = new JedisPool(poolConfig, "47.110.41.15", 6379); public static void main(String[] args) { long start = System.currentTimeMillis(); Jedis jedis = null; try { jedis = jedisPool.getResource(); for (int i = 0; i < 100; i++) { Pipeline pipeline = jedis.pipelined(); for (int j = i * 100; j < (i + 1) * 100; j++) { pipeline.hset("pipelinekey", "field_" + j, "value_" + j); } pipeline.syncAndReturnAll(); } } catch (Exception e) { e.printStackTrace(); } finally { if (jedis != null) { //归还资源 jedis.close(); } } long end = System.currentTimeMillis(); System.out.println(end - start); //3161 } }
与原生 M 操作对比
mset、mget、hmset、hmget 等都是原子操作。
pipeline 命令是非原子操作,但是命令返回顺序能够保证。
使用建议
- 注意每次 pipeline 携带的数据量
- pipeline 每次只能作用在一个 Redis 节点上
- M 命令操作与 pipeline 的区别
- mset、mget、hmget、hmset 等命令是:n 次网络时间+n 次命令时间。
- pipeline 操作命令是:1 次网络时间+n 次命令时间。
发布订阅
- 发布者(publisher)
- 订阅者(subscriber)
- 频道(channel)
模型
多个订阅者订阅一个频道
发布者 publisher 只要发布了消息,所有订阅了这个频道 channel 的订阅者都能收到消息。
一个订阅者可以订阅多个频道
一个订阅者可以订阅多个频道,当发布者发布不同消息到多个频道,订阅者可以接受多个频道消息。
发布订阅与消息队列
Redis 还可以用作消息队列,所有消息订阅者是去抢队列里面的消息。
相关 API
subscribe
首先订阅频道。
127.0.0.1:6379> subscribe baidu Reading messages... (press Ctrl-C to quit) 1) "subscribe" 2) "baidu" 3) (integer) 1 1) "message" 2) "baidu" 3) "hello" 1) "message" 2) "baidu" 3) "world" 1) "message" 2) "baidu" 3) "java" 1) "message" 2) "baidu" 3) "python" 1) "message" 2) "baidu" 3) "go"
publish
发送消息。
127.0.0.1:6379> publish baidu hello (integer) 1 127.0.0.1:6379> publish baidu world (integer) 1 127.0.0.1:6379> publish baidu java (integer) 1 127.0.0.1:6379> publish baidu python (integer) 1 127.0.0.1:6379> publish baidu go (integer) 1
使用 unsubscribe 取消订阅频道
127.0.0.1:6379> unsubscribe baidu 1) "unsubscribe" 2) "baidu" 3) (integer) 0
其它 API
psubscribe [pattern...]
:订阅指定规则的频道。punsubscribe [pattern...]
:退订指定的模式。pubsub channels
:列出至少有一个订阅者的频道。pubsub numsub [channel...]
:列出给定频道的订阅者数量。
Bitmap
位图
位图并不是一种数据结构,其实就是一种普通的字符串,也可以说是 byte 数组。
- b 的 ASCII=98 对应的二进制为:01100010
- i 的 ASCII=105 对应的二进制为:01101001
- g 的 ASCII=103 对应的二进制为:01100111
127.0.0.1:6379> set hello big OK 127.0.0.1:6379> getbit hello 0 (integer) 0 127.0.0.1:6379> getbit hello 1 (integer) 1
setbit命令
对 key
所储存的字符串值,设置或清除指定偏移量上的位(bit)。
位的设置或清除取决于 value
参数,可以是 0
也可以是 1
。
当 key
不存在时,自动生成一个新的字符串值。
字符串会进行伸展(grown)以确保它可以将 value
保存在指定的偏移量上。当字符串值进行伸展时,空白位置以 0
填充。
offset
参数必须大于或等于 0
,小于 2^32 (bit 映射被限制在 512 MB 之内)。
返回指定偏移量原来储存的位。
getbit命令
对 key
所储存的字符串值,获取指定偏移量上的位(bit)。
当 offset
比字符串值的长度大,或者 key
不存在时,返回 0
。
bitcount命令
计算给定字符串中,被设置为 1
的比特位的数量。
一般情况下,给定的整个字符串都会被进行计数,通过指定额外的 start
或 end
参数,可以让计数只在特定的位上进行。
不存在的 key
被当成是空字符串来处理,因此对一个不存在的 key
进行 BITCOUNT
操作,结果为 0
。
bitop命令
对一个或多个保存二进制位的字符串 key
进行位元操作,并将结果保存到 destkey
上。
operation
可以是 AND
、 OR
、 NOT
、 XOR
这四种操作中的任意一种:
BITOP AND destkey key [key ...]
,对一个或多个key
求逻辑并,并将结果保存到destkey
。BITOP OR destkey key [key ...]
,对一个或多个key
求逻辑或,并将结果保存到destkey
。BITOP XOR destkey key [key ...]
,对一个或多个key
求逻辑异或,并将结果保存到destkey
。BITOP NOT destkey key
,对给定key
求逻辑非,并将结果保存到destkey
。
除了 NOT
操作之外,其他操作都可以接受一个或多个 key
作为输入。
返回保存到 destkey
的字符串的长度,和输入 key
中最长的字符串长度相等。
bitpos命令
返回位图中第一个值为 bit
的二进制位的位置。
在默认情况下, 命令将检测整个位图, 但用户也可以通过可选的 start
参数和 end
参数指定要检测的范围。
独立用户统计
- 使用 set 和 bitmap
- 1 亿用户,5 千万独立
数据类型 | 每个 UserId 占用空间 | 需要存储的用户量 | 全部内存量 |
set | 32 位 | 50,000,000 | 32 位 * 50,000,000 = 200MB |
bitmap | 1 位 | 100,000,000 | 1 位 * 100,000,000 = 12.5MB |
但是如果只有 10 万独立用户的话,结果就不一样了。
数据类型 | 每个 UserId 占用空间 | 需要存储的用户量 | 全部内存量 |
set | 32 位 | 100,000 | 32 位 * 100,000 = 4MB |
bitmap | 1 位 | 100,000,000 | 1 位 * 100,000,000 = 12.5MB |
使用经验
- type=string 类型,最大 512MB。
- 注意 setbit 时的偏移量,可能有较大耗时。
- 位图不是绝对好,合理的场景使用合理的技术。
HyperLogLog
Redis HyperLogLog 是用来做基数统计的算法,HyperLogLog 的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定的、并且是很小的。
在 Redis 里面,每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 2^64 个不同元素的基 数。这和计算基数时,元素越多耗费内存就越多的集合形成鲜明对比。
但是,因为 HyperLogLog 只会根据输入元素来计算基数,而不会储存输入元素本身,所以 HyperLogLog 不能像集合那样,返回输入的各个元素。
PFADD命令
将任意数量的元素添加到指定的 HyperLogLog 里面。
作为这个命令的副作用, HyperLogLog 内部可能会被更新, 以便反映一个不同的唯一元素估计数量(也即是集合的基数)。
如果 HyperLogLog 估计的近似基数(approximated cardinality)在命令执行之后出现了变化, 那么命令返回 1
, 否则返回 0
。 如果命令执行时给定的键不存在, 那么程序将先创建一个空的 HyperLogLog 结构, 然后再执行命令。
- 如果给定键已经是一个 HyperLogLog , 那么这种调用不会产生任何效果。
- 但如果给定的键不存在, 那么命令会创建一个空的 HyperLogLog , 并向客户端返回
1
。
如果 HyperLogLog 的内部储存被修改了, 那么返回 1 , 否则返回 0 。
API:PFADD key element [element …]
127.0.0.1:6379> pfadd data 'java' 'python' 'go' (integer) 1 127.0.0.1:6379> pfcount data (integer) 3
PFCOUNT命令
当 PFCOUNT key [key …] 命令作用于单个键时, 返回储存在给定键的 HyperLogLog 的近似基数, 如果键不存在, 那么返回 0
。
当 PFCOUNT key [key …] 命令作用于多个键时, 返回所有给定 HyperLogLog 的并集的近似基数, 这个近似基数是通过将所有给定 HyperLogLog 合并至一个临时 HyperLogLog 来计算得出的。
命令返回的可见集合(observed set)基数并不是精确值, 而是一个带有 0.81% 标准错误(standard error)的近似值。
返回给定 HyperLogLog 包含的唯一元素的近似数量。
API:PFCOUNT key [key …]
127.0.0.1:6379> pfadd data 'java' 'python' 'go' (integer) 1 127.0.0.1:6379> pfcount data (integer) 3
PFCOUNT命令
将多个 HyperLogLog 合并(merge)为一个 HyperLogLog , 合并后的 HyperLogLog 的基数接近于所有输入 HyperLogLog 的可见集合(observed set)的并集。
合并得出的 HyperLogLog 会被储存在 destkey
键里面, 如果该键并不存在, 那么命令在执行之前, 会先为该键创建一个空的 HyperLogLog 。
字符串回复:返回 OK
。
API:PFMERGE destkey sourcekey [sourcekey …]
127.0.0.1:6379> PFADD nosql "Redis" "MongoDB" "Memcached" (integer) 1 127.0.0.1:6379> PFADD RDBMS "MySQL" "MSSQL" "PostgreSQL" (integer) 1 127.0.0.1:6379> PFMERGE databases nosql RDBMS OK 127.0.0.1:6379> pfcount databases (integer) 6
使用经验
- 是否能容忍错误:HyperLogLog 错误率为:0.81%
- 是否需要单条数据:如果需要单条数据,就不适合使用 HyperLogLog。
GEO
GEO 主要用于存储地理位置信息,并对存储的信息进行操作。
GEOADD命令
将给定的空间元素(纬度、经度、名字)添加到指定的键里面。 这些数据会以有序集合的形式被储存在键里面。
- 有效的经度介于 -180 度至 180 度之间。
- 有效的纬度介于 -85.05112878 度至 85.05112878 度之间。
当用户尝试输入一个超出范围的经度或者纬度时, GEOADD
命令将返回一个错误。
返回新添加到键里面的空间元素数量, 不包括那些已经存在但是被更新的元素。
GEOPOS命令
从键里面返回所有给定位置元素的位置(经度和纬度)。
因为 GEOPOS
命令接受可变数量的位置元素作为输入, 所以即使用户只给定了一个位置元素, 命令也会返回数组回复。
GEOPOS
命令返回一个数组, 数组中的每个项都由两个元素组成: 第一个元素为给定位置元素的经度, 而第二个元素则为给定位置元素的纬度。 当给定的位置元素不存在时, 对应的数组项为空值。
GEODIST命令
返回两个给定位置之间的距离。
如果两个位置之间的其中一个不存在, 那么命令返回空值。
指定单位的参数 unit
必须是以下单位的其中一个:
m
表示单位为米。km
表示单位为千米。mi
表示单位为英里。ft
表示单位为英尺。
如果用户没有显式地指定单位参数, 那么 GEODIST
默认使用米作为单位。
GEODIST
命令在计算距离时会假设地球为完美的球形, 在极限情况下, 这一假设最大会造成 0.5% 的误差。
计算出的距离会以双精度浮点数的形式被返回。 如果给定的位置元素不存在, 那么命令返回空值。
GEORADIUS命令
以给定的经纬度为中心, 返回键包含的位置元素当中, 与中心的距离不超过给定最大距离的所有位置元素。
范围可以使用以下其中一个单位:
m
表示单位为米。km
表示单位为千米。mi
表示单位为英里。ft
表示单位为英尺。
在给定以下可选项时, 命令会返回额外的信息:
WITHDIST
: 在返回位置元素的同时, 将位置元素与中心之间的距离也一并返回。 距离的单位和用户给定的范围单位保持一致。WITHCOORD
: 将位置元素的经度和维度也一并返回。WITHHASH
: 以 52 位有符号整数的形式, 返回位置元素经过原始 geohash 编码的有序集合分值。 这个选项主要用于底层应用或者调试, 实际中的作用并不大。
命令默认返回未排序的位置元素。 通过以下两个参数, 用户可以指定被返回位置元素的排序方式:
ASC
: 根据中心的位置, 按照从近到远的方式返回位置元素。DESC
: 根据中心的位置, 按照从远到近的方式返回位置元素。
在默认情况下, GEORADIUS
命令会返回所有匹配的位置元素。 虽然用户可以使用 COUNT <count>
选项去获取前 N 个匹配元素, 但是因为命令在内部可能会需要对所有被匹配的元素进行处理, 所以在对一个非常大的区域进行搜索时, 即使只使用 COUNT
选项去获取少量元素, 命令的执行速度也可能会非常慢。 但是从另一方面来说, 使用 COUNT
选项去减少需要返回的元素数量, 对于减少带宽来说仍然是非常有用的。
返回值
GEORADIUS
命令返回一个数组, 具体来说:
- 在没有给定任何
WITH
选项的情况下, 命令只会返回一个像["New York","Milan","Paris"]
这样的线性(linear)列表。 - 在指定了
WITHCOORD
、WITHDIST
、WITHHASH
等选项的情况下, 命令返回一个二层嵌套数组, 内层的每个子数组就表示一个元素。
在返回嵌套数组时, 子数组的第一个元素总是位置元素的名字。 至于额外的信息, 则会作为子数组的后续元素, 按照以下顺序被返回:
- 以浮点数格式返回的中心与位置元素之间的距离, 单位与用户指定范围时的单位一致。
- geohash 整数。
- 由两个元素组成的坐标,分别为经度和纬度。