Redis常用小结
缓存一致性问题
所有操作都应该先操作DB,再操作Redis;
先更新DB,再删Redis; 只能减少不一致发生的概率;需要设置过期时间;
先添加DB,再添加Redis;
查询
先查Redis, 查不到,再查DB, 查不到就得防止缓存击穿, 查到就放入缓存, 查不到就创建一个对象放入缓存,防穿透
缓存并发
虽然使用缓存速度比DB快,但有些接口, 因为业务逻辑复杂, 不得不多次查询Redis, 像每次与Redis交互差不多需要50ms,如果不可避免的需要交互10次,甚至更多, 这样算下来,一个接口耗时都要1s或者0.5s了,大部分时间都花在了与 redis建立连接上,所以 可以使用rua脚本, 或者管道, 合并多个redis请求,一次性发送个redis,然后一次性执行多个redis命令。
也可以使用管道, 管道使用场景是:所有的键值都放在一个Redis库里,不适合用于集群。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23@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命令:
1
2
3
4
5
6
7
8Set 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数据结构的防缓存击穿
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36/** * 查询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结构防止缓存穿透:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24@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:出现主键重复,或者唯一约束冲突后,会抛出的异常类。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48@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; }
全局公共异常拦截
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34public 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语句学习
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22SELECT 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的值就是为’’;
单元测试实例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39@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 服务, 当前服务都启动了, 都注册到了注册中心, 由于没有在同一个命名空间,就拿不到了对应的服务,也会出现这个异常;
最后
以上就是怕黑萝莉最近收集整理的关于Redis使用 ,异常处理, 杂七杂八的小结Redis常用小结异常处理小结单元测试实例的全部内容,更多相关Redis使用内容请搜索靠谱客的其他文章。
发表评论 取消回复