实习生对项目Redis使用的拟优化

实习生对项目Redis使用的拟优化


说在前

来到公司熟悉完项目框架之后,大概一周左右就上手开发了。

在这个过程中,有遇到几个接口响应时间有点点慢的问题,就想着看看能不能从Redis拿数据或者拿一部分,应该会快很多。结果的确快了很多,但是当我看到Redis中存的数据时,感觉可以有改进,就有了这篇文章,思考一下怎么改造改造它。

Redis原结构

首先我们来想想Redis应该存一些什么东西?以我开发ERP两月半的经验【狗头】,就我们的系统我觉得可以存

  • 最先能想到热点数据,这不用说,任何系统高频被查询的数据放Redis是最合适的,大大缓解数据库压力
  • 用户信息(会话、token、部门、角色、权限)
  • 数据字典表(一般是一个key和多个value)
  • 系统配置数据
  • 一些比较固定的数据且经常用的,改动频率很低的数据
  • 复杂、耗性能的计算结果

我那几个响应时间比较慢的接口,查的都是热点表,不仅数据量大,字段还多。

我看了一下项目Redis中的数据,猜到了这部分数据肯定是缓存了的,于是翻了一下同事是怎么存的:

@Slf4j
@Component
public class DataYncRedisConfig{
    @Autowired
    private IProjectService iProjectService;
    @Autowired
    private IClienteleService iClienteleService;
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @PostConstruct
    public void startScheduledTask() {
        log.info("初始化业务模块redis数据...");
        //项目
        List<Project> projectList = iProjectService.getAllProject();//这一步是获取所有project
        sysInitialization(projectList, RedisConstants.PROJECT_DATA_KEY);//调用方法存到redis
        //客商
        List<Clientele> clienteleList = iClienteleService.getAllClientele();//获取所有客商信息
        sysInitialization(clienteleList, RedisConstants.CLIENTELE_DATA_KEY);//存redis
        log.info("初始化业务模块redis数据(成功)");
    }
    /**
     * 更新表的数据到redis
     */
    public void sysInitialization(Object sysDataList,String DATA_KEY){
        // 将 SysUser 列表转换为 JSON 格式的字符串
        ObjectMapper objectMapper = new ObjectMapper();
        try {
            // 将 sysUserList 转换为 JSON 字符串
            String sysDataListJson = objectMapper.writeValueAsString(sysDataList);
            stringRedisTemplate.delete(DATA_KEY);
            // 存储到 Redis 中
            stringRedisTemplate.opsForValue().set(DATA_KEY, sysDataListJson);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

@PostConstruct 注解的方法在 Spring 容器初始化 Bean 的最后一个阶段 执行,所以容器初始化完,Redis就有数据了。很直接,这里直接selectAll,把所有数据灌进redis,用的是字符串类型。

那数据库和Redis是怎么同步的呢?

@Log4j2
@Component
@Aspect
public class BusinessRedisAspect {
    @Resource
    private IProjectService iProjectService;
    @Resource
    private IClienteleService iClienteleService;
    @Resource
    private DataYncRedisConfig dataYncRedisConfig;
    
    // 切入点(项目)
    @Pointcut("execution(* com.xxx.business.project.*.service.IProjectService.add*(..)) || " +
        "execution(* com.xxx.business.project.*.service.IProjectService.delete*(..)) || " +
        "execution(* com.xxx.business.project.*.service.IProjectService.update*(..)) ||"+
        "execution(* com.xxx.business.project.*.service.IProjectService.insert*(..)) ")
    public void projectRepositoryMethods() {}
    
    
    /**
     * 切面更新redis数据
     */
    @After("projectRepositoryMethods()")
    public void afterUserRepositoryMethod() {
        List<Project> projectList=iProjectService.getAllProject();
        log.info("表project数据变动,更新redis");
        dataYncRedisConfig.sysInitialization(projectList, RedisConstants.PROJECT_DATA_KEY);
    }
    
    
    // 切入点(客商)
    @Pointcut("execution(* com.xxx.business.clientele.*.service.IClienteleService.add*(..)) || " +
        "execution(* com.xxx.business.clientele.*.service.IClienteleService.delete*(..)) || " +
        "execution(* com.xxx.business.clientele.*.service.IClienteleService.update*(..)) ||"+
        "execution(* com.xxx.business.clientele.*.service.IClienteleService.insert*(..)) ")
    public void clienteleRepositoryMethods() {}
    
    
    
    /**
     * 切面更新redis数据
     *
     */
    @After("clienteleRepositoryMethods()")
    public void afterPostRepositoryMethod() {
        List<Clientele> clienteleList=iClienteleService.getAllClientele();
        log.info("表lientele数据变动,更新redis");
        dataYncRedisConfig.sysInitialization(clienteleList, RedisConstants.CLIENTELE_DATA_KEY);
    }

}

可以看到,这里用了AOP,切点是Service下的add,delete,update和insert,这里有add和insert两个新增只是为了解决开发小伙伴命名习惯的不同,策略是数据库动了就去主动更新Redis,而且是全量更新,相当于再灌一次数据到Redis。

用字符串类型存数据的话,好像只能用全量更新。

且更重要的是每次用到缓存中的数据,只能把所有数据拉下来(代码如下),再反序列化,做过滤。

@Slf4j
@Service
public class SysProjectDataServiceImpl implements SysProjectDataService {
        @Autowired
        private StringRedisTemplate stringRedisTemplate;

        /**
         * 通过redis获取业务模块全部项目的List
         */

        public List<Project> getUserListFromRedis() {
            String PROJECT_DATA_KEY = RedisConstants.PROJECT_DATA_KEY;

            // 从 Redis 中获取 JSON 字符串
            String sysProjectListJson = stringRedisTemplate.opsForValue().get(PROJECT_DATA_KEY);
            if (sysProjectListJson == null) {
                log.info("没有数据");
                return Collections.emptyList(); // 返回空列表
            }

            // 使用 ObjectMapper 反序列化 JSON 字符串为 List<SysProject>
            ObjectMapper objectMapper = new ObjectMapper();
            try {
                List<Project> sysProjectList = objectMapper.readValue(sysProjectListJson, new TypeReference<List<Project>>() {});
                return sysProjectList;
            } catch (Exception e) {
                log.error("反序列化失败,错误信息:{}", e.getMessage());
                e.printStackTrace();
                return Collections.emptyList(); // 如果反序列化失败,返回空列表
            }
        }


}

在开发过程中,有一天Redis变得很慢(我觉得不是突然变慢的,可能是越来越慢),然后领导换了一个服务器性能好一点的Redis,就没啥事了。

我去看了一下Redis中某些Key的大小

cz:0>MEMORY USAGE sys_user_data_key:
"458816"

cz:0>MEMORY USAGE project_data_key:
"49224"

cz:0>MEMORY USAGE sys_unit_data_key:
"28736"

cz:0>MEMORY USAGE clientele_data_key:
"28744"
cz:0>MEMORY USAGE sys_post_data_key:
"98368"

这里有一些代码里没有出现的数据(sys_user_data_key,sys_unit_data_key等),是来自别的服务,都是同一个项目的数据,也是相同的存储方式和更新策略。

我查了下多大算BigKey?怎么说的都有,应该是由机器的性能和网络条件来决定。

我不太清楚原来那台服务器的参数规格,这个数据大小可能对它来说算BigKey了?

改进思路

我们开发的系统初期还没有那么多的用户和很大的并发量,用户量起来了的话,初始化、更新都一股脑灌进Redis的做法肯定是不可取的,搞崩Redis只是时间问题,还会浪费大量的流量。

每次取数据也要加载所有数据,这对JVM也是一个挑战…

现在我们可以只把字符串类型换成hash类型,初始化可以照旧,缓存所有数据,但更新和查询必须要拿着key去操作。

粗略来说,Hash类型要到5000个元素或者占用50M才算BigKey,比字符串类型的阈值要大得多,所以暂时还不用担心Key太大的问题。

@Slf4j
@Component
public class DataYncRedisConfig {
    @Autowired
    private IProjectService iProjectService;
    @Autowired
    private IClienteleService iClienteleService;
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    
    private final ObjectMapper objectMapper = new ObjectMapper();

    @PostConstruct
    public void startScheduledTask() {
        log.info("初始化业务模块redis数据...");
        //项目
        List<Project> projectList = iProjectService.getAllProject();
        sysInitialization(projectList, RedisConstants.PROJECT_DATA_KEY_PREFIX);
        //客商
        List<Clientele> clienteleList = iClienteleService.getAllClientele();
        sysInitialization(clienteleList, RedisConstants.CLIENTELE_DATA_KEY_PREFIX);
        log.info("初始化业务模块redis数据(成功)");
    }

    /**
     * 更新表的数据到redis,使用Hash类型存储
     */
    public <T> void sysInitialization(List<T> dataList, String keyPrefix) {
        if (CollectionUtils.isEmpty(dataList)) {
            return;
        }
        
        //删除旧的缓存数据
        Set<String> keys = stringRedisTemplate.keys(keyPrefix + "*");//(*表示匹配全部)
        if (keys != null && !keys.isEmpty()) {
            stringRedisTemplate.delete(keys);
        }

        //批量存储数据到Hash
        dataList.forEach(data -> updateSingleData(data, keyPrefix));
    }

    
    /**
     * 更新单条数据(增量更新)
     */
    public <T> void updateSingleData(T data, String keyPrefix) {
        try {
            String id = getIdFromObject(data);
            if (id != null) {
                String hashKey = keyPrefix + id;
                Map<String, String> valueMap = objectMapper.convertValue(data, new TypeReference<Map<String, String>>() {});
                stringRedisTemplate.opsForHash().putAll(hashKey, valueMap);
            }
        } catch (Exception e) {
            log.error("更新Redis数据失败,keyPrefix: {}, data: {}", keyPrefix, data, e);
        }
    }

    /**
     * 删除单条数据
     */
    public void deleteSingleData(Long id, String keyPrefix) {
        try {
            String hashKey = keyPrefix + id;
            stringRedisTemplate.delete(hashKey);
        } catch (Exception e) {
            log.error("从Redis删除数据失败,key: {}", keyPrefix + id, e);
        }
    }
    
    
    /**
     * 从对象中获取ID值
     */
    private <T> String getIdFromObject(T data) {
        try {
            if (data instanceof Project) {
                return String.valueOf(((Project) data).getId());
            } else if (data instanceof Clientele) {
                return String.valueOf(((Clientele) data).getId());
            }
            //通用方法,通过反射获取ID
            Method getIdMethod = data.getClass().getMethod("getId");
            Object idValue = getIdMethod.invoke(data);
            return idValue != null ? idValue.toString() : null;
        } catch (Exception e) {
            //可以作其他处理
            log.error("获取对象ID失败", e);
            return null;
        }
    }
}

配置类的改动主要把数据存为Hash类型,key是前缀+id,前缀在RedisConstants定义。

还有增加了按Id更新、删除的逻辑,供切面类新增、更新和删除调用。

用于同步的切面类:

@Log4j2
@Component
@Aspect
public class BusinessRedisAspect {
    @Resource
    private DataYncRedisConfig dataYncRedisConfig;
    
    //项目切入点(捕获增改方法)
    @Pointcut("execution(* com.xxx.business.project.*.service.IProjectService.add*(..)) || " +
            "execution(* com.xxx.business.project.*.service.IProjectService.insert*(..)) || " +
            "execution(* com.xxx.business.project.*.service.IProjectService.update*(..))")
    public void projectAddOrUpdateMethods() {}
    
    //删除方法
    @Pointcut("execution(* com.xxx.business.project.*.service.IProjectService.delete*(..))")
    public void projectDeleteMethods() {}
    
    
    //客商切入点(捕获增改方法)
    @Pointcut("execution(* com.xxx.business.clientele.*.service.IClienteleService.add*(..)) || " +
            "execution(* com.xxx.business.clientele.*.service.IClienteleService.insert*(..)) || " +
            "execution(* com.xxx.business.clientele.*.service.IClienteleService.update*(..))")
    public void clienteleAddOrUpdateMethods() {}
    
    //删除方法
    @Pointcut("execution(* com.xxx.business.clientele.*.service.IClienteleService.delete*(..))")
    public void clienteleDeleteMethods() {}
    
    
    /**
     * 处理项目新增或更新
     */
    @AfterReturning(pointcut = "projectAddOrUpdateMethods()", returning = "result")
    public void afterProjectAddOrUpdate(JoinPoint joinPoint, Object result) {
        if (result instanceof Project) {
            Project project = (Project) result;
            log.info("项目数据变更,增量更新Redis: {}", project.getId());
            //调用Redis新增或更新方法
            dataYncRedisConfig.updateSingleData(project, RedisConstants.PROJECT_DATA_KEY_PREFIX);
        }
    }
    
    /**
     * 处理项目删除
     */
    @AfterReturning("projectDeleteMethods()")
    public void afterProjectDelete(JoinPoint joinPoint) {
        Object[] args = joinPoint.getArgs();
        if (args != null && args.length > 0 && args[0] instanceof Long) {
            Long id = (Long) args[0];
            log.info("项目数据删除,从Redis移除: {}", id);
            //调用Redis删除方法
            dataYncRedisConfig.deleteSingleData(id, RedisConstants.PROJECT_DATA_KEY_PREFIX);
        }
    }
    
    /**
     * 处理客商新增或更新
     */
    @AfterReturning(pointcut = "clienteleAddOrUpdateMethods()", returning = "result")
    public void afterClienteleAddOrUpdate(JoinPoint joinPoint, Object result) {
        if (result instanceof Clientele) {
            Clientele clientele = (Clientele) result;
            log.info("客商数据变更,增量更新Redis: {}", clientele.getId());
            dataYncRedisConfig.updateSingleData(clientele, RedisConstants.CLIENTELE_DATA_KEY_PREFIX);
        }
    }
    
    /**
     * 处理客商删除
     */
    @AfterReturning("clienteleDeleteMethods()")
    public void afterClienteleDelete(JoinPoint joinPoint) {
        Object[] args = joinPoint.getArgs();
        if (args != null && args.length > 0 && args[0] instanceof Long) {
            Long id = (Long) args[0];
            log.info("客商数据删除,从Redis移除: {}", id);
            dataYncRedisConfig.deleteSingleData(id, RedisConstants.CLIENTELE_DATA_KEY_PREFIX);
        }
    }
}

切面类把改动(add,insert,update)和删除(delete)分开处理。

这样一套下来就可以避免全量更新啦。

如果熟悉AOP反射的同学,这样的改动还是比较好懂的。

取数据也不用拉取全部了,可以指哪打哪了~

//获取单个项目
Map<Object, Object> projectMap = stringRedisTemplate.opsForHash().entries("project:data:1");

//获取所有项目的key
Set<String> projectKeys = stringRedisTemplate.keys("project:data:*");

//获取所有项目
List<Map<Object, Object>> allProjects = projectKeys.stream()
    .map(key -> stringRedisTemplate.opsForHash().entries(key))
    .collect(Collectors.toList());
//批量获取......

以上操作全部都没有指定过期时间,还有可以优化的就是初始化的时候,看有没有办法先更新一些热点中的热点,其他数据用到了再放到缓存,这样就算用户量数据量起来了也不会给Redis太大的压力,然后缓存的也是热点中的热点,可以在数据库多加一列表示热度,或者引入一些别的组件(ES/Mongo)记录剩余的热点数据…(狠狠yy)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值