哈希概述
Redis 中哈希结构就如同 Java 的 map 一样 , 一个对象里面有许多键值对,它是特别适合存储对象的.
如果内存足够大 ,那么一个 Redis 的 hash 结构可以存储2的32次方-1个键值对 ( 40多亿)。
在 Redis 中, hash 是一个 String 类型的 field 和 value 的映射表,因此我们存储的数据实际在 Redis 内存中都是一个个字符串而己。
假设artisan有 3 个字段 : 编号( id)、名称 (name )、性别( sex),这样就可以使用一个 hash 结构保存它。
在 Redis 中它就是一个这样的结构,其中 artisan代表的是这个 hash 结构在 Redis 内存的 key,通过它就可以找到这个 hash 结构,而 hash 结构由一系列的 field 和 value 组成
客户端操作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>
Redis hash 结构命令
官网:https://redis.io/commands#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>
Spring操作reids的hash
Step1 修改defaultSerializer
<?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>
在 Redis 中, hash 是一个 String 类型的 field 和 value 的映射表。 Spring 对 Redis 进行了封装,所以有必要对 RedisTemplate 的配置项进行修改。修改defaultSerializer-ref
如果不指定的话就是
否则抛出如下异常
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
Step2 操作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 接口进行操作 。