Redis常用小结
缓存一致性问题
所有操作都应该先操作DB,再操作Redis;
先更新DB,再删Redis; 只能减少不一致发生的概率;需要设置过期时间;
先添加DB,再添加Redis;
查询
先查Redis, 查不到,再查DB, 查不到就得防止缓存击穿, 查到就放入缓存, 查不到就创建一个对象放入缓存,防穿透
缓存并发
虽然使用缓存速度比DB快,但有些接口, 因为业务逻辑复杂, 不得不多次查询Redis, 像每次与Redis交互差不多需要50ms,如果不可避免的需要交互10次,甚至更多, 这样算下来,一个接口耗时都要1s或者0.5s了,大部分时间都花在了与 redis建立连接上,所以 可以使用rua脚本, 或者管道, 合并多个redis请求,一次性发送个redis,然后一次性执行多个redis命令。
也可以使用管道, 管道使用场景是:所有的键值都放在一个Redis库里,不适合用于集群。
@Override
public List<Strategy> getAllSdkGlobalStrategy(List<Integer> typeList) {
if (CollectionUtil.isEmpty(typeList)) {
return null;
}
List<Strategy> returnList = Lists.newArrayList();
//策略键集合
Set<byte[]> keySet = Sets.newHashSet();
typeList.forEach( type -> keySet.add(this.getGlobalStrategyKeyPrefix(type).getBytes()));
//批量获取redis键的值
List<Object> list = redisStringTemplate.executePipelined((RedisCallback<?>) connection -> {
keySet.forEach(connection::get);
return null;
});
//命中缓存,直接返回
if (this.checkCache(list)) {
list.forEach(item -> CollectionUtil.addAll(returnList, JSONUtil.toBean(item.toString(),Strategy.class)));
return returnList;
}
return this.getAllSdkGlobalStrategyCache(typeList);
}
模糊删除
大数量的情况下,是不允许使用模糊操作的,例如模糊查询,删除。因为模糊删除会先去查询整个Redis缓存中所有符合 的Key,然后再将符合的key全部删除。
他这个遍历Key的过程,相当于做了一次全表扫描。数据量多, 必定会出现卡顿。导致项目很多地方卡死,无法使用。
因此,一般项目上线后, 会禁用keys命令:
Set keys = redisTemplate.keys("*message:*");
Iterator<String> iterator = list.iterator();
List<MsgLike> msgLikes=new ArrayList<>(256);
while (iterator.hasNext()){
//遍历操作;
}
缓存穿透
有些项目中, 要求接口实现高并发,这就意味不能直接与DB进行交换, 一般情况下都是把数据放入缓存中, 缓存击穿是某些恶意请求获取某些不存在的阿数据,在redis中查不到数据, 就会去请求DB,DB也拿不到,如果大量的这种请求发送过来, 就会造成缓存穿透。
解决方法
- 在Redis中维护一份索引表, 获取数据前, 就查一遍索引表,如果不存在,就直接返回;
实现思路:
- 插入DB记录时,会返回一个记录ID, 然后拿到记录ID后, 就放入Redis中, 可以使用Set,或者 Map数据结构;再把记录的对象Entity,DTO放入Redis中,可以使用String, 也可以使用Map接口,具体看对象是否频繁写,如果频繁写操作, 更适合Map;
- 删除DB记录时, 需要先根据ID删除DB数据,再删除缓存数据, 再删除索引记录;
- 查询的时候, 传入一个记录ID, 先查索引表缓存, 存在, 就再去查询详情, 详情查不到就去DB查,查DB查到数据后,再重新放入缓存;
- 如果DB查询的数据为空, 就往redis中一个空数组new ArrayList()或者空对象new Oject(),空字符串"";
List数据结构的防缓存击穿
/**
* 查询List
*/
private List<SensitiveWordsDTO> getCacheList() {
return redisTemplate.opsForList().range(CacheKey.getSensitiveKey(), RedisConstants.RANGE_MIN, RedisConstants.RANGE_MAX);
}
@Override
public List<SensitiveWordsDTO> getList(BaseDTO baseDTO, StrategyEnum strategyEnum) {
//查询缓存
List<SensitiveWordsDTO> wordsCacheList = this.getCacheList();
//查询结果不为空, 则说明存在缓存
if (CollectionUtils.isNotEmpty(wordsCacheList)) {
//如果缓存的第一个元素为空,则表示没有数据,直接返回一个数组;
if (ObjectUtil.isEmpty(wordsCacheList.get(0))) {
return new LinkedList<>();
}
return wordsCacheList;
}
//如果缓存中查询不到, 查询DB数据
List<SensitiveWords> wordsList = this.list(Wrappers.<SensitiveWords>lambdaQuery());
List<SensitiveWordsDTO> sensitiveWordsDTOList = CollectionUtils.isEmpty(wordsList) ? null : BeanCopyUtils.copyList(wordsList, SensitiveWordsDTO::new);
//如果DB中也不存在
if (CollectionUtils.isEmpty(sensitiveWordsDTOList)) {
//创建一个数组集合, 放入一个空的对象
List<SensitiveWordsDTO> cacheList = new ArrayList<>();
cacheList.add(new SensitiveWordsDTO());
//再将集合放入Redis缓存中。
this.setCacheList(cacheList);
return new LinkedList<>();
}
//保存缓存
this.setCacheList(sensitiveWordsDTOList);
return sensitiveWordsDTOList;
}
Set数据结构的防穿透和List的思路差不多。
String结构防止缓存穿透:
@Override
public Boolean updateDeviceStatus(DeviceInfoUpdateDeviceStatus deviceStatus) {
//根据唯一性的ID去查redis
Device cacheDevice = this.getCache(deviceStatus.getDeviceId(), Device.class);
//查不到
if (cacheDevice == null) {
//查DB
this.updateDevice(deviceStatus);
return true;
}
...
}
private void updateDevice(DeviceInfoUpdateDeviceStatus deviceStatus) {
//1. 查询DB是
Device device = this.getOne(Wrappers.<Device>lambdaQuery().eq(Device::getUmid, deviceStatus.getDeviceId())
.eq(Device::getAccountId, deviceStatus.getAccountId()));
//2. 判断记录是否为空
if (device == null) {
//3. 记录为空,以ID为key,空属性对象为value, 放入缓存中。
this.setCache(deviceStatus.getDeviceId(), new Device());
}
}
异常处理小结
DB的唯一性异常捕捉
我们数据库表一般会有一些唯一键, 像身份证等 ;如果我们不做校验直接保存,必然会抛出唯一键已经存在的异常;
利用Spring的全局异常捕捉机制, 我们可以把唯一性异常为捕捉下来,然后返回给前端。
- 当自定义类加@RestControllerAdvice注解时,方法自动返回json数据,每个方法无需再添加@ResponseBody注解:
- SQLIntegrityConstraintViolationException:出现主键重复,或者唯一约束冲突后,会抛出的异常类。
@RestControllerAdvice
@Slf4j
public class DatabaseExceptionHandler {
/**
* 主键重复或者唯一约束的处理
*
* @param ex 异常对象
* @return 通用结果
*/
@ExceptionHandler({SQLException.class})
@ResponseStatus(HttpStatus.OK)
public Result<?> sqlIntegrityConstraintViolationException(SQLIntegrityConstraintViolationException ex) {
//遍历枚举,
for (UniqueEnum uniqueEnum : UniqueEnum.values()) {
//查看抛出的异常信息字符串是否包含了duplicate字符串,有的话就是我们对应的唯一约束。
// 再查看异常信息是否包含了相关的唯一索引名称, 如果出现了就给捕捉。返回给前端。
if (ex.getMessage().contains("Duplicate") && ex.getMessage().contains(uniqueEnum.getDesc())) {
return Result.error(uniqueEnum.getCode());
}
}
log.error("error", ex);
return Result.error();
}
}
public enum UniqueEnum {
/**
* 约束控制触发值
*/
TYPE_REPEAT1("设备表唯一索引", 400101, "idx_uk_account_umid"),
TYPE_REPEAT2("拦截信息表唯一索引", 400102, "idx_uk_value_type"),
TYPE_REPEAT3("策略表唯一索引", 400104, "idx_uk_name_type"),
TYPE_REPEAT4("范围表唯一索引", 400103, "idx_uk_code_type_strategy"),
TYPE_REPEAT5("策略元数据表唯一索引", 400105, "idx_uk_user_resource"),
;
/**
* 唯一约束名称
*/
private final String uniqueKey;
/**
* 错误码
*/
private final Integer code;
/**
* 值
*/
private final String desc;
}
全局公共异常拦截
public enum UtilMsgEnum implements IMsgCode {
INTERNAL_SERVER_ERROR(100500, "出现未知异常,请检查");
}
/**
* 全局异常拦截保存
*
* @author qinlei
* @date 2021-10-14
*/
@ControllerAdvice
@Slf4j
public class ErrorHandler {
/**
* 拦截的是Exception: 一般是未知异常,返回服务内部异常信息, 给前端
* 一般是不知道异常原因的。需要开发人去手动排错,例如空指针异常
*/
@ResponseBody
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public Result<?> error(Exception e) {
log.error("error", e);
return Result.error(UtilMsgEnum.INTERNAL_SERVER_ERROR.getCode());
}
/**
* 公共异常拦截, 一般项目中,我们会定制一个业务公共异常类, 这个异常抛出就是业务异常,
* 是知道异常原因的。
*/
@ResponseBody
@ExceptionHandler(CommonException.class)
@ResponseStatus(HttpStatus.OK)
public Result<?> common(CommonException e) {
return Result.error(e.getCode(), null, e.getMessage());
}
SQL语句学习
SELECT
CASE
WHEN
idevstatus = 1 THEN
''
WHEN idevstatus = 3 THEN
''
WHEN idevstatus =- 1 THEN
''
WHEN idevstatus =- 2 THEN
''
WHEN idevstatus =- 3 THEN
''
END strsectionname,
count( 1 ) strvalue
FROM
tbl_mobiledevbaseinfo
WHERE
idevstatus IN (- 1,- 2, 1, 3,- 3 )
GROUP BY
idevstatus;
strsectionname是怎么获取到值得呢?
这个sql执行的时候, 会又idevstatus, 但是where条件中, iderstatus的值已经限定为了 1,3, -1,-2,-3。
接着看case:
当idevstatus = 1的时候, stsectionname为’’, 同理, 3,-1,-2,-3也是一样, 所以最终结果, strsectionname的值就是为’’;
单元测试实例
@SpringBootTest(classes = EmmServerApplication.class)
@AutoConfigureMockMvc
public class SensitiveWordControllerTest {
@Autowired
MockMvc mvc;
@Autowired
SensitiveWordService sensitiveWordService;
private static final String TENANT_ID = "test";
public static String language = "en_US";
@Test
public void testList() throws Exception {
//查询请求, GET方式
MvcResult result = mvc.perform(
MockMvcRequestBuilders
.get("/emmServer/security/strategy/sensitiveWord/customRule/list")
.param("tenantId", TENANT_ID)
.header("i18n-language",language)
.contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE)
.accept(MediaType.APPLICATION_JSON))
.andExpect(MockMvcResultMatchers.status().isOk())
.andDo(MockMvcResultHandlers.print()).andReturn();
}
@Test
public void testDelete() throws Exception {
//删除数据, 使用POST方式
Map<String, String> map = new HashMap<>();
map.put("id", "1234567890");
String jsonString = JSON.toJSONString(map);
MvcResult result = mvc.perform(
MockMvcRequestBuilders
.post("/emmServer/security/strategy/sensitiveWord/customRule/delete")
.content(jsonString)
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON))
.andExpect(MockMvcResultMatchers.status().isOk())
.andDo(MockMvcResultHandlers.print()).andReturn();
}
}
ERROR 320004 — [io-32054-exec-2] c.a.e.t.d.feign.FeignTaskServiceImpl : 接口调用异常:java.lang.RuntimeException: com.netflix.client.ClientException: Load balancer does not have available server for client: emm-task-admin
这个问题说了 没有可用的服务emm-task-admin;
造成的原因可能是:
1) emm-task-admin 服务没有启动;
2) emm-task-admin 服务, 当前服务都启动了, 都注册到了注册中心, 由于没有在同一个命名空间,就拿不到了对应的服务,也会出现这个异常;