为什么使用Redis加速
上一章里,我们对token的每次验证都是需要查询数据库的,这就很容易导致数据库压力上升,前后端分离的情况下,接口调用的次数会比未分离状态下会更多,另外就是数据库的访问速度也是相对较慢的,使得接口调用速度下降,影响用户体验。
原结构
改造后
key键生成策略
Redis对于数据结构的选择还是很重要的,选择一个合适的数据结构能大大提高Redis的并发量,从而提升系统的性能。
假设我们现在系统有100万的总用户量,如果我们直接采用String类型进行存储,这就意味着每条用户信息都会生成一个key,那样我们的Redis中就保存有100万个key,这个数量还是比较庞大的,容易出现慢查询的情况。我们出于优化Redis查询可以使用Hash类型进行存储,Redis中Hash的数据结构是这样的---(key,field,value)。比较重要的是key的生成策略,key生成采用Token生成时间进行分组,比如某个用户的Token是2018-05-20 11:22:56这个时间段生成的,那这个用户的信息将会归于user:info:2011这个键中。也就是---- user:info:+日期天数+小时。而已field字段则是存储用户的Id,value则存储的是用户的详细信息。 用user:info:+日期天数+小时做为key还有一个原因是因为缓存过期问题,我们不可能一直把无用的缓存留到Redis中,但Hash数据结构并不能让某个key里面的field过期,所以综合下采取这种方式
开始改造
配置Redis
添加保存Redis key的标识符的线程变量(ThreadLocal)
修改过滤器
修改UserService方法
导入jar
pom.xml
org.springframework.boot
spring-boot-starter-data-redis
配置Redis
以下两个类均位于com.viu.technology.config.redis包下,自行创建.
由于使用的是FastJson序列化方法,所以我们需要创建一个FastJsonRedisSerializer类,用于Redis序列化存储
public class FastJsonRedisSerializer implements RedisSerializer {
public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
private Class clazz;
public FastJsonRedisSerializer(Class 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);
//不加配置无法自动转换为对应类型
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
return JSON.parseObject(str, clazz);
}
}
RedisConfiguration.java
该类配置了Redis连接工厂,以及配置FastJson序列化方式,使用@EnableCaching注解开启Redis缓存
@Configuration
@EnableCaching
@EnableTransactionManagement
public class RedisConfiguration extends CachingConfigurerSupport {
///使用fastjson序列化
@Bean
public RedisSerializer fastJson2JsonRedisSerializer() {
return new FastJsonRedisSerializer(Object.class);
}
@Bean
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory factory, RedisSerializer fastJson2JsonRedisSerializer) {
StringRedisTemplate template = new StringRedisTemplate(factory);
template.setValueSerializer(fastJson2JsonRedisSerializer);
///开启事务支持
template.setEnableTransactionSupport(true);
template.afterPropertiesSet();
return template;
}
///替换Redis Cache方案使用JDK序列化方式为FastJson序列化
@Bean
@Primary
public RedisCacheConfiguration redisCacheConfiguration(){
FastJsonRedisSerializer fastJsonRedisSerializer = new FastJsonRedisSerializer<>(Object.class);
RedisCacheConfiguration configuration = RedisCacheConfiguration.defaultCacheConfig();
configuration = configuration.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(fastJsonRedisSerializer));
configuration.entryTtl(Duration.ofDays(30));
return configuration;
}
///配置Redis连接工厂
@Bean
public LettuceConnectionFactory masterSource() {
return new LettuceConnectionFactory(new RedisStandaloneConfiguration("127.0.0.1", 6379));
}
}
创建TokenContextHolderToken线程信息保存类
TokenContextHolderToken.java
位于com.viu.technology.context包下,自行创建
public class TokenContextHolder {
private static final ThreadLocal cachePrefix = new ThreadLocal<>();
public static String getCachePrefix() {
return cachePrefix.get();
}
public static void setCachePrefix(String prefix) {
cachePrefix.set(prefix);
}
}
创建RedisKeyEnum枚举类
RedisKeyEnum.java
稍微规范化一下,创建了一个枚举类用来获取指定Redis缓存的key前缀
public enum RedisKeyEnum {
REDIS_USER_KEY("user:info:");
private String key;
RedisKeyEnum(String key) {
this.key = key;
}
public String getKey() {
return key;
}
}
修改Token验证过滤器
JwtAuthenticationTokenFilter.java
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
String authHeader = request.getHeader("Authorization");
String tokenHead = "tech-";
if (authHeader != null && authHeader.startsWith(tokenHead)) {
String authToken = authHeader.substring(tokenHead.length());
String userId = JwtTokenUtil.getUsernameFromToken(authToken);
if (userId != null && SecurityContextHolder.getContext().getAuthentication() == null) {
///使用JwtTokenUtil工具类获取Claims 对象,该对象保存着Token里的信息
Claims claims = JwtTokenUtil.getClaimsFromToken(authToken);
//获取Token的创建时间
Date createDate = claims.getExpiration();
//通过日期天数和小时数拼接字符串
String pre = "" +createDate.getDate() +createDate.getHours();
//放到TokenContextHolder线程变量中,交由UserService方法取出使用
TokenContextHolder.setCachePrefix(pre);
UserDetails userDetails = this.userDetailsService.loadUserByUsername(userId);
if (JwtTokenUtil.validateToken(authToken, userDetails)) {
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
} else {
log.info("没有获取到token");
}
chain.doFilter(request, response);
}
修改UserService
UserServiceImpl.java
User getUserAndRoleById(String id);
User insertUserInfoToCache(String userId, User user);
UserServiceImpl.java
insertUserInfoToCache()方法用于将用户信息缓存至Redis中
@Override
public User insertUserInfoToCache(String suffix,User user) {
JSONObject userJson = JsonUtil.objectToJsonObject(user);
String redisKey = RedisKeyEnum.REDIS_USER_KEY.getKey()+ suffix;
//redisTemplate.opsForHash().put(redisKey, user.getId(), JsonUtil.objectToString(user));
if (redisTemplate.hasKey(redisKey)) {
redisTemplate.opsForHash().put(redisKey, user.getId(), JsonUtil.objectToString(user));
} else {
redisTemplate.opsForHash().put(redisKey, user.getId(), JsonUtil.objectToString(user));
//设置Key的过期时间为7天
redisTemplate.expire(redisKey, 7, TimeUnit.DAYS);
}
return user;
}
@Override
public User getUserAndRoleById(String id) {
String suf = TokenContextHolder.getCachePrefix();
String redisKey = RedisKeyEnum.REDIS_USER_KEY.getKey() + suf;
User user = null;
//从Redis中获取对应信息,如果获取失败则查询数据库并将结果缓存到Redis中
String userJson = (String) redisTemplate.opsForHash().get(redisKey, id);
if (null!=userJson) {
user = JSONObject.parseObject(userJson, User.class);
} else {
user = userDao.selUserAndRoleById(id);
if (null != user) {
userService.insertUserInfoToCache(suf, user);
}
}
return user;
}
测试
同样调用我们上章写的接口,获取用户自身的详细信息/user/self/info
温馨提醒:
application.yml配置文件中加入logging.level.com.viu.technology.mapper: debug
这句代码意思是com.viu.technology.mapper包下输出debug级别的日志,数据库SQL查询则属于debug级别
在第一次调用的时候我们可以看到控制台输出了SQL查询语句,证明数据是从MySQL中读取的;接下来我们调用第二次,发现控制台并没有输出SQL语句,证明这次的数据是从Redis中获取的