最近在做一个关于群聊的项目,由于聊天要求实时性,不可能直接访问数据库,所以使用了Redis来做缓存,这里将使用过程中遇到的问题记录一下。
使用Redis之前需要合理设计存储在其中的数据结构,比如聊天系统,就需要存储包括用户登录状态信息,可以使用Hash数据结构,以用户Id做键,状态做值,还可以存储每个用户在每个群的未读消息计数,
客户端使用长轮询拉取。
最主要的就是群里的聊天记录的缓存了,这里可以使用Redis的List数据结构来做消息队列,其中按顺序存储消息实体对象(序列化后存入Redis)。还可以维护一个List存储聊天记录的主键便于定位聊天记录,等等的一系列数据都可以缓存到Redis。
Redis在Java中的实现是Jedis,其中封装了连接Redis的连接池实现、对Redis数据库的各种操作等。
在JavaWeb项目中使用Redis做缓存时有两种实现方式,第一种是使用原生的Jedis包,其中封装的方法可以直接操作Redis。第二种方法是使用Spring框架集成的Redis,其中封装Jedis对Redis的各种操作,包括事务、连接、连接池的维护等,其对Redis的操作进行了很多封装。
这个聊天项目一开始采用的是Spring封装的Redis,使用过程中发现,经常会报一些类型转换异常的问题,后来发现这种错误经常在网络连接异常或是长轮询的地方出现。深入底层发现是由于Redis底层的Socket连接使用了缓冲区,导致有些时候如果前面的连接出现问题,后面的请求再使用前面的连接会获取到前面连接的数据,导致类型转换异常的出现,
尤其是长轮询不断请求Redis服务器的时候,问题出现的频率尤其高,而且一旦出现问题,后面的连接都会出问题,这也许是由于Redis连接池中出问题的连接没有完全释放的原因。
因此建议大家如果使用Redis做缓存,最好使用原生的Jedis,这样可以在连接异常时自己实现异常连接的处理,而使用Spring集成的Redis由于连接已经被封装了,限制了自己对异常连接的处理。
下面就将两种集成Redis的方法分别介绍一下。
第一种是使用Spring集成的Redis。
使用Redis之前需要合理设计存储在其中的数据结构,比如聊天系统,就需要存储包括用户登录状态信息,可以使用Hash数据结构,以用户Id做键,状态做值,还可以存储每个用户在每个群的未读消息计数,
客户端使用长轮询拉取。
最主要的就是群里的聊天记录的缓存了,这里可以使用Redis的List数据结构来做消息队列,其中按顺序存储消息实体对象(序列化后存入Redis)。还可以维护一个List存储聊天记录的主键便于定位聊天记录,等等的一系列数据都可以缓存到Redis。
Redis在Java中的实现是Jedis,其中封装了连接Redis的连接池实现、对Redis数据库的各种操作等。
在JavaWeb项目中使用Redis做缓存时有两种实现方式,第一种是使用原生的Jedis包,其中封装的方法可以直接操作Redis。第二种方法是使用Spring框架集成的Redis,其中封装Jedis对Redis的各种操作,包括事务、连接、连接池的维护等,其对Redis的操作进行了很多封装。
这个聊天项目一开始采用的是Spring封装的Redis,使用过程中发现,经常会报一些类型转换异常的问题,后来发现这种错误经常在网络连接异常或是长轮询的地方出现。深入底层发现是由于Redis底层的Socket连接使用了缓冲区,导致有些时候如果前面的连接出现问题,后面的请求再使用前面的连接会获取到前面连接的数据,导致类型转换异常的出现,
尤其是长轮询不断请求Redis服务器的时候,问题出现的频率尤其高,而且一旦出现问题,后面的连接都会出问题,这也许是由于Redis连接池中出问题的连接没有完全释放的原因。
因此建议大家如果使用Redis做缓存,最好使用原生的Jedis,这样可以在连接异常时自己实现异常连接的处理,而使用Spring集成的Redis由于连接已经被封装了,限制了自己对异常连接的处理。
下面就将两种集成Redis的方法分别介绍一下。
第一种是使用Spring集成的Redis。
1、首先是Jar包,需要jedis包、Spring集成Redis的包spring-data-redis、Redis连接池需要的commons-pool2包,其中spring-data-redis包和jedis包的版本需要适配,否则会报错,具体要求网上可以查到,这3个包的下载可以从Maven仓库获取。
<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-pool2 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.4.2</version>
</dependency>
<!-- spring-data-redis2.0.7版本下载失败 -->
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
<version>1.8.4.RELEASE</version>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
2、Spring集成Redis的Xml配置
2.1、Redis.properties配置(相当于jdbc.properties)
#============================#
#==== Redis settings ====#
#============================#
#redis 服务器 IP
redis.host=192.168.1.222
#192.168.1.222
#格式:redis://:[密码]@[服务器地址]:[端口]/[db index]
redis.uri = redis://:12345@127.0.0.1:6379/0
#redis 服务器端口
redis.port=6379
#redis 密码
redis.pass=
#redis#2018
#超时时间
redis.timeOut=2000
#redis 支持16个数据库(相当于不同用户)可以使不同的应用程序数据彼此分开同时又存储在相同的实例上
redis.dbIndex=0
#redis 缓存数据过期时间单位秒
redis.expiration=3000
#连接池中最大连接数。高版本:maxTotal,低版本:maxActive
#控制一个 pool 可分配多少个jedis实例
redis.maxActive=6
#连接池中最少空闲的连接数
redis.minIdle=1
#连接池中最大空闲的连接数,控制一个 pool 最多有多少个状态为 idle 的jedis实例
redis.maxIdle=300
#当连接池资源耗尽时,调用者最大阻塞的时间,超时将抛出异常。单位,毫秒数;默认为-1.表示永不超时。高版本:maxWaitMillis,低版本:maxWait
#当borrow一个jedis实例时,最大的等待时间,如果超过等待时间,则直接抛出JedisConnectionException;
redis.maxWait=1000
#连接空闲的最小时间,达到此值后空闲连接将可能会被移除。负值(-1)表示不移除
redis.minEvictableIdleTimeMillis=60000
#对于“空闲链接”检测线程而言,每次检测的链接资源的个数。默认为3
redis.numTestsPerEvictionRun=3
#“空闲链接”检测线程,检测的周期,毫秒数。如果为负值,表示不运行“检测线程”。默认为-1
redis.timeBetweenEvictionRunsMillis=30000
#testOnBorrow:向调用者输出“链接”资源时,是否检测是有有效,如果无效则从连接池中移除,并尝试获取继续获取。默认为false。建议保持默认值
#在borrow一个jedis实例时,是否提前进行alidate操作;如果为true,则得到的jedis实例均是可用的;
redis.testOnBorrow=true
#testOnReturn:向连接池“归还”链接时,是否检测“链接”对象的有效性。默认为false。建议保持默认值
#testWhileIdle:向调用者输出“链接”对象时,是否检测它的空闲超时;默认为false。如果“链接”空闲超时,将会被移除。建议保持默认值
#whenExhaustedAction:当“连接池”中active数量达到阀值时,即“链接”资源耗尽时,连接池需要采取的手段, 默认为1(0:抛出异常。1:阻塞,直到有可用链接资源。2:强制创建新的链接资源)
2.2、applicationContext.xml引入redis.properties文件
<!-- 这里配置数据库properties文件位置 -->
<!-- 使用PropertyPlaceholderConfigurer加载redis.properties文件失败改用PropertySourcesPlaceholderConfigurer -->
<bean id="propertyConfigurer" class="org.springframework.context.support.PropertySourcesPlaceholderConfigurer">
<property name="locations">
<list>
<value>classpath:jdbc.properties</value>
<value>classpath:system.properties</value>
<value>classpath:redis.properties</value>
</list>
</property>
</bean>
将Redis相关配置单独放在一个配置文件中:
<import resource="spring-redis-context.xml" />
spring-redis-context.xml中的配置包括,连接池的配置、连接工厂的配置以及RedisTemplate类的相关配置。
<?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"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<description>Spring容器管理redis连接和使用 </description>
<!--载入 redis 配置文件-->
<!-- Spring容器仅允许最多定义一个PropertyPlaceholderConfigurer 或 <content:property-placeholder>其余的会被Spring忽略 -->
<!-- <context:property-placeholder location="classpath:redis.properties" ignore-unresolvable="true"/> -->
<!-- 高版本Jedis没有maxActive变量,具体有哪些可以看JedisPoolConfig继承的类GenericObjectPoolConfig -->
<!-- 连接池配置 -->
<!-- 配置 JedisPoolConfig 实例 -->
<bean id="poolConfig" class="redis.clients.jedis.JedisPoolConfig">
<property name="maxIdle" value="${redis.maxIdle}"/>
<property name="maxTotal" value="${redis.maxActive}"/>
<property name="maxWaitMillis" value="${redis.maxWait}"/>
<property name="testOnBorrow" value="${redis.testOnBorrow}"/>
</bean>
<!-- <bean id="clusterRedisNodes1" class="org.springframework.data.redis.connection.RedisNode">
<constructor-arg value="${redis.host}" />
<constructor-arg value="${redis.port}" type="int"/>
</bean> -->
<!-- ERR This instance has cluster support disabled -->
<!-- <bean id="redisClusterConfiguration" class="org.springframework.data.redis.connection.RedisClusterConfiguration">
<property name="maxRedirects" value="3"></property>
节点配置
<property name="clusterNodes">
<set>
<bean class="org.springframework.data.redis.connection.RedisClusterNode">
<constructor-arg name="host" value="${redis.host}"/>
<constructor-arg name="port" value="${redis.port}"/>
</bean>
<bean class="org.springframework.data.redis.connection.RedisClusterNode">
<constructor-arg name="host" value="192.168.1.111"/>
<constructor-arg name="port" value="6380"/>
</bean>
</set>
</property>
</bean> -->
<!-- Spring提供的Redis连接工厂 -->
<!-- 配置JedisConnectionFactory -->
<bean id="jedisConnectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory" destroy-method="destroy">
<property name="hostName" value="${redis.host}"/>
<property name="port" value="${redis.port}"/>
<property name="password" value="${redis.pass}"/>
<property name="database" value="${redis.dbIndex}"/>
<property name="poolConfig" ref="poolConfig"/>
<!-- <constructor-arg ref="redisClusterConfiguration"></constructor-arg>
<constructor-arg ref="poolConfig"></constructor-arg> -->
<!-- <property name="clusterConfig" ref="redisClusterConfiguration"></property> -->
</bean>
<bean id="redisConfig" class="com.teriste.redis.util.RedisConfig"></bean>
<!-- Spring提供的访问Redis类 -->
<!-- 配置RedisTemplate -->
<!-- <bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate" scope="prototype">
<property name="connectionFactory" ref="jedisConnectionFactory"/>
下面是设置key和value序列化的方式,不同方式导致存入的字节序列不一样,什么样的序列化方式就要使用相应的反序列化方式查询,否则差不多redis存入的数据
由此也可以知道spring对redis序列化有很多种方式
<property name="keySerializer">
<bean class="org.springframework.data.redis.serializer.StringRedisSerializer" />
</property>
<property name="valueSerializer">
defaultSerializer是JdkSerializationRedisSerializer在调试时看不出来键是什么样的
<bean class="org.springframework.data.redis.serializer.JdkSerializationRedisSerializer" />
<bean class="org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer" />
fastjson序列化
<bean class="com.teriste.redis.util.FastJson2JsonRedisSerializer">
<constructor-arg name="clazz" value=Object.class></constructor-arg>
</bean>
</property>
<property name="hashKeySerializer">
<bean class="org.springframework.data.redis.serializer.StringRedisSerializer" />
</property>
配置true可以使用transactional控制事务,spring已经提供支持
<property name="enableTransactionSupport" value="true"/>
</bean> -->
<!-- 配置RedisCacheManager -->
<bean id="redisCacheManager" class="org.springframework.data.redis.cache.RedisCacheManager">
<constructor-arg name="redisOperations" ref="redisTemplate"/>
<property name="defaultExpiration" value="${redis.expiration}"/>
</bean>
<!-- 配置RedisCacheConfig -->
<!-- <bean id="redisCacheConfig" class="com.teriste.redis.util.RedisCacheConfig">
<constructor-arg ref="jedisConnectionFactory"/>
<constructor-arg ref="redisTemplate"/>
<constructor-arg ref="redisCacheManager"/>
</bean> -->
<!-- Redis sentinel集群配置 -->
<!-- <bean id="sentinelConfig" class="org.springframework.data.redis.connection.RedisSentinelConfiguration">
<constructor-arg index="0" type="java.lang.String" value="host6379" />
<constructor-arg index="1" type="java.util.Set">
<set>
<value>192.168.1.111:6380</value>
<value>192.168.1.111:6381</value>
</set>
</constructor-arg>
</bean> -->
<!-- Spring提供的Redis连接工厂 -->
<!-- <bean id="jedisConnectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory" destroy-method="destroy">
Redis sentinel集群配置
<constructor-arg index="0" type="org.springframework.data.redis.connection.RedisSentinelConfiguration" ref="sentinelConfig" />
连接池配置.
<constructor-arg index="1" type="redis.clients.jedis.JedisPoolConfig" ref="poolConfig" />
Redis服务主机.
<property name="hostName" value="192.168.1.111" />
Redis服务端口号.
<property name="port" value="26379" />
Redis服务连接密码.
<property name="password" value="${redis.pass}" />
连超时设置.
<property name="timeout" value="15000" />
是否使用连接池.
<pr