需求
在线考试的一个交卷功能(商城的超时未支付订单也类似,不同之处在于商城的处理比较简单,失效就可以了,考试功能还需要在key失效后取回value做业务处理),用户整个答题流程中产生的数据存在redis中,当用户点交卷时,从redis中取出数据,做后续的评分、统计、存数据库等处理。如果用户未答完就非正常退出,要在到达考试结束时间时清除掉redis里面的数据,并执行后续的评分等操作。
处理方式
方式一:(本文采用的方式)
redis中对每次考试存两份数据(key1根据考试时长设定失效时间,key2永久有效),且value相同,当数据修改时,两份数据都做修改。编写一个监听器,当监听到key1失效时,从key2中取回数据做业务处理,然后删除key2。
方式二:
数据存入redis的同时启动一个定时任务,到时间后通过定时任务做业务处理及删除key。
方式三:(这种方式比较耗性能,不建议使用)
做一个频繁执行的定时任务,轮询检测是否考试超时。
环境及代码
一、代码地址:https://gitee.com/chrisfzh/dailytest
二、windows安装、配置、启动redis
1、下载地址:https://github.com/microsoftarchive/redis/releases
2、解压后,修改配置文件
设置redis密码
配置监听超时事件
在redis目录打开cmd命令行窗口,输入以下命令指定配置文件启动redis服务
redis-server.exe redis.wondows.conf
出现如下界面则启动成功
三、代码
1、项目目录结构
2、具体代码
如需具体代码请到码云拉去,已在第一步代码地址中注明,本文中仅介绍关键代码
存入redis业务代码
package com.chrisf.business.service.impl;
import com.chrisf.business.service.TestService;
import com.chrisf.config.SufConfig;
import com.chrisf.redis.RedisCache;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
@Service
public class TestServiceImpl implements TestService {
@Autowired
private RedisCache redisCache;
@Autowired
private SufConfig sufConfig;
@Autowired
private StringRedisTemplate redisTemplate;
@Override
public String test01() {
//redis中的key
String key = "key";
//value即为具体的业务数据
String value = "value";
//配置文件中配置的key2相对于key1的后缀
String rediskey = sufConfig.getRediskey();
//将key:value存入redis并设置失效时间为10秒
redisCache.setCacheObject(key, value, 10, TimeUnit.SECONDS);
//key1拼接后缀后成为key2,不设置失效时间,且value与key1的相同
redisCache.setCacheObject(key + rediskey, value);
return "ok";
}
}
监听器
package com.chrisf.redis;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
@Configuration
public class RedisListenerConfig {
@Bean
RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
return container;
}
}
package com.chrisf.redis;
import com.chrisf.config.RedisConfig;
import com.chrisf.config.SufConfig;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.listener.KeyExpirationEventMessageListener;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.stereotype.Component;
@Component
public class RedisKeyExpirationListener extends KeyExpirationEventMessageListener {
@Autowired
private SufConfig sufConfig;
@Autowired
private RedisCache redisCache;
public RedisKeyExpirationListener(RedisMessageListenerContainer listenerContainer) {
super(listenerContainer);
}
/**
* 针对redis数据失效事件,进行数据处理
* @param message
* @param pattern
*/
@Override
public void onMessage(Message message, byte[] pattern) {
// 失效key
String expiredKey = message.toString();
System.out.println("失效的key: " + expiredKey);
//失效key拼接配置文件中的后缀后获取value
String key = expiredKey + sufConfig.getRediskey();
String value = (String) redisCache.getCacheObject(key);
//TODO 拿到value去处理业务逻辑
//调用删除方法删除key2
redisCache.deleteObject(key);
}
}
3、测试效果
触发之后,通过redis桌面工具查看是否添加了Key,并且验证失效时能否被监听到
4、踩到的坑
在测试该功能的时候,遇到key乱码问题,后来查找到原因,redis默认编码方式为ISO-8859-1,我的项目编码方式为utf-8,因此需要在序列化的时候做一些配置。
package com.chrisf.config;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* redis配置
*
*/
@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport
{
@Bean
@SuppressWarnings(value = { "unchecked", "rawtypes" })
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory)
{
RedisTemplate<Object, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);
ObjectMapper mapper = new ObjectMapper();
mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
serializer.setObjectMapper(mapper);
template.setValueSerializer(serializer);
// 使用StringRedisSerializer来序列化和反序列化redis的key值
template.setKeySerializer(new StringRedisSerializer());
template.afterPropertiesSet();
return template;
}
}
package com.chrisf.config;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.type.TypeFactory;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.SerializationException;
import com.alibaba.fastjson.parser.ParserConfig;
import org.springframework.util.Assert;
import java.nio.charset.Charset;
/**
* Redis使用FastJson序列化
*
*/
public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T>
{
@SuppressWarnings("unused")
private ObjectMapper objectMapper = new ObjectMapper();
public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
private Class<T> clazz;
static
{
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
}
public FastJson2JsonRedisSerializer(Class<T> clazz)
{
super();
this.clazz = clazz;
}
@Override
public byte[] serialize(T t) throws SerializationException
{
if (t == null)
{
return new byte[0];
}
return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);
}
@Override
public T deserialize(byte[] bytes) throws SerializationException
{
if (bytes == null || bytes.length <= 0)
{
return null;
}
String str = new String(bytes, DEFAULT_CHARSET);
return JSON.parseObject(str, clazz);
}
public void setObjectMapper(ObjectMapper objectMapper)
{
Assert.notNull(objectMapper, "'objectMapper' must not be null");
this.objectMapper = objectMapper;
}
protected JavaType getJavaType(Class<?> clazz)
{
return TypeFactory.defaultInstance().constructType(clazz);
}
}