• SpringBoot使用redis缓存List<Object>


    一、概述

      最近在做性能优化,之前有一个业务是这样实现的:

      1.温度报警后第三方通讯管理机直接把报警信息保存到数据库

      2.我们在数据库中添加触发器,(BEFORE INSERT)根据这条报警信息处理业务逻辑,在数据库中插入“其他业务数据”

      3.前端setTimeout每隔5秒ajax去后端查询“其他业务数据”(查库)

      优化后这样实现:

      两个微服务,消息中间件专门一个服务,接收消息存入数据库,存入redis;业务服务直接从redis获取

      1.MQTT订阅通讯管理机报警事件主题

      2.发生报警后,java中根据报警信息保存“其他业务数据”到数据库并放入redis缓存

      3.前端setTimeout每隔5秒ajax去后端查询“其他业务数据”(改为从redis中获取)

      4.下一步计划使用WebSocekt,去掉前端setTimeout

    二、SpringBoot配置redis

      pom.xml、application.properties、@EnableCaching等等这些配置就不列出来了,大家可以百度,提一下RedisTemplate的配置

      RedisTemplate<String, Object>可以直接存直接存List、Map等,使用jackson2JsonRedisSerializer序列化,

    package ;
    
    import java.lang.reflect.Method;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.cache.CacheManager;
    import org.springframework.cache.interceptor.KeyGenerator;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.data.redis.cache.RedisCacheManager;
    import org.springframework.data.redis.connection.RedisConnectionFactory;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
    import org.springframework.data.redis.serializer.StringRedisSerializer;
    import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
    
    import com.fasterxml.jackson.annotation.JsonAutoDetect;
    import com.fasterxml.jackson.annotation.JsonInclude.Include;
    import com.fasterxml.jackson.annotation.PropertyAccessor;
    import com.fasterxml.jackson.databind.DeserializationFeature;
    import com.fasterxml.jackson.databind.ObjectMapper;
    
    @Configuration
    public class RedisConfiguration {
    	@Bean("jsonRedisCache")
    	public CacheManager cacheManager(@Autowired RedisTemplate<String, Object> redisTemplate) {
    		return new RedisCacheManager(redisTemplate);
    	}
    	
        @Bean
        public KeyGenerator keyGenerator() {
            return new KeyGenerator() {
    			
    			@Override
    			public Object generate(Object target, Method method, Object... params) {
    				  StringBuilder sb = new StringBuilder();
    	                sb.append(target.getClass().getName());
    	                sb.append(method.getName());
    	                for (Object obj : params) {
    	                    sb.append(obj.toString());
    	                }
    	                return sb.toString();
    			}
    		};
        }
    
    	@Bean
    	public RedisTemplate<String, Object> redisTemplate(@Autowired RedisConnectionFactory cf) {
    		RedisTemplate<String, Object> redisTemplate = new RedisTemplate<String, Object>();
    		redisTemplate.setKeySerializer(new StringRedisSerializer());
    		redisTemplate.setHashKeySerializer(jackson2JsonRedisSerializer());
    		redisTemplate.setValueSerializer(jackson2JsonRedisSerializer());
    		redisTemplate.setConnectionFactory(cf);
    		redisTemplate.afterPropertiesSet();
    		return redisTemplate;
    	}
    
    	@SuppressWarnings({ "unchecked", "rawtypes" })
    	@Bean
    	public Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer() {
    		final Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(
    				Object.class);
    		final ObjectMapper objectMapper = Jackson2ObjectMapperBuilder.json().build();
    		objectMapper.disable(DeserializationFeature.FAIL_ON_IGNORED_PROPERTIES);
    		objectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
    		objectMapper.setSerializationInclusion(Include.NON_NULL);
    		objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
    		objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
    		jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
    		return jackson2JsonRedisSerializer;
    	}
    }  

    三、List对象存入redis遇到的问题

      1.@Cacheable不起作用问题

      刚开始,计划在service层方法上使用注解@Cacheable进行缓存,但是redis没有保存,最后百度得到答案:一个类中@Cacheable标注的方法不能被本类中其他方法调用,否则缓存不起作用

      修改类方法调用后此问题解决

      错误的方法调用:

      正确的调用:其他类调用该方法

      这其中有个报错:No cache could be resolved for 'Builder[public java.util.List com.es.service.evralarm.EvrAlarmCacheService.getEvrAlarmByAccountId(java.lang.String)] caches=[] | key=''EvrAlarm-'+#accountId' | keyGenerator='' | cacheManager='' | cacheResolver='' | condition='' | unless='' | sync='false'' using resolver 'org.springframework.cache.interceptor.SimpleCacheResolver@7fbfc31a'. At least one cache should be provided per cache operation.

      @Cacheable注解中添加cacheNames即可

    package ;
    
    import java.util.HashMap;
    import java.util.List;
    import java.util.Map;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.cache.annotation.Cacheable;
    import org.springframework.stereotype.Service;
    
    import com.es.entity.evralarm.EvrAlarm;
    import com.es.repository.evralarm.EvrAlarmDao;
    
    @Service
    public class EvrAlarmCacheService {
    
    	@Autowired
    	private EvrAlarmDao evrAlarmDao;
    	
    	@Cacheable(cacheNames="EvrAlarms",key="'EvrAlarm-'+#accountId")
    	public List<EvrAlarm> getEvrAlarmByAccountId(String accountId){
    		Map<String,Object> params = new HashMap<>();
    		params.put("accountId", accountId);
    		params.put("limit", 1);
    		List<EvrAlarm> evrAlarms = evrAlarmDao.selectEvrAlarmByAccount(params);
    		
    		return evrAlarms;
    	}
    }
    

      redis中存储的数据如下图:

      2.Could not resolve type id 'com.es.xx.evralarm.EvrAlarm' into a subtype of [simple type, class java.lang.Object]: no such class found

    at [Source: [B@29a6e242; line: 1, column: 60] (through reference chain: java.util.ArrayList[0])

      业务服务中原代码:

    @Cacheable(cacheNames="EvrAlarms",key="'EvrAlarm-'+#accountId")
    		public List<EvrAlarm> selectEvrAlarmByAccount(String accountId){
    			Map<String,Object> params = new HashMap<>();
    			params.put("accountId", accountId);
    			return evrAlarmDao.selectEvrAlarmByAccount(params);
    		}
    

      看到一遍文档后明白了,根本原因是:两个微服务,实体类内容虽然一样,但是类路径不一样

    四、使用StringRedisTemplate、RedisTemplate<String, Object>

      进一步分析发现使用@Cacheable有问题,消息中间件收到第二条报警消息,如果业务系统没有处理第一条报警消息(redis中未删除,同样的key redis中已有一条)则redis中的信息不会更新

      正确的操作应该是:消息中间件每次接收消息,处理后都往redis中更新

    1.使用RedisTemplate<String, Object>

      使用RedisTemplate<String, Object>存储

    package ;
    
    import java.util.HashMap;
    import java.util.List;
    import java.util.Map;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.data.redis.core.ListOperations;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.stereotype.Service;
    
    import com.es.entity.evralarm.EvrAlarm;
    import com.es.repository.evralarm.EvrAlarmDao;
    
    @Service
    public class EvrAlarmCacheService {
    
    	@Autowired
    	private EvrAlarmDao evrAlarmDao;
    	
    	@Autowired
    	private RedisTemplate<String, Object> redisTemplate;
    	
    	public List<EvrAlarm> getEvrAlarmByAccountId(String accountId){
    		Map<String,Object> params = new HashMap<>();
    		params.put("accountId", accountId);
    		params.put("limit", 1);
    		List<EvrAlarm> evrAlarms = evrAlarmDao.selectEvrAlarmByAccount(params);
    		
    		//redis缓存
    		ListOperations<String, Object> lo = redisTemplate.opsForList();
    		lo.rightPush("EvrAlarm-"+accountId, evrAlarms);
    		return evrAlarms;
    	}
    }
    

      

    存储后redis中的数据结构入下图:

      业务服务获取redis中的数据:

    /**
    		 * 根据账户ID查询最新告警信息
    		 * */
    		public List<EvrAlarm> selectEvrAlarmByAccount(String accountId){
    			//redis缓存中获取
    			ListOperations<String,Object> lo = redisTemplate.opsForList();
    			Object obj = lo.index("EvrAlarm-"+accountId, 0);
    			List<EvrAlarm> evrAlarms = (List<EvrAlarm>) obj;
    			return evrAlarms == null ? new ArrayList<>() : evrAlarms;
    		}
    

      报错了:

      报错信息很明了,“no such class found”,还是上面的问题,两个服务都有实体类“EvrAlarm”,内容相同,类路径不同

      把消息服务中“EvrAlarm”实体类类路径修改后,redis存储结构如下:

      

      业务服务获取redis中的数据:

    /**
    		 * 根据账户ID查询最新告警信息
    		 * */
    		public List<EvrAlarm> selectEvrAlarmByAccount(String accountId){
    			//redis缓存中获取
    			ListOperations<String,Object> lo = redisTemplate.opsForList();
    			List<EvrAlarm> evrAlarms = (List<EvrAlarm>) lo.index("EvrAlarm-"+accountId, 0);
    			return evrAlarms == null ? new ArrayList<>() : evrAlarms;
    		}
    

      没有报错能够正常获取:

      

    2.使用StringRedisTemplate 

      StringRedisTemplate不需要考虑存储对象的类路径问题,存储前直接把对象转成json字符串,获取的时候再把json转成对象

      使用Gson直接把要保存的List<>对象转成json再保存到redis

      中间件所在服务存入redis:

    package com.xx.service.evralarm;
    
    import java.util.HashMap;
    import java.util.List;
    import java.util.Map;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.data.redis.core.StringRedisTemplate;
    import org.springframework.data.redis.core.ValueOperations;
    import org.springframework.stereotype.Service;
    
    import com.es.entity.evralarm.EvrAlarm;
    import com.es.repository.evralarm.EvrAlarmDao;
    import com.google.gson.Gson;
    
    @Service
    public class EvrAlarmCacheService {
    
    	@Autowired
    	private EvrAlarmDao evrAlarmDao;
    	
    	@Autowired
    	private StringRedisTemplate redisTemplate;
    	
    	public List<EvrAlarm> getEvrAlarmByAccountId(String accountId){
    		Map<String,Object> params = new HashMap<>();
    		params.put("accountId", accountId);
    		params.put("limit", 1);
    		List<EvrAlarm> evrAlarms = evrAlarmDao.selectEvrAlarmByAccount(params);
    		
    		//redis缓存
    		ValueOperations<String,String> vo = redisTemplate.opsForValue();
    		Gson gson = new Gson();
    		vo.set("EvrAlarm-"+accountId, gson.toJson(evrAlarms));
    		return evrAlarms;
    	}
    }
    

      

      业务服务从redis中取:

      从redis中获取key对应的value,得到string类型的value,使用Gson转成List<>对象

    查询:
    /**
    		 * 根据账户ID查询最新告警信息
    		 * */
    		public List<EvrAlarm> selectEvrAlarmByAccount(String accountId){
    			//redis缓存中获取
    			ValueOperations<String,String> vo = redisTemplate.opsForValue();
    			String value = vo.get("EvrAlarm-"+accountId);
    			Gson gson = new Gson();
    			List<EvrAlarm> evrAlarms = gson.fromJson(value, List.class);
    			return evrAlarms == null ? new ArrayList<>() : evrAlarms;
    		}

       业务操作删除、同时删除redis:  

    public void deleteAccountEvralarm(String accountId, String evrAlarmId){
        	Map<String, Object> queryMap = new HashMap<>();
        	queryMap.put("accountId", accountId);
        	queryMap.put("evrAlarmId", evrAlarmId);
        	accountEvralarmDao.deleteByPrimaryKey(queryMap);
        	//redis删除缓存
        	redisTemplate.delete("EvrAlarm-"+accountId);
        }
    

      

    最后问题解决

    参考文档:

    http://www.mamicode.com/info-detail-2267905.html

    https://blog.csdn.net/ranweizheng/article/details/42267803

    https://yq.aliyun.com/ziliao/444278

    https://blog.csdn.net/hanchao5272/article/details/79051364

  • 相关阅读:
    Splay复习
    带权并查集复习-HDU3038
    罗素悖论-图灵停机问题
    数独解法c++实现
    状压DP NOI2001 炮兵阵地
    区间第k大数
    分块随笔T2
    分块感想
    webkit的高级属性
    设计模式
  • 原文地址:https://www.cnblogs.com/ph7seven/p/9845234.html
Copyright © 2020-2023  润新知