Redis 全局命令、数据类型、内部编码、spring操作使用、使用场景

Redis 全局命令、数据类型、内部编码、spring操作使用、使用场景

  • 一、全局命令
    • 1、查看所有键 keys *
    • 2、键总数dbsize
    • 3、检查键是否存在 exists key
    • 4、删除键del key
    • 5、键过期expire key seconds
    • 6、键的数据结构类型type key
    • 7、数据结构和内部编码
    • 单线程架构
  • 二、Redis数据结构 - 字符串(string)
    • 1、概述
    • 2、基本命令
        • 2.1、设置过期时间
        • 2.2、批量设置值
        • 2.3、批量获取值
    • 3、string的内部编码
    • 4、string 客户端操作
    • 5、Spring中操作redis的字符串
        • 5.1、spring-redis-string.xml
        • 5.2、SpringRedisStringsDemo
    • 6、Redis中对整数和浮点型数字的支持
      • Spring中操作redis的字符串的加减运算
    • 注意
    • 常见使用场景
        • 缓存
        • 计数器
        • 限流
        • session共享
  • 三、哈希(hash)
    • 1、概述
    • 2、客户端操作hash
    • 3、Redis hash 结构命令
    • 4、Spring操作reids的hash
        • 4.1、 修改defaultSerializer
        • 4.2、 操作hash
    • 注意
  • 四、链表(linked-list)
    • 1、概述
    • 2、Redis 关于链表的命令
    • 3、使用 Spring 操作 Redis 链表命令
    • 4、链表的阻塞命令
    • 5、使用Spring 操作Redis 链表阻塞命令
    • 注意
  • 五、集合(set)
    • 1、概述
    • 2、常用集合命令
    • 3、交集、并集和差集保存命令的用法
    • 4、Spring中操作Redis 集合命令
    • 注意
  • 六、有序集合(zset)
    • 1、概述
    • 2、有序集合的数据结构
    • 3、Redis 有序集合的部分命令
    • 4、spring-data-redis 对有序集合的封装
    • 5、使用 Spring 操作有序集合
    • 注意
  • 七、基数HyperLogLog
    • 1、概述
    • 2、Redis 的 Hyperloglog 命令
    • 3、Spring 中操作基数
    • 注意

一、全局命令

Redis 是一个Key-Value内存数据库,不管是何种数据结构,对于键来说有一些通用的命令。举几个例子:

1、查看所有键 keys *

keys * 命令会将所有的键输出

127.0.0.1:6379> set  key1 artisan
OK
127.0.0.1:6379> set key2 artisan2
OK
127.0.0.1:6379> set key3 artisan3
OK
127.0.0.1:6379> keys *
1) "key3"
2) "key2"
3) "key1"
127.0.0.1:6379> 

keys命令会遍历所有键, 所以它的时间复杂度是O(n) , 当Redis保存了大量键时, 线上环境 禁止使用。

2、键总数dbsize

dbsize命令会返回当前数据库中键的总数

127.0.0.1:6379> rpush artisanlist a d d d x we  sdsd  dsd sddfdg  dsfdf  sdfsdfsdf  sdfsfdf   ---- 返回增加的list的个数
(integer) 12
127.0.0.1:6379> dbsize   --加上刚才设置的3个,一共4个key
(integer) 4
127.0.0.1:6379> keys *
1) "artisanlist"
2) "key3"
3) "key2"
4) "key1"
127.0.0.1:6379> 

dbsize命令在计算键总数时不会遍历所有键, 而是直接获取Redis内置的键总数变量, 所以dbsize命令的时间复杂度是O(1)

3、检查键是否存在 exists key

127.0.0.1:6379> exists  artisanlist
(integer) 1
127.0.0.1:6379> exists sdsdsd
(integer) 0
127.0.0.1:6379> 

如果键存在则返回1, 不存在则返回0

4、删除键del key

127.0.0.1:6379> del key1
(integer) 1
127.0.0.1:6379> del xdsdse
(integer) 0
127.0.0.1:6379> 

del是一个通用命令, 无论值是什么数据结构类型, del命令都可以将其删除

27.0.0.1:6379> exists artisanlist 
(integer) 1
127.0.0.1:6379> del artisanlist
(integer) 1
127.0.0.1:6379> exists artisanlist 
(integer) 0
127.0.0.1:6379> 

返回结果为成功删除键的个数, 假设删除一个不存在的键, 返回0,如上。

同时del命令可以支持删除多个键

127.0.0.1:6379> set x 123
OK
127.0.0.1:6379> set y 456
OK
127.0.0.1:6379> set z 789
OK
127.0.0.1:6379> del x y z 
(integer) 3
127.0.0.1:6379> 

5、键过期expire key seconds

Redis支持对键添加过期时间, 当超过过期时间后, 会自动删除键

ttl命令会返回键的剩余过期时间 ,它有三种返回值

  1. 大于等于0的整数: 键剩余的过期时间。
  2. -1: 键没设置过期时间。
  3. -2: 键不存在
127.0.0.1:6379> set name artisan
OK
127.0.0.1:6379> expire name 20
(integer) 1
127.0.0.1:6379> ttl name
(integer) 16
.....
127.0.0.1:6379> ttl name
(integer) 1
127.0.0.1:6379> ttl name   -- -2说明不存在
(integer) -2
127.0.0.1:6379> get name  --- get为空 
(nil)
127.0.0.1:6379> 

上面的 set name artisan expire name 20 两句 可以简化成 set name artisan EX 20

语法如下:

  • set key value [expiration EX seconds|PX milliseconds] [NX|XX
127.0.0.1:6379> set name artisan EX 20
OK
127.0.0.1:6379> ttl name
(integer) 16
127.0.0.1:6379> get name
"artisan"
.......
127.0.0.1:6379> ttl name
(integer) 1
127.0.0.1:6379> ttl name
(integer) -2
127.0.0.1:6379> 

6、键的数据结构类型type key

  • 键name 是字符串类型, 返回结果为string,
  • 键artisanlist是列表类型, 返回结果为list,
  • 键user是列表hash, 返回结果为hash
127.0.0.1:6379> set name artisan
OK
127.0.0.1:6379> rpush artisanlist asds sddfd sdfdsf dfdfsfd sdfsdfsd f
(integer) 6
127.0.0.1:6379> hset user name artisan2 age 20 country china sex male
(integer) 4
127.0.0.1:6379> type name 
string
127.0.0.1:6379> type artisanlist
list
127.0.0.1:6379> type user
hash
127.0.0.1:6379> 

7、数据结构和内部编码

type命令实际返回的就是当前键的数据结构类型: string(字符串) 、 hash(哈希) 、 list(列表) 、 set(集合) 、 zset(有序集合) , 其实这些只是Redis对外的数据结构。 事实上,Redis内部每种数据结构都有自己底层的内部编码实现, 而且是多种实现,Redis会在合适的场景选择合适的内部编码。
Redis 全局命令、数据类型、内部编码、spring操作使用、使用场景_第1张图片
Redis 全局命令、数据类型、内部编码、spring操作使用、使用场景_第2张图片
可以通过object encoding命令查询内部编码

127.0.0.1:6379> set name artisan
OK
127.0.0.1:6379> LPUSH artisanlist c b a 
(integer) 9
127.0.0.1:6379> hset user name artisan age 20 sex male
(integer) 0
127.0.0.1:6379> sadd myset a b c 
(integer) 3
127.0.0.1:6379> zadd myzset 90 artisan
(integer) 1
127.0.0.1:6379> object encoding name
"embstr"
127.0.0.1:6379> object encoding user
"ziplist"
127.0.0.1:6379> object encoding myset
"hashtable"
127.0.0.1:6379> object encoding myzset
"ziplist"
127.0.0.1:6379> 

单线程架构

Redis使用了单线程架构和I/O多路复用模型来实现高性能的内存数据库服务。

为什么Redis使用单线程模型会达到每秒万级别的处理能力呢?

  1. 纯内存访问, Redis将所有数据放在内存中, 内存的响应时长大约为100纳秒, 这是Redis达到每秒万级别访问的重要基础
  2. 非阻塞I/O, Redis使用epoll作为I/O多路复用技术的实现, 再加上Redis自身的事件处理模型将epoll中的连接、 读写、 关闭都转换为事件, 不在网络I/O上浪费过多的时间
  3. 单线程避免了线程切换和竞态产生的消耗。

二、Redis数据结构 - 字符串(string)

1、概述

字符串是 Redis 最基本的数据结构 ,它将以一个键和一个值存储于 Redis 内部,很像Java 的 Map 结构 ,让 Redis 通过键去找到值。

Redis 会通过 key 去找到对应的字符串 ,比如通过 keyl 找到 valuel。假设产品的编号为 0001 , 只要设置 key 为 product_0001 , 就可以通过 product_0001去保存该产品到 Redis 中,也可以通过 product_0001 从 redis 中找到产品信息。

字符串类型的值实际可以 是字符串(简单的字符串、 复杂的字符串(例如JSON、 XML) ) 、 数字 (整数、 浮点数) , 甚至是二进制(图片、 音频、 视频) , 但是值最大不能 超过512MB。

2、基本命令

Redis 全局命令、数据类型、内部编码、spring操作使用、使用场景_第3张图片


命令 说明 备注
set key value 设置键值对 最常用的写入命令
get key 通过键获取值 最常用的读取命
del key 通过 key ,删除键值对 删除命令,返刨删除数,注意,它是一个通用的命令,换句话说在其他数据纺构中,也可以使用它
strlen key 求 key 指向字符串的氏度 返回长度
getset key value 修改原来 key 的对应值,并将旧值返回 如果原来值为空,则返回为空,并设置新值
getrange key start end 获取子串 记字符串的长度为 len,把字符串看作一个数组,而Redis 是以 0 开始计数的,所以 s tart 和l end 的取值范为 0 到 len-1
append key value 将新的字符串value加入到原来 key指向的字符串末 返回 key 指向新字符净的长度

set key value [ex seconds] [px milliseconds] [nx|xx]
  • ex seconds: 为键设置秒级过期时间。

  • px milliseconds: 为键设置毫秒级过期时间。

  • nx: 键必须不存在, 才可以设置成功, 用于添加。

  • xx: 与nx相反, 键必须存在, 才可以设置成功, 用于更新

除了set选项, Redis还提供了setex和setnx两个命令

2.1、设置过期时间

setex key seconds value

setnx当前键不存在,允许设置

setnx key value
127.0.0.1:6379> set  key1 value1
OK
127.0.0.1:6379> setnx key1 value2  -- 因为存在,所以设置key1 失败,返回0
(integer) 0
127.0.0.1:6379> setex key1 100 vlaue3 -- 设置过期时间 100秒
OK
127.0.0.1:6379> get key1
"vlaue3"
127.0.0.1:6379> ttl key1
(integer) 93
127.0.0.1:6379> ttl key1
(integer) 88

setnx和setxx在实际使用中有什么应用场景吗? 以setnx命令为例子, 由于Redis的单线程命令处理机制, 如果有多个客户端同时执行setnx key value,根据setnx的特性只有一个客户端能设置成功, setnx可以作为分布式锁的一种实现方案。

2.2、批量设置值

mset key value [key value ...]
127.0.0.1:6379> mset key1 a key2 b key3 c key4 d key5 5 
OK

2.3、批量获取值

mget key [key ...]
127.0.0.1:6379> mget key1 key2 key3 key4 key5
1) "a"
2) "b"
3) "c"
4) "d"
5) "5"

如果有些键不存在, 那么它的值为nil(空) , 结果是按照传入键的顺序返回:

127.0.0.1:6379> mget key1 key2 xxx key3
1) "a"
2) "b"
3) (nil)
4) "c"
127.0.0.1:6379> 

批量操作命令可以有效提高效率,假如没有mget这样的命令, 要执行n次get命令如下所示

n次get时间 = n次网络时间 + n次命令时间
Redis 全局命令、数据类型、内部编码、spring操作使用、使用场景_第4张图片

使用mget命令后, 要执行n次get命令操作如下

n次get时间 = 1次网络时间 + n次命令时间
Redis 全局命令、数据类型、内部编码、spring操作使用、使用场景_第5张图片

Redis可以支撑每秒数万的读写操作, 但是这指的是Redis服务端的处理能力, 对于客户端来说, 一次命令除了命令时间还是有网络时间, 假设网络时间为1毫秒, 命令时间为0.1毫秒(按照每秒处理1万条命令算) , 那么执行1000次get命令和1次mget命令如下:

操作 时间
1000次get请求 10001(网络时间)+10000.1(命令执行时间)=1100毫秒=1.1秒
1次mget请求 11(网络时间)+10000.1(命令执行时间)=101毫秒=0.101秒

因为Redis的处理能力已经足够高,所以 网络可能会成为性能的瓶颈。

使用批量操作, 有助于提高业务处理效率, 但是要注意的是每次批量操作所发送的命令数不是无节制的, 如果数量过多可能造成Redis阻塞或者网络拥塞。

3、string的内部编码

字符串类型的3种内部编码

  1. int: 8个字节的长整型

  2. embstr: 小于等于39个字节的字符串

  3. raw: 大于39个字节的字符串

Redis会根据当前值的类型和长度决定使用哪种内部编码实现。

127.0.0.1:6379> set k1 1234
OK
127.0.0.1:6379> object encoding k1
"int"
127.0.0.1:6379> set k2 "my name is artisan"
OK
127.0.0.1:6379> object encoding k2
"embstr"
127.0.0.1:6379> STRLEN k2
(integer) 18
127.0.0.1:6379> set k3 "sdfdfsdfsdfdf sfds dfsdfs dsf  sfsdfs fd fsf fs sf sfds fdfs fsdf sfs fsdf sdfs f ssfd fsd fdf sfsdf f"
OK
127.0.0.1:6379> STRLEN k3
(integer) 102
127.0.0.1:6379> object encoding k3
"raw"
127.0.0.1:6379> 

4、string 客户端操作

127.0.0.1:6379> set key1 value1
OK
127.0.0.1:6379> set key2 value2
OK
127.0.0.1:6379> get key1
"value1"
127.0.0.1:6379> del key1
(integer) 1
127.0.0.1:6379> strlen key2
(integer) 6
127.0.0.1:6379> getset key2 new_value2
"value2"
127.0.0.1:6379> get key2
"new_value2"
127.0.0.1:6379> getrange key2 0 3 
"new_"
127.0.0.1:6379> append key2 _app
(integer) 14
127.0.0.1:6379> get key2
"new_value2_app"
127.0.0.1:6379>

5、Spring中操作redis的字符串

5.1、spring-redis-string.xml


<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
	xmlns:context="http://www.springframework.org/schema/context"
	xmlns:p="http://www.springframework.org/schema/p"
	xsi:schemaLocation="http://www.springframework.org/schema/beans 
		http://www.springframework.org/schema/beans/spring-beans.xsd
		http://www.springframework.org/schema/context 
		http://www.springframework.org/schema/context/spring-context.xsd">

    <context:property-placeholder location="classpath:redis/redis.properties" />

    
    
    <bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig">
        
        <property name="maxIdle" value="${redis.maxIdle}" />
        
        <property name="maxTotal" value="${redis.maxTotal}" />
        
        <property name="maxWaitMillis" value="${redis.maxWaitMillis}" />
        
        <property name="minEvictableIdleTimeMillis" value="${redis.minEvictableIdleTimeMillis}" />
        
        <property name="numTestsPerEvictionRun" value="${redis.numTestsPerEvictionRun}" />
        
        <property name="timeBetweenEvictionRunsMillis" value="${redis.timeBetweenEvictionRunsMillis}" />
   		<property name="testOnBorrow" value="true">property>
		<property name="testOnReturn" value="true">property>
		<property name="testWhileIdle" value="true">property>
    bean>
	
	
    <bean id="jedisConnectionFactory"
        class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory"
        destroy-method="destroy">
        <property name="poolConfig" ref="jedisPoolConfig">property>
        
        <property name="hostName" value="${redis.host.ip}">property>
        
        <property name="port" value="${redis.port}">property>
        
        <property name="password" value="${redis.password}" /> 
        
        <property name="timeout" value="${redis.timeout}">property>
        <property name="usePool" value="true" />
        
    bean>
	
	
	<bean id="stringRedisSerializer" class="org.springframework.data.redis.serializer.StringRedisSerializer"/>

	
	<bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate"
		p:connection-factory-ref="jedisConnectionFactory"
		p:keySerializer-ref="stringRedisSerializer"
		p:valueSerializer-ref="stringRedisSerializer">
	bean>

beans>

5.2、SpringRedisStringsDemo

package com.artisan.redis.baseStructure.strings;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.data.redis.core.RedisTemplate;

public class SpringRedisStringsDemo {

	public static void main(String[] args) {
		
		ApplicationContext ctx = new ClassPathXmlApplicationContext("classpath:spring/spring-redis-string.xml");
		
		// 在 Spring 中,
		// redisTemplate.opsForValue()所返回的对象可以操作简单的键值对,可以是字符串,也可以是对象,具体依据你所配置的序列化方案
		// 这里在spring-redis-string.xml中key和value都是指定的 stringRedisSerializer
		RedisTemplate<String,String> redisTemplate = (RedisTemplate<String, String>) ctx.getBean("redisTemplate");

		// 设置值
		//127.0.0.1:6379> set key1 value1
		//OK
		//127.0.0.1:6379> set key2 value2
		//OK
		redisTemplate.opsForValue().set("key1", "value1");
		redisTemplate.opsForValue().set("key2", "value2");
		
		// 通过 key 获取值
		//127.0.0.1:6379> get key1
		//"value1"
		String key1 = redisTemplate.opsForValue().get("key1");
		System.out.println("key1:" + key1);
		
		// 通过 key 删除值
		//127.0.0.1:6379> del key1
		//(integer) 1
		Boolean success = redisTemplate.delete("key1");
		System.out.println("删除key1是否成功:" + success);
		
		
		// 求长度
		//127.0.0.1:6379> strlen key2
		//(integer) 6
		Long size = redisTemplate.opsForValue().size("key2");
		System.out.println("key2长度:" + size);
		
		// 设值新值并返回旧值
		//127.0.0.1:6379> getset key2 new_value2
		//"value2"
		String oldValue = redisTemplate.opsForValue().getAndSet("key2", "new_value2");
		System.out.println("key2旧值:" + oldValue);

		
		// 127.0.0.1:6379> get key2
		// "new_value2"
		String newValue = redisTemplate.opsForValue().get("key2");
		System.out.println("key2新值:" + newValue);

		// 获取子串
		// 127.0.0.1:6379> getrange key2 0 3
		// "new_"
		String subString = redisTemplate.opsForValue().get("key2", 0, 3);
		System.out.println("subString:" + subString);

		// 将新的字符串value加入到原来 key指向的字符串末
		// 127.0.0.1:6379> append key2 _app
		// (integer) 14
		Integer value = redisTemplate.opsForValue().append("key2", "_app");
		System.out.println("value:" + value);

		// 127.0.0.1:6379> get key2
		// "new_value2_app"
		String newValue2 = redisTemplate.opsForValue().get("key2");
		System.out.println("key2:" + newValue2);
	}
}

6、Redis中对整数和浮点型数字的支持

上面介绍了字符串最常用的命令 , 但是 Redis 除了这些之外还提供了对整数和浮点型数字的功能,如果字符串是数字(整数或者浮点数〉,那么 Redis 还能支持简单的运算。但是它的运算能力比较弱 , 目前版本只能支持简单的加减法运算。


命令 说明 备注
incr key 在原字段上加 1 只能对整数操作
incrby key increment 在原字段上加上整数( increment ) 只能对整数操作
decr key 在原字段上减 1 只能对整数操作
decrby key decrement 在原字段上减去整数( decrement ) 只能对整数操作
incrbyfloat keyincrement 在原字段上加上浮点数( increment) 可以操作浮点数或者整

127.0.0.1:6379> flushdb
OK
127.0.0.1:6379> set number 8
OK
127.0.0.1:6379> GET number
"8"
127.0.0.1:6379> INCR number
(integer) 9
127.0.0.1:6379> INCRBY number 5
(integer) 14
127.0.0.1:6379> DECR number
(integer) 13
127.0.0.1:6379> DECRBY number 10
(integer) 3
127.0.0.1:6379> INCRBYFLOAT number 8.88
"11.88"
127.0.0.1:6379> get number
"11.88"
127.0.0.1:6379> 

如果开始把val 设置为浮点数,那么 incr、 deer、 incrby 、 deerby 的命令都会失败。 Redis 并不支持减法 、 乘法、除法操作,功能十分有限,需要注意。

Spring中操作redis的字符串的加减运算

package com.artisan.redis.baseStructure.strings;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.data.redis.core.RedisTemplate;

public class SpringRedisStringAdd_Subtraction {

	public static void main(String[] args) {

		ApplicationContext ctx = new ClassPathXmlApplicationContext("classpath:spring/spring-redis-string.xml");

		RedisTemplate redisTemplate = (RedisTemplate) ctx.getBean("redisTemplate");

		// 127.0.0.1:6379> set number 8
		// OK
		redisTemplate.opsForValue().set("number", String.valueOf(8));

		// 127.0.0.1:6379> GET number
		// "8"
		System.out.println(redisTemplate.opsForValue().get("number"));

		// 127.0.0.1:6379> INCR number
		// (integer) 9
		System.out.println(redisTemplate.opsForValue().increment("number", 1));

		// 127.0.0.1:6379> INCRBY number 5
		// (integer) 14
		System.out.println(redisTemplate.opsForValue().increment("number", 5));

		// 127.0.0.1:6379> DECR number
		// (integer) 13
		Long number = redisTemplate.getConnectionFactory().getConnection().decr(redisTemplate.getKeySerializer().serialize("number"));
		System.out.println(number);
		// 127.0.0.1:6379> DECRBY number 10
		// (integer) 3
		Long number2 = redisTemplate.getConnectionFactory().getConnection().decrBy(redisTemplate.getKeySerializer().serialize("number"), 10);
		System.out.println(number2);

		// 127.0.0.1:6379> INCRBYFLOAT number 8.88
		// "11.88"
		System.out.println(redisTemplate.opsForValue().increment("number", 8.88));
		// 127.0.0.1:6379> get number
		// "11.88"
		System.out.println(redisTemplate.opsForValue().get("number"));

	}

}

在配置文件中使用的是字符串序列化器,所以 Redis 保存的还是字符串 ,如果采用其他的序列化器,比如 JDK 序列化器,那么 Redis 保存的将不会是数字而是产生异常。

Spring 己经优化了代码,所increment 方法可 以支持long和double的加法,如下:

对于减法, RedisTemplate 并没有进行支持。 所以用下面的代码去代替它 :

redisTemplate . getConnectionFactory() .getConnection() . decrBy(redisTemplate.getKeySerializer() . serialize ("number") , 10) ;

通过获得连接工厂再获得连接从而得到底层的 Redis 连接对象。为了和 RedisTemplate的配置保持一致 ,所以先获取了其 keySerializer 属性 ,对键进行了序列化,如果获取结果也可以进行同样的转换。

当然 getConnection()只是获取一个 spring data redis 项目中封装的底层对象 RedisConnection , 甚至可以获取原始的链接对象Jedis 对象。

Jedis jedis =(Jedis)redisTemplate.getConnectionFactory() .getConnection() .getNativeConnection() ;

关于减法的方法,原有值都必须是整数,否则就会引发异常.

// 如果进行减法运算原有值都必须是整数,否则就会引发异常,比如下面的,编译通过,但运行会抛出异常
redisTemplate.opsForValue().set("number", "1.1");
redisTemplate.getConnectionFactory().getConnection().decr(redisTemplate.getKeySerializer().serialize("number"));
Exception in thread "main" org.springframework.dao.InvalidDataAccessApiUsageException: ERR value is not an integer or out of range; nested exception is redis.clients.jedis.exceptions.JedisDataException: ERR value is not an integer or out of range
	at org.springframework.data.redis.connection.jedis.JedisExceptionConverter.convert(JedisExceptionConverter.java:64)
	at org.springframework.data.redis.connection.jedis.JedisExceptionConverter.convert(JedisExceptionConverter.java:41)
	at org.springframework.data.redis.PassThroughExceptionTranslationStrategy.translate(PassThroughExceptionTranslationStrategy.java:44)
	at org.springframework.data.redis.FallbackExceptionTranslationStrategy.translate(FallbackExceptionTranslationStrategy.java:42)
	at org.springframework.data.redis.connection.jedis.JedisConnection.convertJedisAccessException(JedisConnection.java:181)
	at org.springframework.data.redis.connection.jedis.JedisStringCommands.convertJedisAccessException(JedisStringCommands.java:757)
	at org.springframework.data.redis.connection.jedis.JedisStringCommands.decr(JedisStringCommands.java:469)
	at org.springframework.data.redis.connection.DefaultedRedisConnection.decr(DefaultedRedisConnection.java:301)
	at com.artisan.redis.baseStructure.strings.SpringRedisStringAdd_Subtraction.main(SpringRedisStringAdd_Subtraction.java:49)
Caused by: redis.clients.jedis.exceptions.JedisDataException: ERR value is not an integer or out of range
	at redis.clients.jedis.Protocol.processError(Protocol.java:127)
	at redis.clients.jedis.Protocol.process(Protocol.java:161)
	at redis.clients.jedis.Protocol.read(Protocol.java:215)
	at redis.clients.jedis.Connection.readProtocolWithCheckingBroken(Connection.java:340)
	at redis.clients.jedis.Connection.getIntegerReply(Connection.java:265)
	at redis.clients.jedis.BinaryJedis.decr(BinaryJedis.java:708)
	at org.springframework.data.redis.connection.jedis.JedisStringCommands.decr(JedisStringCommands.java:467)
	... 2 more

注意

使用 Spring 提供的 RedisTemplate 去展示多个命令可以学习到如何使用 RedisTemplate 操作 Redis 。 实际工作中并不是那么用的,因为每一 个操作会尝试从连接池里获取 一 个新的 Redis 连接,多个命令应该使用SessionCallback 接口进行操作 。

常见使用场景

缓存

Redis作为缓存层, 绝大部分请求的数据都是从Redis中获取。由于Redis具有支撑高并发的特性, 所以缓存通常能起到加速读写和降低后端压力的作用。

计数器

使用Redis作为计数的基础工具, 它可以实现快速计数、查询缓存的功能, 同时数据可以异步落地到其他数据源。

限流

举个例子,对某个接口在1分钟内限制调用10次

session共享

使用Redis将用户的Session进行集中管理,在这种模式下只要保证Redis是高可用和扩展性的, 每次用户更新或者查询登录信息都直接从Redis中集中获取

三、哈希(hash)

1、概述

Redis 中哈希结构就如同 Java 的 map 一样 , 一个对象里面有许多键值对,它是特别适合存储对象的.

如果内存足够大 ,那么一个 Redis 的 hash 结构可以存储2的32次方-1个键值对 ( 40多亿)。

在 Redis 中, hash 是一个 String 类型的 field 和 value 的映射表,因此我们存储的数据实际在 Redis 内存中都是一个个字符串而己。

假设artisan有 3 个字段 : 编号( id)、名称 (name )、性别( sex),这样就可以使用一个 hash 结构保存它。

artisan
field value
id 123
name zhang
sex family

在 Redis 中它就是一个这样的结构,其中 artisan代表的是这个 hash 结构在 Redis 内存的 key,通过它就可以找到这个 hash 结构,而 hash 结构由一系列的 field 和 value 组成

2、客户端操作hash

127.0.0.1:6379> HMSET artisan  id 123 name littleArtisan sex female
OK
127.0.0.1:6379> HGETALL artisan
1) "id"
2) "123"
3) "name"
4) "littleArtisan"
5) "sex"
6) "female"
127.0.0.1:6379> 

3、Redis hash 结构命令

官网:https://redis.io/commands#hash
Redis 全局命令、数据类型、内部编码、spring操作使用、使用场景_第6张图片

命令 说明 备注
hdel key field 1 [ field2 …] 删除 hash 结构中的某个(些)字段 可以进行多个字段的删除
hexists key field 判断 hash 结构中是否存在 field 字段 存在返回 1 ,否则返回0
hgetall key 获取所有 hash 结构中的键值 返回键和值
hincrby key field increment 指定给 hash 结构中的某一字段加上一个整数 要求该字段也是整数字符串
hincrbyfloat key field increment 指定给 hash 结构中的某一字段加上一个浮点数 要求该字段是数字型字符串
hkeys key 返回 hash 中所有的键
hlen key 返问 hash 中键值对的数量
hmget key field1 [field2…] 返回 hash 中指定的键的值,可以是多个 依次返回值
hmset key field1 value1 [field2 value2…] hash 结构设置多个键值对
hset key filed value 在 hash 结构中设置键值对 单个设值
hsetnx key field value 当 hash 结构中不存在对应的键,才设置值
hvals key 获取 hash 结构中所有的值

在 Redis 中的哈希结构和字符串有着比较明显的不同。

  • 首先,命令都是以 h 开头,代表操作的是 hash 结构

  • 其次,大多数命令多了一个层级 field,这是hash 结构的一个内部键,也就是说Redis 需要通过 key 索引到对应的 hash
    结构,再通过 field来确定使用 hash 结构的哪个键值对

注意事项:

  • 哈希结构的大小,如果哈希结构是个很大的键值对,那么使用它要十分注意。 尤其是关于 hkeys 、 hgetall 、 hvals 等返回所有哈希结构数据的命令,会造成大量数据的读取。这需要考虑性能和读取数据大小对 JVM 内存的影响 。
  • 对于数字的操作命令 hincrby 而言,要求存储的也是整数型的字符串
  • 对于hincrbyfloat 而言,则要求使用浮点数或者整数,否则命令会失败。
127.0.0.1:6379> flushdb
OK
127.0.0.1:6379> 
127.0.0.1:6379> 
127.0.0.1:6379> HMSET obj k1 value1 k2 value2 k3 value3 
OK
127.0.0.1:6379> HSET obj k4  6
(integer) 1
127.0.0.1:6379> HEXISTS obj k2
(integer) 1
127.0.0.1:6379> HGETALL obj
1) "k1"
2) "value1"
3) "k2"
4) "value2"
5) "k3"
6) "value3"
7) "k4"
8) "6"
127.0.0.1:6379> HINCRBY obj k4 8
(integer) 14
127.0.0.1:6379> HINCRBYFLOAT obj k4 6.2
"20.2"
127.0.0.1:6379> HKEYS obj
1) "k1"
2) "k2"
3) "k3"
4) "k4"
127.0.0.1:6379> HMGET obj k1 k2 k4
1) "value1"
2) "value2"
3) "20.2"
127.0.0.1:6379> HLEN obj
(integer) 4
127.0.0.1:6379> HSETNX obj k2 test
(integer) 0
127.0.0.1:6379> HSETNX obj k5 test
(integer) 1
127.0.0.1:6379> HGETALL obj
 1) "k1"
 2) "value1"
 3) "k2"
 4) "value2"
 5) "k3"
 6) "value3"
 7) "k4"
 8) "20.2"
 9) "k5"
10) "test"
127.0.0.1:6379> HVALS obj
1) "value1"
2) "value2"
3) "value3"
4) "20.2"
5) "test"
127.0.0.1:6379> HDEL obj k5
(integer) 1
127.0.0.1:6379> HGETALL obj
1) "k1"
2) "value1"
3) "k2"
4) "value2"
5) "k3"
6) "value3"
7) "k4"
8) "20.2"
127.0.0.1:6379> HGET obj k4
"20.2"
127.0.0.1:6379> 

4、Spring操作reids的hash

4.1、 修改defaultSerializer


<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
	xmlns:context="http://www.springframework.org/schema/context"
	xmlns:p="http://www.springframework.org/schema/p"
	xsi:schemaLocation="http://www.springframework.org/schema/beans 
		http://www.springframework.org/schema/beans/spring-beans.xsd
		http://www.springframework.org/schema/context 
		http://www.springframework.org/schema/context/spring-context.xsd">

    <context:property-placeholder location="classpath:redis/redis.properties" />

    
    
    <bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig">
        
        <property name="maxIdle" value="${redis.maxIdle}" />
        
        <property name="maxTotal" value="${redis.maxTotal}" />
        
        <property name="maxWaitMillis" value="${redis.maxWaitMillis}" />
        
        <property name="minEvictableIdleTimeMillis" value="${redis.minEvictableIdleTimeMillis}" />
        
        <property name="numTestsPerEvictionRun" value="${redis.numTestsPerEvictionRun}" />
        
        <property name="timeBetweenEvictionRunsMillis" value="${redis.timeBetweenEvictionRunsMillis}" />
   		<property name="testOnBorrow" value="true">property>
		<property name="testOnReturn" value="true">property>
		<property name="testWhileIdle" value="true">property>
    bean>
	
	
    <bean id="jedisConnectionFactory"
        class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory"
        destroy-method="destroy">
        <property name="poolConfig" ref="jedisPoolConfig">property>
        
        <property name="hostName" value="${redis.host.ip}">property>
        
        <property name="port" value="${redis.port}">property>
        
        <property name="password" value="${redis.password}" /> 
        
        <property name="timeout" value="${redis.timeout}">property>
        <property name="usePool" value="true" />
        
    bean>
	
	
	<bean id="stringRedisSerializer" class="org.springframework.data.redis.serializer.StringRedisSerializer"/>

	
	<bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate"
		p:connection-factory-ref="jedisConnectionFactory"
		p:keySerializer-ref="stringRedisSerializer"
		p:defaultSerializer-ref="stringRedisSerializer"
		p:valueSerializer-ref="stringRedisSerializer">
	bean>

beans>

在 Redis 中, hash 是一个 String 类型的 field 和 value 的映射表。 Spring 对 Redis 进行了封装,所以有必要对 RedisTemplate 的配置项进行修改。修改defaultSerializer-ref
Redis 全局命令、数据类型、内部编码、spring操作使用、使用场景_第7张图片

不指定的话,否则抛出如下异常

Cannot deserialize; nested exception is org.springframework.core.serializer.support.SerializationFailedException: Failed to deserialize payload. Is the byte array a result of corresponding serialization for DefaultDeserializer?; nested exception is java.io.EOFException

4.2、 操作hash

package com.artisan.redis.baseStructure.hash;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.data.redis.core.RedisTemplate;

public class SpringRedisHashDemo {

	public static void main(String[] args) {
		
		ApplicationContext ctx = new ClassPathXmlApplicationContext("classpath:spring/spring-redis-hash.xml");
		
		
		RedisTemplate redisTemplate = (RedisTemplate) ctx.getBean("redisTemplate");
		
		// 127.0.0.1:6379> HMSET obj k1 value1 k2 value2 k3 value3
		// OK
		String key = "obj";
		Map<String, String> map = new HashMap<String, String>();
		map.put("k1", "value1");
		map.put("k2", "value2");
		map.put("k3", "value3");
		redisTemplate.opsForHash().putAll(key, map);

		// 127.0.0.1:6379> HSET obj k4 6
		// (integer) 1
		redisTemplate.opsForHash().put(key, "k4", String.valueOf(6));

		// 127.0.0.1:6379> HEXISTS obj k2
		// (integer) 1
		boolean exist = redisTemplate.opsForHash().hasKey(key, "k2");
		System.out.println(key + " 这个键中是否存在 k2这个field:" + exist);

		// 127.0.0.1:6379> HGETALL obj
		// 1) "k1"
		// 2) "value1"
		// 3) "k2"
		// 4) "value2"
		// 5) "k3"
		// 6) "value3"
		// 7) "k4"
		// 8) "6"
		Map<String,String> map2 = redisTemplate.opsForHash().entries(key);
		if (map2 != null) {
			scanMap(map2);
		}

		// 127.0.0.1:6379> HINCRBY obj k4 8
		// (integer) 14
		System.out.println(redisTemplate.opsForHash().increment(key, "k4", 8));
		
		
		// 127.0.0.1:6379> HINCRBYFLOAT obj k4 6.2
		// "20.2"
		System.out.println(redisTemplate.opsForHash().increment(key, "k4", 6.2));

		// 127.0.0.1:6379> HKEYS obj
		// 1) "k1"
		// 2) "k2"
		// 3) "k3"
		// 4) "k4"
		Set<String> set = redisTemplate.opsForHash().keys(key);
		for (String str : set) {
			System.out.println(str);
		}
		// 127.0.0.1:6379> HMGET obj k1 k2 k4
		// 1) "value1"
		// 2) "value2"
		// 3) "20.2"
		List<String> list = new ArrayList<String>();
		list.add("k1");
		list.add("k2");
		list.add("k4");
		List<String> list2 = redisTemplate.opsForHash().multiGet(key, list);
		scanList(list2);
		
		
		// 127.0.0.1:6379> HLEN obj
		// (integer) 4
		System.out.println(redisTemplate.opsForHash().size(key));
		
		// 127.0.0.1:6379> HSETNX obj k2 test
		// (integer) 0
		System.out.println(redisTemplate.opsForHash().putIfAbsent(key, "k2", "test"));
		
		// 127.0.0.1:6379> HSETNX obj k5 test
		// (integer) 1
		System.out.println(redisTemplate.opsForHash().putIfAbsent(key, "k5", "test"));

		// 127.0.0.1:6379> HGETALL obj
		// 1) "k1"
		// 2) "value1"
		// 3) "k2"
		// 4) "value2"
		// 5) "k3"
		// 6) "value3"
		// 7) "k4"
		// 8) "20.2"
		// 9) "k5"
		// 10) "test"

		Map<String, String> map3 = redisTemplate.opsForHash().entries(key);
		if (map3 != null) {
			scanMap(map3);
		}

		// 127.0.0.1:6379> HVALS obj
		// 1) "value1"
		// 2) "value2"
		// 3) "value3"
		// 4) "20.2"
		// 5) "test"

		List<String> list3 = redisTemplate.opsForHash().values(key);
		scanList(list3);

		// 127.0.0.1:6379> HDEL obj k5
		// (integer) 1
		redisTemplate.opsForHash().delete(key, "k5");

		// 127.0.0.1:6379> HGETALL obj
		// 1) "k1"
		// 2) "value1"
		// 3) "k2"
		// 4) "value2"
		// 5) "k3"
		// 6) "value3"
		// 7) "k4"
		// 8) "20.2"

		Map<String, String> map4 = redisTemplate.opsForHash().entries(key);
		if (map4 != null) {
			scanMap(map4);
		}

		// 127.0.0.1:6379> HGET obj k4
		// "20.2"
		System.out.println(redisTemplate.opsForHash().get(key, "k4"));
	}


	private static void scanList(List<String> list2) {
		for (String string : list2) {
			System.out.println(string);
		}
	}

	private static void scanMap(Map<String, String> map4) {
		for (Map.Entry<String, String> entry : map4.entrySet()) {
			System.out.println("Key = " + entry.getKey() + ", Value = " + entry.getValue());
		}
	}
}

输出:

INFO : org.springframework.context.support.ClassPathXmlApplicationContext - Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@73a8dfcc: startup date [Thu Sep 20 19:13:10 CST 2018]; root of context hierarchy
INFO : org.springframework.beans.factory.xml.XmlBeanDefinitionReader - Loading XML bean definitions from class path resource [spring/spring-redis-hash.xml]
obj 这个键中是否存在 k2这个field:true
Key = k2, Value = value2
Key = k3, Value = value3
Key = k4, Value = 6
Key = k1, Value = value1
14
20.2
k1
k2
k3
k4
value1
value2
20.2
4
false
true
Key = k2, Value = value2
Key = k5, Value = test
Key = k3, Value = value3
Key = k1, Value = value1
Key = k4, Value = 20.2
value1
value2
value3
20.2
test
Key = k3, Value = value3
Key = k2, Value = value2
Key = k4, Value = 20.2
Key = k1, Value = value1
20.2
  • hmset 命令,在 Java 的 API 中,是使用 map 保存多个键值对。
  • hgetall 命令会返回所有的键值对,并保存到一个 map 对象中,如果 hash 结构很大,那么要考虑它对 JVM 的内存影响。
  • hincrby 和 hincrbyFloat 命令都采用 increment 方法, Spring 会识别它具体使用何种方法。
  • redisTemplate.opsForHash().values(key)方法相当于 hvals 命令,它会返回所有的值,并保存到一个 List 对象中;
  • redisTemplate.opsForHash().keys(key)方法相当于 hkeys命令,它会获取所有的键,保存到一个 Set 对象中 。
  • 在 Spring 中使用 redisTemplate.opsForHash().putAll(key, map )方法相当于执行了hmset 命令,使用了 map ,由于配置了默认的序列化器为字符串,所以它也只会用字符串进行转化,这样才能执行对应的数值加法,如果使用其他序列化器,则后面的命令可能会抛出异常。
  • 在使用大的 hash 结构时,需要考虑返回数据的大小,以避免返回太多的数据,引发JVM内存溢出或者 Redis 的性能问题。

注意

使用 Spring 提供的 RedisTemplate 去展示多个命令可以学习到如何使用 RedisTemplate 操作 Redis 。 实际工作中并不是那么用的,因为每一 个操作会尝试从连接池里获取 一 个新的 Redis 连接,多个命令应该使用SessionCallback 接口进行操作 。

四、链表(linked-list)

1、概述

  1. 链表结构是 Redis 中一个常用的结构,它可以存储多个字符串
  2. 它是有序的
  3. 能够存储2的32次方减一个节点(超过 40 亿个节点)
  4. Redis 链表是双向的,因此即可以从左到右,也可以从右到左遍历它存储的节点
  5. 链表结构查找性能不佳,但 插入和删除速度很快

由于是双向链表,所以只能够从左到右,或者从右到左地访问和操作链表里面的数据节点。 但是使用链表结构就意味着读性能的丧失,所以要在大量数据中找到一个节点的操作性能是不佳的,因为链表只能从一个方向中去遍历所要节点,比如从查找节点 10000 开始查询,它需要按照节点1 、节点 2、节点 3……直至节点 10000,这样的顺序查找,然后把一个个节点和你给出的值比对,才能确定节点所在。如果这个链表很大,如有上百万个节点,可能需要遍历几十万次才能找到所需要的节点,显然查找性能是不佳的。

链表结构的优势在于插入和删除的便利 ,因为链表的数据节点是分配在不同的内存区域的,并不连续,只是根据上一个节点保存下一个节点的顺序来索引而己,无需移动元素。

因为是双向链表结构,所以 Redis 链表命令分为左操作和右操作两种命令,左操作就意味着是从左到右,右操作就意味着是从右到左。

2、Redis 关于链表的命令

Redis 全局命令、数据类型、内部编码、spring操作使用、使用场景_第8张图片

命令 说明 备注
lpush key node1 [node2.]… 把节点 node1 加入到链表最左边 如果是 node 1 、 node2…noden 这样加入,那么链表开头从左到右顺序是 noden…node2 、 node1
rpush key node1 [node2]… 把节点 node1 加入到链表最右边 如果是 node 1 、 node2…noden 这样加入,那么链表开头从左到右顺序是node 1 、 node2…noden
lindex key index 读取下标为 index 的节点 返回节点字符串,从 0 开始算
llen key 求链表的长度 返回链表节点数
lpop key 删除左边第一个节点,并将其返回
rpop key 删除右边第一个节点,并将其返回
linsert key before/after pivot node 插入一个节点 node,并且可以指定在值为 pivot的节点的前面( before )或者后面 ( after ) 如果 list 不存在,则报销:如果没有值为对应 pivot 的,也会插入失败返回-1
lpushx list node 如果存在 key 为 list 的链表,则插入节点 node, 并且作为从左到右的第一个节点 如果 list 不存在,则失败
rpushx list node 如果存在 key 为 list 的链表,则插入节点 node, 并且作为从左到右的最后一个节点 如果 list 不存在,则失败
lrange list start end 获取链表 list 从 start 下标到 end 下标的节点值 包含 start 和 end 下标的值
lrem list count value 如果 count 为 0,则删除所有值等于 value 的节点:如果 count 不是 0,则先对 count 取绝对值,假设记为 abs,然后从左到右删除不大于 abs 个等于 value 的节点 注意, count 为整数,如l果是负数 , 则Redis会先求取其绝对值 , 然后传递到后台操作
lset key index node 设置列表下标为 index 的节点的值为 node
ltrim key start stop 修剪链表,只保留从 start 到 stop 的区间的节点,其余的都删除掉 包含 start 和 end 的下标的节点会保留

对于很多个节点同时操作的,需要考虑其花费的时间,链表数据结构对于查找而言并不适合于大数据。我们需要考虑插入和删除内容的大小,因为这将是十分消耗性能的命令,会导致 Redis 服务器的卡顿 。对于不允许卡顿的一些服务器,可以进行分批次操作,以避免出现卡顿。

127.0.0.1:6379> flushdb
OK
127.0.0.1:6379> LPUSH list node3 node2 node1
(integer) 3
127.0.0.1:6379> RPUSH list node4
(integer) 4
127.0.0.1:6379> LINDEX list 0
"node1"
127.0.0.1:6379> LLEN list
(integer) 4
127.0.0.1:6379> LPOP list
"node1"
127.0.0.1:6379> RPOP list
"node4"
127.0.0.1:6379> LINSERT list before node2 before_node
(integer) 3
127.0.0.1:6379> LINSERT list after node2 after_node
(integer) 4
127.0.0.1:6379> LPUSHX list head
(integer) 5
127.0.0.1:6379> RPUSHX list end
(integer) 6
127.0.0.1:6379> LRANGE list 0 10
1) "head"
2) "before_node"
3) "node2"
4) "after_node"
5) "node3"
6) "end"
127.0.0.1:6379> LPUSH list node node node 
(integer) 9
127.0.0.1:6379> LREM list 3 node 
(integer) 3
127.0.0.1:6379> LSET list 0 new_head_value
OK
127.0.0.1:6379> LRANGE list 0 10
1) "new_head_value"
2) "before_node"
3) "node2"
4) "after_node"
5) "node3"
6) "end"
127.0.0.1:6379> LTRIM list 0 2 
OK
127.0.0.1:6379> LRANGE list 0 10
1) "new_head_value"
2) "before_node"
3) "node2"
127.0.0.1:6379> 

3、使用 Spring 操作 Redis 链表命令

配置文件


<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
	xmlns:context="http://www.springframework.org/schema/context"
	xmlns:p="http://www.springframework.org/schema/p"
	xsi:schemaLocation="http://www.springframework.org/schema/beans 
		http://www.springframework.org/schema/beans/spring-beans.xsd
		http://www.springframework.org/schema/context 
		http://www.springframework.org/schema/context/spring-context.xsd">

    <context:property-placeholder location="classpath:redis/redis.properties" />

    
    
    <bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig">
        
        <property name="maxIdle" value="${redis.maxIdle}" />
        
        <property name="maxTotal" value="${redis.maxTotal}" />
        
        <property name="maxWaitMillis" value="${redis.maxWaitMillis}" />
        
        <property name="minEvictableIdleTimeMillis" value="${redis.minEvictableIdleTimeMillis}" />
        
        <property name="numTestsPerEvictionRun" value="${redis.numTestsPerEvictionRun}" />
        
        <property name="timeBetweenEvictionRunsMillis" value="${redis.timeBetweenEvictionRunsMillis}" />
   		<property name="testOnBorrow" value="true">property>
		<property name="testOnReturn" value="true">property>
		<property name="testWhileIdle" value="true">property>
    bean>
	
	
    <bean id="jedisConnectionFactory"
        class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory"
        destroy-method="destroy">
        <property name="poolConfig" ref="jedisPoolConfig">property>
        
        <property name="hostName" value="${redis.host.ip}">property>
        
        <property name="port" value="${redis.port}">property>
        
        <property name="password" value="${redis.password}" /> 
        
        <property name="timeout" value="${redis.timeout}">property>
        <property name="usePool" value="true" />
        
    bean>
	
	
	<bean id="stringRedisSerializer" class="org.springframework.data.redis.serializer.StringRedisSerializer"/>

	
	<bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate"
		p:connection-factory-ref="jedisConnectionFactory"
		p:keySerializer-ref="stringRedisSerializer"
		p:defaultSerializer-ref="stringRedisSerializer"
		p:valueSerializer-ref="stringRedisSerializer">
	bean>

beans>
package com.artisan.redis.baseStructure.list;

import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.List;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.data.redis.connection.RedisListCommands;
import org.springframework.data.redis.core.RedisTemplate;

public class SpringRedisListDemo {
	
	private static String key = "list";
	
	public static void main(String[] args) {
		
		ApplicationContext ctx = new ClassPathXmlApplicationContext("classpath:spring/spring-redis-list.xml");
		RedisTemplate redisTemplate = (RedisTemplate) ctx.getBean("redisTemplate");
		
		// 不管存不存在,先根据key清理掉链表,方便测试
		Boolean flag  = redisTemplate.delete(key);
		System.out.println((flag = true) ? "删除list成功" : "删除list失败");
		
		// 127.0.0.1:6379> LPUSH list node3 node2 node1
		// (integer) 3

		// 把 node3 插入链表 list
		System.out.println(redisTemplate.opsForList().leftPush(key, "node3"));

		// 相当于 lpush 把多个价值从左插入链表
		List<String> nodeList = new ArrayList<String>();
		for (int i = 2; i >= 1; i--) {
			nodeList.add("node" + i);
		}
		System.out.println(redisTemplate.opsForList().leftPushAll(key, nodeList));

		// 127.0.0.1:6379> RPUSH list node4
		// (integer) 4
		// 从右边插入一个节点
		System.out.println(redisTemplate.opsForList().rightPush(key, "node4"));

		// 127.0.0.1:6379> LINDEX list 0
		// "node1"
		// 获取下标为 0 的节点
		String node = (String) redisTemplate.opsForList().index(key, 0);
		System.out.println("第一个节点:" + node);

		// 127.0.0.1:6379> LLEN list
		// (integer) 4
		// 获取链表长度
		System.out.println(key + "中的总数为:" + redisTemplate.opsForList().size(key));

		// 127.0.0.1:6379> LPOP list
		// "node1"
		// 从左边弹出 一个节点
		String leftPopNode = (String) redisTemplate.opsForList().leftPop(key);
		System.out.println("leftPopNode:" + leftPopNode);

		// 127.0.0.1:6379> RPOP list
		// "node4"
		// 从右边弹出 一个节点
		String rightPopNode = (String) redisTemplate.opsForList().rightPop(key);
		System.out.println("rightPopNode:" + rightPopNode);

		// 注意,需要使用更为底层的命令才能操作 linsert 命令
		// 127.0.0.1:6379> LINSERT list before node2 before_node
		// (integer) 3
		// 使用 linsert 命令在node2 前插入一个节点
		try {
			Long long1 = redisTemplate.getConnectionFactory().getConnection()
					.lInsert(key.getBytes("utf-8"), RedisListCommands.Position.BEFORE, "node2".getBytes("utf-8"), "before_node".getBytes("utf-8"));
			System.out.println(long1);
		} catch (UnsupportedEncodingException e) {
			e.printStackTrace();
		}


		// 127.0.0.1:6379> LINSERT list after node2 after_node
		// (integer) 4
		// 使用 linsert 命令在 node2 后插入一个节点
		try {
			Long long2 = redisTemplate.getConnectionFactory().getConnection()
					.lInsert(key.getBytes("utf-8"), RedisListCommands.Position.AFTER, "node2".getBytes("utf-8"), "after_node".getBytes("utf-8"));
			System.out.println(long2);
		} catch (UnsupportedEncodingException e) {
			e.printStackTrace();
		}

		// 127.0.0.1:6379> LPUSHX list head
		// (integer) 5
		// 判断 list 是否存在,如果存在则从左边插入 head 节点
		System.out.println(redisTemplate.opsForList().leftPushIfPresent(key, "head"));

		// 127.0.0.1:6379> RPUSHX list end
		// (integer) 6
		// 判断 list 是否存在,如果存在则从右边插入 end 节点
		System.out.println(redisTemplate.opsForList().rightPushIfPresent(key, "end"));

		// 127.0.0.1:6379> LRANGE list 0 10
		// 1) "head"
		// 2) "before_node"
		// 3) "node2"
		// 4) "after_node"
		// 5) "node3"
		// 6) "end"
		List<String> list = redisTemplate.opsForList().range(key, 0, 10);
		for (String string : list) {
			System.out.println("节点:" + string);
		}

		// 127.0.0.1:6379> LPUSH list node node node
		// (integer) 9
		// 在链表左边插入三个值为 node 的节点
		nodeList.clear();
		for (int i = 0; i < 3; i++) {
			nodeList.add("node");
		}
		System.out.println(redisTemplate.opsForList().leftPushAll(key, nodeList));

		// 127.0.0.1:6379> LREM list 3 node
		// (integer) 3
		// 从左到右删除至多三个 node 节点
		System.out.println(redisTemplate.opsForList().remove(key, 3, "node"));

		// 127.0.0.1:6379> LSET list 0 new_head_value
		// OK
		// 给链表下标为 0 的节点设置新值
		redisTemplate.opsForList().set(key, 0, "new_head_value");

		// 127.0.0.1:6379> LRANGE list 0 10
		// 1) "new_head_value"
		// 2) "before_node"
		// 3) "node2"
		// 4) "after_node"
		// 5) "node3"
		// 6) "end"
		list = redisTemplate.opsForList().range(key, 0, 10);
		for (String string : list) {
			System.out.println("节点:" + string);
		}

		// 127.0.0.1:6379> LTRIM list 0 2
		// OK
		redisTemplate.opsForList().trim(key, 0, 2);
		System.out.println("---------------------");
		// 127.0.0.1:6379> LRANGE list 0 10
		// 1) "new_head_value"
		// 2) "before_node"
		// 3) "node2"
		list = redisTemplate.opsForList().range(key, 0, 10);
		for (String string : list) {
			System.out.println("节点:" + string);
		}
	}
}

输出结果:

INFO : org.springframework.context.support.ClassPathXmlApplicationContext - Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@73a8dfcc: startup date [Wed Sep 26 10:31:21 CST 2018]; root of context hierarchy
INFO : org.springframework.beans.factory.xml.XmlBeanDefinitionReader - Loading XML bean definitions from class path resource [spring/spring-redis-list.xml]
删除list成功
1
3
4
第一个节点:node1
list中的总数为:4
leftPopNode:node1
rightPopNode:node4
3
4
5
6
节点:head
节点:before_node
节点:node2
节点:after_node
节点:node3
节点:end
9
3
节点:new_head_value
节点:before_node
节点:node2
节点:after_node
节点:node3
节点:end
---------------------
节点:new_head_value
节点:before_node
节点:node2

有些命令 Spring 所提供的 RedisTemplate 并不能支持,比如 linsert 命令。可以使用更为底层的方法去操作 ,如下

redisTemplate.getConnectionFactory().getConnection()
					.lInsert(key.getBytes("utf-8"), RedisListCommands.Position.AFTER, "node2".getBytes("utf-8"), "after_node".getBytes("utf-8"));

在多值操作的时候,往往会使用 list 进行封装 , 比如 leftPushAll 方法,对于很大的 list的操作需要注意性能 , 比如 remove 这样的操作,在大的链表中会消耗 Redis 系统很多的性能。

4、链表的阻塞命令

上面的这些操作链表的命令都是进程不安全的,因为 当我们操作这些命令的时候,其他 Redis 的客户端也可能操作同一个链表,这样就会造成并发数据安全和一致性的问题,尤其是当你操作一个数据量不小的链表结构时,常常会遇到这样的问题 。 Redis 提供了链表的阻塞命令,它们在运行的时候 , 会给链表加锁,以保证操作链表的命令安全性.

命令 说明 备注
blpop key timeout [node2.]… 移出并获取列表的第一个元索,如果列表没有元素会阻塞列表直到等待超 时或发现可弹出元素为止 相对于 lpop 命令 , 它的操作是进程安全的
brpop key timeout [node2.]… 移出并获取列表的最后一个元索,如果列表没有元素会阻塞列表直到等待超 时或发现可弹出元素为止 相对于 lpop 命令 , 它的操作是进程安全的
rpoplpush key src dest 按从左到右的顺序,将一个链表的最后一个元素移除,并插入到目标链表的最左边 不能设置超时间
brpoplpush key src dest timeout 按从左到右的顺序,将一个链表的最后一个元素移除,并插入到目标链表的最左边,并可以设置届时时间 可以设置超时时间

当使用这些命令时, Redis 就会对对应的链表加锁,加锁的结果就是其他的进程不能再读取或者写入该链表,只能等待命令结束 。 加锁的好处可以保证在多线程并发环境中数据的一致性,保证一些重要数据的一致性,比如账户的金额 、 商品的数量。不过在保证这些的同时也要付出其他线程等待、线程环境切换等代价,这将使得系统的并发能力下。

127.0.0.1:6379> flushdb
OK
127.0.0.1:6379> LPUSH list node1 node2 node3 node4 node5
(integer) 5
127.0.0.1:6379> LRANGE list 0 4
1) "node5"
2) "node4"
3) "node3"
4) "node2"
5) "node1"
127.0.0.1:6379> BLPOP list 2
1) "list"
2) "node5"
127.0.0.1:6379> LRANGE list 0 4
1) "node4"
2) "node3"
3) "node2"
4) "node1"
127.0.0.1:6379> BRPOP list 3
1) "list"
2) "node1"
127.0.0.1:6379> LPUSH list2 data1 data2 data3
(integer) 3
127.0.0.1:6379> RPOPLPUSH list list2
"node2"
127.0.0.1:6379> BRPOPLPUSH list list2 3
"node3"
127.0.0.1:6379> LRANGE list 0 10
1) "node4"
127.0.0.1:6379> LRANGE list2 0 10
1) "node3"
2) "node2"
3) "data3"
4) "data2"
5) "data1"

在实际的项目中 , 虽然阻塞可以有效保证了数据的一致性,但是阻塞就意味着其他进程的等待, CPU 需要给其他线程挂起、恢复等操作,更多的时候我们希望的并不是阻塞的处理请求,所以这些命令在实际中使用得并不多.

5、使用Spring 操作Redis 链表阻塞命令

package com.artisan.redis.baseStructure.list;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.data.redis.core.RedisTemplate;


public class SpringRedisBlockListDemo {
	
	private static final String KEY1 = "list1";
	private static final String KEY2 = "list2";
	
	public static void main(String[] args) {
		
		ApplicationContext ctx = new ClassPathXmlApplicationContext("classpath:spring/spring-redis-list.xml");
		RedisTemplate redisTemplate = ctx.getBean(RedisTemplate.class);

		// 清空操作
		redisTemplate.delete(KEY1);
		redisTemplate.delete(KEY2);
		
		// 127.0.0.1:6379> LPUSH list node1 node2 node3 node4 node5
		// (integer) 5
		
		List<String> list = new ArrayList<String>();
		for (int i = 1; i <= 5; i++) {
			list.add("node" + i);
		}
		redisTemplate.opsForList().leftPushAll(KEY1, list);
		scanList(redisTemplate, KEY1, 0, 4);
		System.out.println("---------------------------");

		// 127.0.0.1:6379> BLPOP list 2
		// 1) "list"
		// 2) "node5"
		// Spring 使用参数超时时间作为阻塞命令区分,等价于 blpop 命令,并且可以设置时间参数
		String lefpPodNode = (String) redisTemplate.opsForList().leftPop(KEY1, 2, TimeUnit.SECONDS);
		System.out.println("leftPopNode:" + lefpPodNode);
		System.out.println("---------------------------");

		// 127.0.0.1:6379> LRANGE list 0 4
		// 1) "node4"
		// 2) "node3"
		// 3) "node2"
		// 4) "node1"
		scanList(redisTemplate, KEY1, 0, 4);
		System.out.println("---------------------------");

		// 127.0.0.1:6379> BRPOP list 3
		// 1) "list"
		// 2) "node1"
		// Spring 使用参数超时时间作为阻塞命令区分,等价于 brpop 命令,并且可以设置时间参数
		System.out.println("rightPopNode:" + redisTemplate.opsForList().rightPop(KEY1, 3, TimeUnit.SECONDS));
		System.out.println("---------------------------");

		// 127.0.0.1:6379> LRANGE list 0 4
		// 1) "node4"
		// 2) "node3"
		// 3) "node2"
		scanList(redisTemplate, KEY1, 0, 4);
		System.out.println("---------------------------");


		// 127.0.0.1:6379> LPUSH list2 data1 data2 data3
		// (integer) 3

		List<String> list2 = new ArrayList<String>();
		for (int i = 3; i >= 1; i--) {
			list2.add("data" + i);
		}
		System.out.println(redisTemplate.opsForList().leftPushAll(KEY2, list2));
		System.out.println("---------------------------");


		// 127.0.0.1:6379> RPOPLPUSH list list2
		// "node2"
		// 相当于 rpoplpush 命令,弹出 list1最右边的节点,插入到 list2 最左边
		String value2 = (String) redisTemplate.opsForList().rightPopAndLeftPush(KEY1, KEY2);
		System.out.println("rightPopAndLeftPush:" + value2);
		System.out.println("-------------------");

		// 127.0.0.1:6379> BRPOPLPUSH list list2 3
		// "node3"
		// 相当于 brpoplpush 命令,注意在 Spring 中使用超时参数区分
		String value3 = (String) redisTemplate.opsForList().rightPopAndLeftPush(KEY1, KEY2, 3, TimeUnit.SECONDS);
		System.out.println("rightPopAndLeftPush:" + value3);
		System.out.println("-------------------");

		// 127.0.0.1:6379> LRANGE list 0 10
		// 1) "node4"
		scanList(redisTemplate, KEY1, 0, 10);
		System.out.println("-------------------");

		// 127.0.0.1:6379> LRANGE list2 0 10
		// 1) "node3"
		// 2) "node2"
		// 3) "data3"
		// 4) "data2"
		// 5) "data1"
		scanList(redisTemplate, KEY2, 0, 10);
		
	}

	private static void scanList(RedisTemplate redisTemplate, String key, int begin, int end) {
		List<String> data = redisTemplate.opsForList().range(key, begin, end);
		for (String string : data) {
			System.out.println("节点:" + string);
		}
	}
}

输出结果:

INFO : org.springframework.context.support.ClassPathXmlApplicationContext - Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@73a8dfcc: startup date [Wed Sep 26 12:53:56 CST 2018]; root of context hierarchy
INFO : org.springframework.beans.factory.xml.XmlBeanDefinitionReader - Loading XML bean definitions from class path resource [spring/spring-redis-list.xml]
节点:node5
节点:node4
节点:node3
节点:node2
节点:node1
---------------------------
leftPopNode:node5
---------------------------
节点:node4
节点:node3
节点:node2
节点:node1
---------------------------
rightPopNode:node1
---------------------------
节点:node4
节点:node3
节点:node2
---------------------------
3
---------------------------
rightPopAndLeftPush:node2
-------------------
rightPopAndLeftPush:node3
-------------------
节点:node4
-------------------
节点:node3
节点:node2
节点:data1
节点:data2
节点:data3

上面展示了 Redis 关于链表的阻塞命令,在 Spring 中它和非阻塞命令的方法是一致的,只是它会通过超时参数进行区分,而且我们还可以通过方法设置时间的单位。 注意,它是阻塞的命令,在多线程的环境中,它能在一定程度上保证数据 的一致而性能却不佳。

注意

使用 Spring 提供的 RedisTemplate 去展示多个命令可以学习到如何使用 RedisTemplate 操作 Redis 。 实际工作中并不是那么用的,因为每一 个操作会尝试从连接池里获取 一 个新的 Redis 连接,多个命令应该使用SessionCallback 接口进行操作 。

五、集合(set)

1、概述

Redis 的集合不是一个线性结构,而是一个哈希表结构,它的内部会根据 hash 分子来存储和查找数据,理论上一个集合可以存储2的32次方减一(约42亿)个元素。

因为采用哈希表结构,所以对于 Redis 集合的插入、删除和查找的复杂度都是 0(1),只是我们需要注意 3 点

  1. 对于集合而言,它的每一个元素都是不能重复的,当插入相同记录的时候都会失败
  2. 集合是无序的
  3. 集合的每一个元素都是 String 数据结构类型

2、常用集合命令

官网: https://redis.io/commands#set
Redis 全局命令、数据类型、内部编码、spring操作使用、使用场景_第9张图片

Redis 的集合可以对于不同的集合进行操作,比如求出两个或者以上集合的交集、 差集和并集等

命令 说明 备注
sadd key member1 [member2 member3 …] 给键为 key 的集合增加成员 可以同时增加多个
scard key 统计键为 key 的集合成员数
sdiff key1 [key2] 找出两个综合的差集 参数如果是单 key,那么 Redis 就返回这个 key 的所有元素
sdiffstore des key1 [key2] 先按 sdiff命令的规则,找出 key1 和 key2 两个集合的差集,然后将其保存到 des 集合
sinter key1 [key2] 求 key1 和 key2 两个集合的交集。 参数如果是单 key,那么 Redis 就返回这个 key 的所有元素
sinterstore des key1 key2 先按 sinter 命令的规则,找出 key1和 kye2两个集合的交集,然后保存到 des 中
sismember key member 判断 member 是否键为 key 的集合的成员 如果是返回1 , 否则返回0
smembers key 返回集合所有成员 如果数据最大,需要考虑法代泡历的问题
smove src des member 将成员 member 从集合 src 迁移到集合 des 中
spop key 随机弹出集合的一个元素 注意其随机性 , 因为集合是无序的
srandmember key [count] 随机返回集合 中一个或者多个元素 , count为限制返回总数,如果 count 为负数 , 则先求其绝对值 count 为整数,如果不填默认为1,如果count 大于等于集合总数, 则返回整个集合
srem key member1 [ member2 …] 移除集合中 的元素,可以是多个元素 对于很大 的集合可以通过它删除部分元素,避免删除大量数据引发 Redis 停顿
sunion key1 [key2] 求两个集合的并集 参数如果是单 key,那么 Redis 就返回这个 key 的所有元索
sunionstore des key1 key2 先执行 sunion 命令求 出并集,然后保存到键为 des 的集合中

上述命令的前缀都包含 了 一个 s,用来表达这是集合的命令 , 集合是无序的 , 并且支持并集 、 交集和差集的运算。

下面通过命令行客户端来演示这些命令

127.0.0.1:6379> FLUSHDB
OK
127.0.0.1:6379> SADD set1 v1 v2 v3 v4 v5 v6
(integer) 6
127.0.0.1:6379> SADD set2 v0 v2 v4 v6 v8
(integer) 5
127.0.0.1:6379> SCARD set1
(integer) 6
127.0.0.1:6379> SDIFF set1 set2
1) "v5"
2) "v1"
3) "v3"
127.0.0.1:6379> SINTER set1 set2
1) "v4"
2) "v6"
3) "v2"
127.0.0.1:6379> SISMEMBER set2 v2
(integer) 1
127.0.0.1:6379> SISMEMBER set1 v2
(integer) 1
127.0.0.1:6379> SISMEMBER set v2
(integer) 0
127.0.0.1:6379> SMEMBERS set2
1) "v4"
2) "v8"
3) "v6"
4) "v0"
5) "v2"
127.0.0.1:6379> SPOP set1
"v5"
127.0.0.1:6379> SMEMBERS set1
1) "v4"
2) "v6"
3) "v1"
4) "v2"
5) "v3"
127.0.0.1:6379> SRANDMEMBER set1 2
1) "v4"
2) "v3"
127.0.0.1:6379> SREM set1 v1
(integer) 1
127.0.0.1:6379> SMEMBERS set1
1) "v4"
2) "v6"
3) "v2"
4) "v3"
127.0.0.1:6379> SUNION set1 set2
1) "v4"
2) "v8"
3) "v6"
4) "v0"
5) "v2"
6) "v3"
127.0.0.1:6379> SMEMBERS set1
1) "v4"
2) "v6"
3) "v2"
4) "v3"
127.0.0.1:6379> SMEMBERS set2
1) "v4"
2) "v8"
3) "v6"
4) "v0"
5) "v2"
127.0.0.1:6379> 

3、交集、并集和差集保存命令的用法

127.0.0.1:6379> FLUSHDB
OK
127.0.0.1:6379> SADD set1  v1 v2 v3 v4 v5 v6
(integer) 6
127.0.0.1:6379> SADD set2 v2 v4 v6 v8 
(integer) 4
127.0.0.1:6379> SDIFFSTORE diff_set set1 set2 
(integer) 3
127.0.0.1:6379> SMEMBERS diff_set
1) "v5"
2) "v1"
3) "v3"
127.0.0.1:6379> SUNIONSTORE union_set  set1 set2
(integer) 7
127.0.0.1:6379> SMEMBERS union_set
1) "v4"
2) "v8"
3) "v5"
4) "v1"
5) "v6"
6) "v3"
7) "v2"
127.0.0.1:6379> SINTERSTORE inter_set set1 set2
(integer) 3
127.0.0.1:6379> SMEMBERS inter_set
1) "v6"
2) "v2"
3) "v4"
127.0.0.1:6379> 

上述命令主要是求差集、并集和交集 , 并保存到新的集合中。

4、Spring中操作Redis 集合命令


<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
	xmlns:context="http://www.springframework.org/schema/context"
	xmlns:p="http://www.springframework.org/schema/p"
	xsi:schemaLocation="http://www.springframework.org/schema/beans 
		http://www.springframework.org/schema/beans/spring-beans.xsd
		http://www.springframework.org/schema/context 
		http://www.springframework.org/schema/context/spring-context.xsd">

    <context:property-placeholder location="classpath:redis/redis.properties" />

    
    
    <bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig">
        
        <property name="maxIdle" value="${redis.maxIdle}" />
        
        <property name="maxTotal" value="${redis.maxTotal}" />
        
        <property name="maxWaitMillis" value="${redis.maxWaitMillis}" />
        
        <property name="minEvictableIdleTimeMillis" value="${redis.minEvictableIdleTimeMillis}" />
        
        <property name="numTestsPerEvictionRun" value="${redis.numTestsPerEvictionRun}" />
        
        <property name="timeBetweenEvictionRunsMillis" value="${redis.timeBetweenEvictionRunsMillis}" />
   		<property name="testOnBorrow" value="true">property>
		<property name="testOnReturn" value="true">property>
		<property name="testWhileIdle" value="true">property>
    bean>
	
	
    <bean id="jedisConnectionFactory"
        class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory"
        destroy-method="destroy">
        <property name="poolConfig" ref="jedisPoolConfig">property>
        
        <property name="hostName" value="${redis.host.ip}">property>
        
        <property name="port" value="${redis.port}">property>
        
        <property name="password" value="${redis.password}" /> 
        
        <property name="timeout" value="${redis.timeout}">property>
        <property name="usePool" value="true" />
        
    bean>
	
	
	<bean id="stringRedisSerializer" class="org.springframework.data.redis.serializer.StringRedisSerializer"/>

	
	<bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate"
		p:connection-factory-ref="jedisConnectionFactory"
		p:keySerializer-ref="stringRedisSerializer"
		p:defaultSerializer-ref="stringRedisSerializer"
		p:valueSerializer-ref="stringRedisSerializer">
	bean>

beans>
package com.artisan.redis.baseStructure.set;

import java.util.List;
import java.util.Set;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.data.redis.core.RedisTemplate;

/**
 * 
 * 
 * @ClassName: SpringRedisSetDemo
 * 
 * @Description: 记得将 RedisTemplate 值序列化器设置为 StringRedisSerializer 然后运行该代码
 * 
 * @author: Mr.Yang
 * 
 * @date: 2018年9月26日 下午3:22:57
 */
public class SpringRedisSetDemo {
	
	private static final String SET1 = "set1";
	private static final String SET2 = "set2";

	public static void main(String[] args) {
		
		ApplicationContext ctx = new ClassPathXmlApplicationContext("classpath:spring/spring-redis-set.xml");
		RedisTemplate<String, String> redisTemplate = (RedisTemplate<String, String>) ctx.getBean("redisTemplate");

		Set<String> set = null;

		// 127.0.0.1:6379> FLUSHDB
		// OK
		// 127.0.0.1:6379> SADD set1 v1 v2 v3 v4 v5 v6
		// (integer) 6
		// 127.0.0.1:6379> SADD set2 v0 v2 v4 v6 v8
		// (integer) 5
		// 将元素加入列表
		redisTemplate.boundSetOps(SET1).add("v1", "v2", "v3", "v4", "v5", "v6");
		redisTemplate.boundSetOps(SET2).add("v0", "v2", "v4", "v6", "v8");
		
		// 127.0.0.1:6379> SCARD set1
		// (integer) 6
		// 求集合长度
		System.out.println(SET1 + "的长度为:" + redisTemplate.opsForSet().size(SET1));
		System.out.println(SET2 + "的长度为:" + redisTemplate.opsForSet().size(SET2));

		// 127.0.0.1:6379> SDIFF set1 set2
		// 1) "v5"
		// 2) "v1"
		// 3) "v3"
		// 求差集
		set = redisTemplate.opsForSet().difference(SET1, SET2);
		scanSet(set);

		// 求并集
		set = redisTemplate.opsForSet().intersect(SET1, SET2);
		scanSet(set);
		
		
		// 判断是否是集合中的元素
		boolean exist = redisTemplate.opsForSet().isMember(SET1, "v1");
		System.out.println(SET1 + "中存在v1:" + exist);

		// 获取集合所有元素
		set = redisTemplate.opsForSet().members(SET1);
		scanSet(set);
		set = redisTemplate.opsForSet().members(SET2);
		scanSet(set);

		// 从集合中随机弹出一个元素,集合中会删除该元素
		String randomValue = redisTemplate.opsForSet().pop(SET2);
		System.out.println(SET2 + "中弹出的随机元素为" + randomValue);
		System.out.println(SET2 + "的长度为:" + redisTemplate.opsForSet().size(SET2));
		
		// 随机获取一个集合的元素 ,该元素仍然在集合中
		randomValue = (String) redisTemplate.opsForSet().randomMember(SET1);
		System.out.println(randomValue);
		System.out.println(SET1 + "的长度为:" + redisTemplate.opsForSet().size(SET1));

		System.out.println("------------");
		
		// 随机获取集合中 的4 个元素
		List<String> list = redisTemplate.opsForSet().randomMembers(SET1, 4);
		for (String string : list) {
			System.out.println(string);
		}

		// 删除一个集合的元素,参数可以是多个
		Long value = redisTemplate.opsForSet().remove(SET1, "v1", "v2");
		System.out.println(SET1 + "中删除了" + value + "个元素");
		System.out.println(SET1 + "的长度为:" + redisTemplate.opsForSet().size(SET1));

		// 求两个集合的并集
		set = redisTemplate.opsForSet().union(SET1, SET2);
		scanSet(set);
		
		// 求两个集合的差集,并保存到集合 diff_set 中
		redisTemplate.opsForSet().differenceAndStore(SET1, SET2, "diff_set");
		scanSet(redisTemplate.opsForSet().members("diff_set"));

		// 求两个集合的交集,并保存到集合 inter_set 中
		redisTemplate.opsForSet().intersectAndStore(SET1, SET2, "inter_set");
		scanSet(redisTemplate.opsForSet().members("inter_set"));

		// 求两个集合的并集,并保存到集合 union_set 中
		redisTemplate.opsForSet().unionAndStore(SET1, SET2, "union_set");
		scanSet(redisTemplate.opsForSet().members("union_set"));

	}

	private static void scanSet(Set<String> set) {
		for (String value : set) {
			System.out.println(value);
		}
		System.out.println("----------------");
	}

}
INFO : org.springframework.context.support.ClassPathXmlApplicationContext - Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@73a8dfcc: startup date [Wed Sep 26 16:18:55 CST 2018]; root of context hierarchy
INFO : org.springframework.beans.factory.xml.XmlBeanDefinitionReader - Loading XML bean definitions from class path resource [spring/spring-redis-set.xml]
set1的长度为:6
set2的长度为:5
v5
v1
v3
----------------
v4
v6
v2
----------------
set1中存在v1:true
v4
v5
v1
v6
v2
v3
----------------
v4
v8
v6
v2
v0
----------------
set2中弹出的随机元素为v4
set2的长度为:4
v3
set1的长度为:6
------------
v1
v1
v4
v4
set1中删除了2个元素
set1的长度为:4
v4
v5
v8
v6
v0
v3
v2
----------------
v5
v3
v4
----------------
v6
----------------
v4
v5
v8
v6
v0
v3
v2
----------------

注意

使用 Spring 提供的 RedisTemplate 去展示多个命令可以学习到如何使用 RedisTemplate 操作 Redis 。 实际工作中并不是那么用的,因为每一 个操作会尝试从连接池里获取 一 个新的 Redis 连接,多个命令应该使用SessionCallback 接口进行操作 。

六、有序集合(zset)

1、概述

有序集合和集合类似,只是说它是有序的,和无序集合的主要区别在于每一个元素除了值之外,它还会多一个分数。

  1. 分数是一个浮点数,在 Java 中是使用双精度表示的,根据分数, Redis 就可以支持对分数从小到大或者从大到小的排序
  2. 和无序集合一样,对于每一个元素都是唯一的 ,但是对于不同元素而言,它的分数可以一样
  3. 元素也是 String 数据类型,也是一种基于 hash 的存储结构。
  4. 集合是通过哈希表实现的,所以添加、删除、 查找的复杂度都是 0(1)
  5. 集合中最大的成员数为 2的32次方减 1 ( 40 多亿个成员)

2、有序集合的数据结构

Redis 全局命令、数据类型、内部编码、spring操作使用、使用场景_第10张图片

有序集合是依赖 key 标示它是属于哪个集合,依赖分数进行排序,所以值和分数是必须的,而实际上不仅可以对分数进行排序,在满足一定的条件下,也可以对值进行排序 。

3、Redis 有序集合的部分命令

官网: https://redis.io/commands#sorted_set

有序集合和无序集合的命令是接近的,只是在这些命令的基础上,会增加对于排序的操作,这些是我们在使用的时候需要注意的细节.

有些时候 Redis 借助数据区间的表示方法来表示包含或者不包含,比如在数学的区间表示中[2,5 ]表示包含 2,但是不包含 5 的 区间。

Redis 全局命令、数据类型、内部编码、spring操作使用、使用场景_第11张图片

命令 说明 备注
zadd key score1 value1 [score2 value2 …] 向有序集合的 key,增加一个或者多个成员 如果不存在对应的 key,则创建键为 key 的有序集合
zcard key 获取有序集合的成员数 -----
zcount key min max 根据分数返回对应的成员列表 min 为最小值, max为最大值,默认为包含min 和 max 值,采用数学区间表示的方法,如果需要不包含,则在分数前面加入“(”,注意不支持“[”表示
zincrby key increment member 给有序集合成员值为 member 的分数增加 increment -----
zinterstore desKey nurnkeys key1 [key2 key3 …] 求多个有序集合的交集,并且将结果保存到 des Key 中 numkeys 是一个整数,表示多少个有序集合
zlexcount key min max 求有序集合 key 成员值在 min 和 max 的范围 这里范围为 key 的成员值, Redis 借助数据区间的表示方法,“[”表示包含该值,“(”表示不包含该值
zrange key start stop [withscores] 按照分值的大小〈从小到大)返回成员,加入 start 和 stop 参数可以截取某一段返回.如果输入可选项 withscores,则连同分数一起返回 这里记集合最大长度为len,Redis 会将集合排序后,形成一个从 0 到len-1的下标,然后根据 start 和 stop 控制的下标(包含 start 和 stop)返回
zrank key member 按从小到大求有序集合的排行 排名第一的为 0,第二的为 1 …
zrangebylex key min max [limit offset count] 根据值的大小,从小到大排序, min 为最小值, max 为最大值;limit 选项可选,当 Red is 求出范围集合后,会生产下标0到n,然后根据偏移量offset 和限定返回 数 count,返回对应的成员 这里范围为 key 的成员值, Red i s 借助数学区间的表示方法,“[”表示包含该值,“(”表示不包含该值
zrangebyscore key min max [withscores] [limit offset count] 根据分数大小,从小到大求取范围,选项 withscores 和 limit 请参考 zrange 命令和zrangebylex 说明 根据分析求取集合的范围。这里默认包含 min和 max,如果不想包含,则在参数前加入“(”,注意不支持“ [”表示
zremrangebyscore key start stop 根据分数区间进行删除 按照 socre 进行排序,然后排除 0 到len-1的下标,然后根据 start 和 stop 进行删除, Redis 借助数学区间的表示方法,“[”表示包含该值,“(”表示不包含该值
zremrangebyrank key start stop 按照分数排行从小到大的排序删除,从0开始计算 -----
zremrangebylex key min max 按照值的分布进行删除 -----
zrevrange key start stop [withscores] 从大到小的按分数排序,参数请参见zrange 与 zrange 相同,只是排序是从大到小
zrevrangebyscore key max min [withscores] 从大到小的按分数排序,参数请参见zrangebyscore 与 zrangebyscore 相同 ,只是排序是从大到小
zrevrank key member 按从大到小的顺序,求元素的排行 排名第一位 0,第二位1 …
zscore key member 返回成员的分数值 返回成员的分数
zunionstore desKey numKeys key1 [key2 key3 key4 …] 求多个有序集合的并集,其中 numKeys是有序,集合的个数 -----

在对有序集合、下标、区间的表示方法进行操作的时候,需要十分小心命令,注意它是操作分数还是值,稍有不慎就会出现问题。

# 为了测试的数据干净,删除当前db的数据

127.0.0.1:6379> FLUSHDB
OK
127.0.0.1:6379> 
127.0.0.1:6379> 
#  zadd key score1 value1 [score2 value2 …] 向有序集合zset1 ,增加9个成员 
127.0.0.1:6379> ZADD zset1 1 x1 2 x2 3 x3 4 x4 5 x5 6 x6 7 x7 8 x8 9 x9
(integer) 9


#  zadd key score1 value1 [score2 value2 …] 向有序集合zset2 ,增加9个成员
127.0.0.1:6379> ZADD zset2 1 y1 2 x2 3 y3 4 x4 5 y5 6 x6 7 y7 8 x8 9 y9
(integer) 9


# zcard key 获取有序集合zset1的成员数
127.0.0.1:6379> ZCARD zset1
(integer) 9


# zcount key min max  根据分数返回对应的成员列表
127.0.0.1:6379> ZCOUNT zset1 1 4 
(integer) 4

# zinterstore desKey nurnkeys key1 [key2 key3 …] 求多个有序集合的交集,并且将结果保存到 des Key 中
127.0.0.1:6379> ZINTERSTORE des_key 2 zset1 zset2 
(integer) 4

# zlexcount key min max  求有序集合 zset1 成员值在 min 和 max 的范围 [ 表示包含该值,( 表示不包含该值
127.0.0.1:6379> ZLEXCOUNT zset1 (x1 [x5
(integer) 4

# zrange key start stop [withscores] 按照分值的大小(从小到大)返回成员,加入 start 和 stop 参数可以截取某一段返回.如果输入可选项 withscores,则连同分数一起返回
127.0.0.1:6379> ZRANGE zset1 1 5 withscores
 1) "x2"
 2) "2"
 3) "x3"
 4) "3"
 5) "x4"
 6) "4"
 7) "x5"
 8) "5"
 9) "x6"
10) "6"



#  zrank key member 按从小到大求有序集合的排行
127.0.0.1:6379> ZRANK zset1 x5
(integer) 4

#  zrangebylex key min max [limit offset count]根据值的大小,从小到大排序   [表示包含该值 (表示不包含该值
127.0.0.1:6379> ZRANGEBYLEX zset1 (x1 [x6
1) "x2"
2) "x3"
3) "x4"
4) "x5"
5) "x6"
127.0.0.1:6379> 

# zrangebyscore key min max [withscores] [limit offset count] 根据分数大小,从小到大求取范围
127.0.0.1:6379> ZRANGEBYSCORE zset1 5 7 
1) "x5"
2) "x6"
3) "x7"

# zrangebyscore key min max [withscores] [limit offset count] 根据分数大小,从小到大求取范围
127.0.0.1:6379> ZRANGEBYSCORE zset1 2 7 withscores limit 1 5 
 1) "x3"
 2) "3"
 3) "x4"
 4) "4"
 5) "x5"
 6) "5"
 7) "x6"
 8) "6"
 9) "x7"
10) "7"

# zrevrange key start stop [withscores] 从大到小的按分数排序
127.0.0.1:6379> ZREVRANGE zset1 1 5 
1) "x8"
2) "x7"
3) "x6"
4) "x5"
5) "x4"

#  zrevrangebyscore key max min [withscores] 从大到小的按分数排序
127.0.0.1:6379> ZREVRANGEBYSCORE zset2 5 2 withscores
1) "y5"
2) "5"
3) "x4"
4) "4"
5) "y3"
6) "3"
7) "x2"
8) "2"

#  zrevrank key member 按从大到小的顺序,求元素的排行
127.0.0.1:6379> ZREVRANK zset1 x4
(integer) 5

# zscore key member 返回成员的分数值
127.0.0.1:6379> ZSCORE zset1 x5
"5"

# zunionstore desKey numKeys key1 [key2 key3 key4 …] 求多个有序集合的并集,其中 numKeys是有序,集合的个数
127.0.0.1:6379> ZUNIONSTORE des_key 2 zset1 zset2
(integer) 14

# zincrby key increment member 给有序集合成员值为 member 的分数增加 increment
127.0.0.1:6379> ZINCRBY zset1 5 x9
"14"

# zremrangebyscore key start stop 根据分数区间进行删除
127.0.0.1:6379> ZREMRANGEBYSCORE zset1 3 2
(integer) 0

# zremrangebyscore key start stop 根据分数区间进行删除
127.0.0.1:6379> ZREMRANGEBYSCORE zset1 3 5 
(integer) 3

# zremrangebyrank key start stop 按照分数排行从小到大的排序删除,从0开始计算
127.0.0.1:6379> ZREMRANGEBYRANK zset1 1 3 
(integer) 3

# zremrangebylex key min max  按照值的分布进行删除
127.0.0.1:6379> ZREMRANGEBYLEX zset2 [y1 [y5
(integer) 6
127.0.0.1:6379>
127.0.0.1:6379> ZCARD zset1
(integer) 3
127.0.0.1:6379> ZCARD zset2
(integer) 3
127.0.0.1:6379> 
127.0.0.1:6379> ZRANGE zset1 0 999
1) "x1"
2) "x8"
3) "x9"
127.0.0.1:6379> ZRANGE zset2  0 999
1) "y7"
2) "x8"
3) "y9"
127.0.0.1:6379> 

4、spring-data-redis 对有序集合的封装

在 Spring 中使用 Redis 的有序集合,需要注意的是 Spring 对 Redis 有序集合的元素的值和分数的范围( Range )和限制( Limit)进行了封装。

我们来看下Spring是如何封装的。 先介绍一个主要的接口一一TypedTuple,它不是一个普通的接口,而一个内部接口.

org.springframework . data. redis.core .ZSetOperations 接口的内部接口,它定义了两个方

  • getValue()是获取值, getScore()是获取分数,但是它只是一个接口,而不是一个实现类
  • spring-data-red is 提供了 一个默认的实现类一DefaultTypedTuple

在默认的情况下 Spring 就会把带有分数的有序集合的值和分数封装到这个类中 ,这样就可以通过这个类对象读取对应的值和分数了 .

Spring 不仅对有序集合元素封装,而且对范围也进行了封装,方便使用.它是使用接口 org.springframe.work.data.redis.connection.RedisZSetCommands 下的内部类 Range 进行封装的,它有一个静态的 range()方法,使用它就可以生成一个 Range 对象了,只是要清楚 Range对象的几个方法才行.

//  设置大于等于 min
public Range gte(Object min)
//  设置大于 min
public Range gt(Object min)
//  设置小于等于 max
public Range lte(Object max)
//  设置小于 max
public Range lt(Object max)

这 4 个方法就是最常用的范围方法.

下面看一下limit,它是接口 org.springframework.data.redis.connection.RedisZSetCommands 下的内部类,它是一个简单的 POJO,它存在两个属性

通过属性的名称很容易知道:offset 代表从第几个开始截取,而 count 代表限制返回的总数量。

5、使用 Spring 操作有序集合

刚才讨论了 spring-data-redis 项目对有序集合的封装,在此基础上这里的演示示例代码在测试代码前,要把 RedisTemplate 的 keySerializer 和 valueSerializer属性都修改为字符串序列化器 StringRedisSerializer

配置文件

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
	xmlns:context="http://www.springframework.org/schema/context"
	xmlns:p="http://www.springframework.org/schema/p"
	xsi:schemaLocation="http://www.springframework.org/schema/beans 
		http://www.springframework.org/schema/beans/spring-beans.xsd
		http://www.springframework.org/schema/context 
		http://www.springframework.org/schema/context/spring-context.xsd">

    <context:property-placeholder location="classpath:redis/redis.properties" />

    <!--2,注意新版本2.3以后,JedisPoolConfig的property name,不是maxActive而是maxTotal,而且没有maxWait属性,建议看一下Jedis源码或百度。 -->
    <!-- redis连接池配置 -->
    <bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig">
        <!--最大空闲数 -->
        <property name="maxIdle" value="${redis.maxIdle}" />
        <!--连接池的最大数据库连接数 -->
        <property name="maxTotal" value="${redis.maxTotal}" />
        <!--最大建立连接等待时间 -->
        <property name="maxWaitMillis" value="${redis.maxWaitMillis}" />
        <!--逐出连接的最小空闲时间 默认1800000毫秒(30分钟) -->
        <property name="minEvictableIdleTimeMillis" value="${redis.minEvictableIdleTimeMillis}" />
        <!--每次逐出检查时 逐出的最大数目 如果为负数就是 : 1/abs(n), 默认3 -->
        <property name="numTestsPerEvictionRun" value="${redis.numTestsPerEvictionRun}" />
        <!--逐出扫描的时间间隔(毫秒) 如果为负数,则不运行逐出线程, 默认-1 -->
        <property name="timeBetweenEvictionRunsMillis" value="${redis.timeBetweenEvictionRunsMillis}" />
   		<property name="testOnBorrow" value="true"></property>
		<property name="testOnReturn" value="true"></property>
		<property name="testWhileIdle" value="true"></property>
    </bean>
	
	<!--redis连接工厂 -->
    <bean id="jedisConnectionFactory"
        class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory"
        destroy-method="destroy">
        <property name="poolConfig" ref="jedisPoolConfig"></property>
        <!--IP地址 -->
        <property name="hostName" value="${redis.host.ip}"></property>
        <!--端口号 -->
        <property name="port" value="${redis.port}"></property>
        <!--如果Redis设置有密码 -->
        <property name="password" value="${redis.password}" /> 
        <!--客户端超时时间单位是毫秒 -->
        <property name="timeout" value="${redis.timeout}"></property>
        <property name="usePool" value="true" />
        <!--<property name="database" value="0" /> -->
    </bean>
	
	<!-- 键值序列化器设置为String 类型 -->
	<bean id="stringRedisSerializer" class="org.springframework.data.redis.serializer.StringRedisSerializer"/>

	<!-- redis template definition -->
	<bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate"
		p:connection-factory-ref="jedisConnectionFactory"
		p:keySerializer-ref="stringRedisSerializer"
		p:defaultSerializer-ref="stringRedisSerializer"
		p:valueSerializer-ref="stringRedisSerializer">
	</bean>

</beans>
package com.artisan.redis.baseStructure.zset;

import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.data.redis.connection.RedisZSetCommands.Limit;
import org.springframework.data.redis.connection.RedisZSetCommands.Range;
import org.springframework.data.redis.core.DefaultTypedTuple;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.data.redis.core.ZSetOperations.TypedTuple;


public class SpringRedisZSetDemo {

	private static final String ZSET1 = "zset1";
	private static final String ZSET2 = "zset2";

	@SuppressWarnings({ "unchecked", "rawtypes" })
	public static void main(String[] args) {

		ApplicationContext ctx = new ClassPathXmlApplicationContext("classpath:spring/spring-redis-zset.xml");
		RedisTemplate redisTemplate = (RedisTemplate) ctx.getBean("redisTemplate");

		// 方便测试,清空数据
		redisTemplate.delete(ZSET1);
		redisTemplate.delete(ZSET2);

		// Spring 提供接口 TypedTuple 操作有序集合
		Set<TypedTuple> set1 = new HashSet<ZSetOperations.TypedTuple>();
		Set<TypedTuple> set2 = new HashSet<ZSetOperations.TypedTuple>();

		// 构造数据
		// 127.0.0.1:6379>
		// # zadd key score1 value1 [score2 value2 …] 向有序集合zset1 ,增加9个成员
		// >ZADD zset1 1 x1 2 x2 3 x3 4 x4 5 x5 6 x6 7 x7 8 x8 9 x9
		// (integer) 9

		// # zadd key score1 value1 [score2 value2 …] 向有序集合zset2 ,增加9个成员
		// > ZADD zset2 1 y1 2 x2 3 y3 4 x4 5 y5 6 x6 7 y7 8 x8 9 y9
		// (integer) 9
		int j = 9;
		
		String value1, value2 = null;
		double score1, score2 = 0.0;
		for (int i = 1; i <= 9; i++) {

			// 计算分数和值
			 score1 = Double.valueOf(i);
			 value1 = "x" + i;
			
			if (j > 0) {
				score2 = Double.valueOf(j);
				value2 = j % 2 == 1 ? "y" + j : "x" + j;
				j--;
			}
			// 使用 Spring提供的默认 TypedTuple-DefaultTypedTuple
			TypedTuple typedTuplel = new DefaultTypedTuple(value1,score1);
			set1.add(typedTuplel);
			TypedTuple typedTuple2 = new DefaultTypedTuple(value2,score2);
			set2.add(typedTuple2);

		}
		// 写入redis
		redisTemplate.opsForZSet().add(ZSET1, set1);
		redisTemplate.opsForZSet().add(ZSET2, set2);
		
		// 统计总数
		Long size = redisTemplate.opsForZSet().size(ZSET1);
		System.out.println(ZSET1 + "的size为" + size);

		// 计分数为 score ,那么下面的方法就是求 3<=score<=6 的元素
		Long count = redisTemplate.opsForZSet().count(ZSET1, 3, 6);
		System.out.println(ZSET1 + "中3<=score<=6 的count为" + count);

		// 从下标一开始截驭 5 个元素,但是不返回分数 , 每一个元素是 String
		Set set = redisTemplate.opsForZSet().range(ZSET1, 1, 5);
		printSet(set);

		// 截取集合所有元素,并且对集合按分数排序,并返回分数 , 每一个元素是 TypedTuple
		Set<TypedTuple> typedTuples = redisTemplate.opsForZSet().rangeWithScores(ZSET1, 0, -1);
		printTypedTuple(typedTuples);
		
		
		// 将 zsetl 和 zset2 两个集合的交集放入集合 inter_zset
		size = redisTemplate.opsForZSet().intersectAndStore(ZSET1, ZSET2, "inter_zset");
		System.out.println("inter_zset size:" + size);
		
		// 查看交集inter_zset中的数据
		set = redisTemplate.opsForZSet().range("inter_zset", 0, redisTemplate.opsForZSet().size("inter_zset"));
		printSet(set);

		// 区间
		Range range = Range.range();
		range.lt("x8");// 小于
		range.gt("x1");// 大于

		set = redisTemplate.opsForZSet().rangeByLex(ZSET1, range);
		printSet(set);

		range.lte("x8");// 小于等于
		range.gte("x1");// 大于等于

		set = redisTemplate.opsForZSet().rangeByLex(ZSET1, range);
		printSet(set);
		
		// 限制返回个数
		Limit limit = Limit.limit();
		// 限制返回个数
		limit.count(4);
		// 限制从第2个开始截取
		limit.offset(2);
		
		// 求区间内的元素,并限制返回 4 条
		set = redisTemplate.opsForZSet().rangeByLex(ZSET1, range, limit);
		printSet(set);

		// 求排行,排名第 1 返回 0 ,第 2 返回 1
		Long rank = redisTemplate.opsForZSet().rank(ZSET1, "x4");
		System.out.println("rank=" + rank);

		// 删除元素 , 返回删除个数
		size = redisTemplate.opsForZSet().remove(ZSET1, "x5", "x6");
		System.out.println("remove " + size + " 个元素");

		// 按照排行删除从 0 开始算起,这里将删除第排名第 2 和第 3 的元素
		size = redisTemplate.opsForZSet().removeRange(ZSET1, 1, 2);
		System.out.println("removeRange " + size + " 个元素");

		// 获取所有集合的元索和分数 , 以 -1 代表全部元素
		typedTuples = redisTemplate.opsForZSet().rangeWithScores(ZSET1, 0, -1);
		printTypedTuple(typedTuples);
		
		// 删除指定的元素
		size = redisTemplate.opsForZSet().remove(ZSET2, "y3", "y5");
		System.out.println("remove " + size + " 个元素");
		
		// 给集合中的一个元素的分数加上 11
		Double double1 = redisTemplate.opsForZSet().incrementScore(ZSET2, "y1", 11);
		printTypedTuple(redisTemplate.opsForZSet().rangeWithScores(ZSET2, 0, redisTemplate.opsForZSet().size(ZSET2)));
		
		// 从大到小排列
		typedTuples = redisTemplate.opsForZSet().reverseRangeWithScores(ZSET2, 0, 99);
		printTypedTuple(typedTuples);
	}
	
	@SuppressWarnings("rawtypes")
	public static void printTypedTuple(Set<TypedTuple> typedTuples) {
		if (typedTuples != null && typedTuples.isEmpty()) {
			return;
		}
		Iterator<TypedTuple> iterator = typedTuples.iterator();
		while (iterator.hasNext()) {
			TypedTuple typedTuple = iterator.next();
			System.out.println("{value =" + typedTuple.getValue() + ", score=" + typedTuple.getScore() + "}");
		}
		System.out.println("----------------------");
	}
	
	@SuppressWarnings("rawtypes")
	public static void printSet(Set set) {
		if (set != null && set.isEmpty()) {
			return;
		}
		Iterator iterator = set.iterator();
		while (iterator.hasNext()) {
			Object val = iterator.next();
			System.out.println(val + "\t");
		}
		System.out.println("----------------------");
	}
}

输出:

INFO : org.springframework.context.support.ClassPathXmlApplicationContext - Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@73a8dfcc: startup date [Wed Sep 26 23:26:54 CST 2018]; root of context hierarchy
INFO : org.springframework.beans.factory.xml.XmlBeanDefinitionReader - Loading XML bean definitions from class path resource [spring/spring-redis-zset.xml]
zset1的size为9
zset1中3<=score<=6 的count为4
x2	
x3	
x4	
x5	
x6	
----------------------
{value =x1, score=1.0}
{value =x2, score=2.0}
{value =x3, score=3.0}
{value =x4, score=4.0}
{value =x5, score=5.0}
{value =x6, score=6.0}
{value =x7, score=7.0}
{value =x8, score=8.0}
{value =x9, score=9.0}
----------------------
inter_zset size:4
x2	
x4	
x6	
x8	
----------------------
x2	
x3	
x4	
x5	
x6	
x7	
----------------------
x1	
x2	
x3	
x4	
x5	
x6	
x7	
x8	
----------------------
x3	
x4	
x5	
x6	
----------------------
rank=3
remove 2 个元素
removeRange 2 个元素
{value =x1, score=1.0}
{value =x4, score=4.0}
{value =x7, score=7.0}
{value =x8, score=8.0}
{value =x9, score=9.0}
----------------------
remove 2 个元素
{value =x2, score=2.0}
{value =x4, score=4.0}
{value =x6, score=6.0}
{value =y7, score=7.0}
{value =x8, score=8.0}
{value =y9, score=9.0}
{value =y1, score=12.0}
----------------------
{value =y1, score=12.0}
{value =y9, score=9.0}
{value =x8, score=8.0}
{value =y7, score=7.0}
{value =x6, score=6.0}
{value =x4, score=4.0}
{value =x2, score=2.0}
----------------------

注意

使用 Spring 提供的 RedisTemplate 去展示多个命令可以学习到如何使用 RedisTemplate 操作 Redis 。 实际工作中并不是那么用的,因为每一 个操作会尝试从连接池里获取 一 个新的 Redis 连接,多个命令应该使用SessionCallback 接口进行操作 。

七、基数HyperLogLog

1、概述

基数是一种算法。

举个例子 , 一本英文著作由数百万个单词组成,你的内存却不足以存储它们,那么我们先分析一下业务。英文单词本身是有限的,在这本书的几百万个单词中有许许多多重复单词 ,扣去重复的单词,这本书中也就是几千到一万多个单词而己,那么内存就足够存储它们 了。比如数字集合{1,2,5,7,9, 1,5,9 }的基数集合为{ 1,2,5,7,9}那么基数(不重复元素)就是 5 , 基数的作用是评估大约需要准备多少个存储单元去存储数据,但是基数的算法一般会存在一定的误差(一般是可控的)。

Redis 对基数数据结构的支持是从版本 2.8.9 开始的。

基数并不是存储元素,存储元素消耗内存空间比较大,而是给某一个有重复元素的数据集合( 一般是很大的数据集合〉评估需要的空间单元数,所以它没有办法进行存储 ,加上在工作中用得不多 ,所以简要介绍一下 Redis的HyperLogLog 命令就可以了.

2、Redis 的 Hyperloglog 命令

官网:https://redis.io/commands#hyperloglog

命令 说明 备注
pfadd key element 添加指定元素到 HyperLogLog 中 如果已经存储元素,则返回为 0,添加失败
pfcount key 返回 HyperLogLog 的基数值 ----
pfmerge desKey key1 [key2 key3 …] 合并多个 HyperLogLog,并将其保存在 desKey 中 ----
127.0.0.1:6379> FLUSHDB
OK
127.0.0.1:6379> PFADD h1 a
(integer) 1
127.0.0.1:6379> PFADD h1 b
(integer) 1
127.0.0.1:6379> PFADD h1 c
(integer) 1
127.0.0.1:6379> PFADD h1 d
(integer) 1
127.0.0.1:6379> PFADD h1 a
(integer) 0
127.0.0.1:6379> PFADD h2 a
(integer) 1
127.0.0.1:6379> PFADD h2 z
(integer) 1
127.0.0.1:6379> PFMERGE h3 h1 h2
OK
127.0.0.1:6379> PFCOUNT h3
(integer) 5
127.0.0.1:6379> 

分析一下逻辑,首先往一个键为 h1的 HyperLogLog 插入元素 ,让其计算基数,到 了第 5 个命令“ pfadd h1 a”的时候,由于在此以前已经添加过,所以返回了 0。 它 的基数集合是{a,b,c,d},因此求集合长度是4 。

之后再添加第二个基数h2,它的基数是{a,z},所以在合并h1和h2到h3中的时候,它的基数和为{a,b,c,d,z}。所以求它的基数是5.

3、Spring 中操作基数


<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
	xmlns:context="http://www.springframework.org/schema/context"
	xmlns:p="http://www.springframework.org/schema/p"
	xsi:schemaLocation="http://www.springframework.org/schema/beans 
		http://www.springframework.org/schema/beans/spring-beans.xsd
		http://www.springframework.org/schema/context 
		http://www.springframework.org/schema/context/spring-context.xsd">

    <context:property-placeholder location="classpath:redis/redis.properties" />

    
    
    <bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig">
        
        <property name="maxIdle" value="${redis.maxIdle}" />
        
        <property name="maxTotal" value="${redis.maxTotal}" />
        
        <property name="maxWaitMillis" value="${redis.maxWaitMillis}" />
        
        <property name="minEvictableIdleTimeMillis" value="${redis.minEvictableIdleTimeMillis}" />
        
        <property name="numTestsPerEvictionRun" value="${redis.numTestsPerEvictionRun}" />
        
        <property name="timeBetweenEvictionRunsMillis" value="${redis.timeBetweenEvictionRunsMillis}" />
   		<property name="testOnBorrow" value="true">property>
		<property name="testOnReturn" value="true">property>
		<property name="testWhileIdle" value="true">property>
    bean>
	
	
    <bean id="jedisConnectionFactory"
        class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory"
        destroy-method="destroy">
        <property name="poolConfig" ref="jedisPoolConfig">property>
        
        <property name="hostName" value="${redis.host.ip}">property>
        
        <property name="port" value="${redis.port}">property>
        
        <property name="password" value="${redis.password}" /> 
        
        <property name="timeout" value="${redis.timeout}">property>
        <property name="usePool" value="true" />
        
    bean>
	
	
	<bean id="stringRedisSerializer" class="org.springframework.data.redis.serializer.StringRedisSerializer"/>

	
	<bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate"
		p:connection-factory-ref="jedisConnectionFactory"
		p:keySerializer-ref="stringRedisSerializer"
		p:defaultSerializer-ref="stringRedisSerializer"
		p:valueSerializer-ref="stringRedisSerializer">
	bean>

beans>
package com.artisan.redis.baseStructure.hyperloglgo;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.data.redis.core.RedisTemplate;



public class SpringRedisHyperLogLogDemo {

	@SuppressWarnings({ "unchecked", "rawtypes", "resource" })
	public static void main(String[] args) {
		
		ApplicationContext ctx = new ClassPathXmlApplicationContext("classpath:spring/spring-redis-hyperloglog.xml");

		RedisTemplate redisTemplate = (RedisTemplate) ctx.getBean("redisTemplate");

		// 为确保数据干净,先清除
		redisTemplate.delete("h1");
		redisTemplate.delete("h2");
		redisTemplate.delete("h3");

		// 添加指定元素到 HyperLogLog 中
		Long count = redisTemplate.opsForHyperLogLog().add("h1", "a", "b", "c", "d", "a");
		System.out.println(count);
		count = redisTemplate.opsForHyperLogLog().add("h2", "a");
		System.out.println(count);
		count = redisTemplate.opsForHyperLogLog().add("h2", "z");
		System.out.println(count);

		Long size = redisTemplate.opsForHyperLogLog().size("h1");
		System.out.println(size);
		Long size2 = redisTemplate.opsForHyperLogLog().size("h2");
		System.out.println(size2);

		Long size3 = redisTemplate.opsForHyperLogLog().union("h3", "h1", "h2");
		System.out.println(size3);

		Long size4 = redisTemplate.opsForHyperLogLog().size("h3");
		System.out.println(size4);
	}
	
}

输出:

INFO : org.springframework.context.support.ClassPathXmlApplicationContext - Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@73a8dfcc: startup date [Thu Sep 27 00:11:19 CST 2018]; root of context hierarchy
INFO : org.springframework.beans.factory.xml.XmlBeanDefinitionReader - Loading XML bean definitions from class path resource [spring/spring-redis-hyperloglog.xml]
1
1
1
4
2
5
5

注意

使用 Spring 提供的 RedisTemplate 去展示多个命令可以学习到如何使用 RedisTemplate 操作 Redis 。 实际工作中并不是那么用的,因为每一 个操作会尝试从连接池里获取 一 个新的 Redis 连接,多个命令应该使用SessionCallback 接口进行操作 。

你可能感兴趣的:(Redis非关系型数据库)