Redis使用 ,异常处理, 杂七杂八的小结

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也拿不到,如果大量的这种请求发送过来, 就会造成缓存穿透。

解决方法

  1. 在Redis中维护一份索引表, 获取数据前, 就查一遍索引表,如果不存在,就直接返回;
    实现思路:
  • 插入DB记录时,会返回一个记录ID, 然后拿到记录ID后, 就放入Redis中, 可以使用Set,或者 Map数据结构;再把记录的对象Entity,DTO放入Redis中,可以使用String, 也可以使用Map接口,具体看对象是否频繁写,如果频繁写操作, 更适合Map;
  • 删除DB记录时, 需要先根据ID删除DB数据,再删除缓存数据, 再删除索引记录;
  • 查询的时候, 传入一个记录ID, 先查索引表缓存, 存在, 就再去查询详情, 详情查不到就去DB查,查DB查到数据后,再重新放入缓存;
  1. 如果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 服务, 当前服务都启动了, 都注册到了注册中心, 由于没有在同一个命名空间,就拿不到了对应的服务,也会出现这个异常;

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值