一、概述
最近在做性能優化,之前有一個業務是這樣實現的:
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