大家在用springboot集成redis保存数据时,除了会保存一些基础类型的数据,也一定会保存一些对象数据,例如登录用户的对象数据。redis本身也支持对象的获取与保存,只需要修改默认的序列化方式即可。
而在开发过程中为了方便大家应该也会封装redis的工具类,以便方便redis的使用,我这里就简单的写一个工具类,一个是保存数据,2个读取数据,分别是字符串读取和对象读取,
序列化则用Jackson2JsonRedisSerializer。
工具类代码如下
@Component
public class RedisUtils {
@Autowired
RedisTemplate<String, Object> redisTemplate;
/**
* 写入有时效性的缓存
*
* @param key
* @param object
* @param timeout
* 时效,秒
* @return
*/
public boolean set(final String key, final Object object, final Long timeout) {
boolean b = false;
try {
this.redisTemplate.opsForValue().set(key, object);
if (timeout != null) {
this.redisTemplate.expire(key, timeout, TimeUnit.SECONDS);
}
b = true;
} catch (final Exception e) {
e.printStackTrace();
}
return b;
}
/**
* 读取缓存
*
* @param key
* @return
*/
public String get(final String key) {
this.redisTemplate.setValueSerializer(new StringRedisSerializer());
Object result = this.redisTemplate.opsForValue().get(key);
if (result == null) {
return null;
}
return result.toString();
}
/**
* 读取实体对象的缓存
*
* @param key
* @param clazz
* @return
*/
@SuppressWarnings("unchecked")
public <T extends Object> T getObject(final String key, final Class<T> clazz) {
this.redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<T>(clazz));
return (T) this.redisTemplate.opsForValue().get(key);
}
}
我们先做个简单的测试
写入一个字符串和一个用户对象
@SpringBootTest(classes = Application.class)
@RunWith(SpringRunner.class)
class ApplicationTests {
@Autowired
NsRedisUtils redisUtils;
@Test
public void testSet() {
String strKey = "userStr";
this.redisUtils.set(strKey, "半路凉亭");
CurrentLoginUser user = new CurrentLoginUser();
user.setId("1");
user.setUsername("admin");
user.setRealname("半路凉亭");
String userKey = "userObj";
this.redisUtils.set(userKey, user);
}
}
运行后Redis里会保存2个数据,内容如下图
接下来测试获取redis里的数据
@Test
public void testget() {
String strKey = "userStr";
System.out.println("userStr *** " + this.redisUtils.get(strKey));
String userKey = "userObj";
CurrentLoginUser user = this.redisUtils.getObject(userKey, CurrentLoginUser.class);
System.out.println("userObj ***" + user);
}
运行结果如下图
从控制中可以看到数据被正常获取到了,没有问题,这是因为只做了一次的场景,如果你的系统用户很少,可能也不会出现异常,但如果你的系统访问并发量很大,就可能出现异常情况,
接下来我们用 apache-jmeter 软件来模拟高并发下从redis中同时获取字符串和对象数据,线程数分别设置了10,30,50。
设置为10的时候,没出现异常,设置为30的时候,偶尔会出现异常,设置为50的时候,肯定会出现异常,异常如下:
java.lang.ClassCastException: java.lang.String cannot be cast to com.normstar.framework.entity.CurrentLoginUser
出现这个原因就是因为在大并发下同时获取stirng和对象类型的数据,会出现线程A的获取对象的时候,线程B正好在获取字符串,而A还没获取完成,B已经在获取完成,这个时候,redis的序列化成字符串,A在转换对象时就会导致上列错误。
解决上面问题的方法有两种。
一、在工具类中获取数据的2个方法上加关键字 “synchronized”
二、redis保存时统一用string方式,这样保存对象时,先用阿里的JSON工具类将对象转换成字符串保存 : JSON.toJSONString(value);然后再取出对象的时候在用JSON.parseObject(jsonString, cls)转换即可。
以上是在大并发获取缓存数据时出现的异常,同样在大并发下同时写入不同类型的缓存数据时也一样会出现此问题,例如在大并发下登录时要同时保存token和登录用户对象到redis中。
个人建议使用第二种方式,如果用第一种方式,那么在保存的方法前面同样也要加关键字 “synchronized”