Bootstrap

Java基于redis实现进度条

一. 问题背景

为了提升用户体验,开发中有很多场景需要用到进度条,比如导入、导出、大规模更新操作等。进度条在许多大型系统中使用频率较高,反复编写既麻烦又不利于维护,因此基于Redis抽成公共方法供不同功能调用。

二. 实现方案

1.引入依赖

如果系统已集成Redis,直接跳到第5步,进度条实现。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

2.配置数据源

  # redis 配置
  redis:
    # 地址
    host: 127.0.0.1
    # 端口,默认为6379
    port: 6379
    # 数据库索引
    database: 0
    # 密码
    password: 123456
    # 连接超时时间
    timeout: 10s
    lettuce:
      pool:
        # 连接池中的最小空闲连接
        min-idle: 0
        # 连接池中的最大空闲连接
        max-idle: 8
        # 连接池的最大数据库连接数
        max-active: 8
        # #连接池最大阻塞等待时间(使用负值表示没有限制)
        max-wait: -1ms

3.配置类

package com.cms.framework.config;

import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;

/**
 * redis配置
 *
 * @author cms
 */
@Configuration
@EnableCaching
public class RedisConfig
{
    @Bean
    @SuppressWarnings(value = { "unchecked", "rawtypes" })
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory)
    {
        RedisTemplate<Object, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);

        FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);

        ObjectMapper mapper = new ObjectMapper();
        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
        serializer.setObjectMapper(mapper);

        // 使用StringRedisSerializer来序列化和反序列化redis的key值
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(serializer);

        // Hash的key也采用StringRedisSerializer的序列化方式
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(serializer);

        template.afterPropertiesSet();
        return template;
    }
}

4.Redis工具类

package com.cms.common.core.redis;

import java.util.Collection;
import java.util.concurrent.TimeUnit;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;

/**
 * spring redis 工具类
 *
 * @author daixin
 **/
@SuppressWarnings(value = { "unchecked", "rawtypes" })
@Component
public class RedisCache
{
    @Autowired
    public RedisTemplate redisTemplate;

    /**
     * 缓存基本的对象,Integer、String、实体类等
     *
     * @param key 缓存的键值
     * @param value 缓存的值
     */
    public <T> void setCacheObject(final String key, final T value)
    {
        redisTemplate.opsForValue().set(key, value);
    }

    /**
     * 缓存基本的对象,Integer、String、实体类等
     *
     * @param key 缓存的键值
     * @param value 缓存的值
     * @param timeout 时间
     * @param timeUnit 时间颗粒度
     */
    public <T> void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit)
    {
        redisTemplate.opsForValue().set(key, value, timeout, timeUnit);
    }

    /**
     * 设置有效时间
     *
     * @param key Redis键
     * @param timeout 超时时间
     * @return true=设置成功;false=设置失败
     */
    public boolean expire(final String key, final long timeout)
    {
        return expire(key, timeout, TimeUnit.SECONDS);
    }

    /**
     * 设置有效时间
     *
     * @param key Redis键
     * @param timeout 超时时间
     * @param unit 时间单位
     * @return true=设置成功;false=设置失败
     */
    public boolean expire(final String key, final long timeout, final TimeUnit unit)
    {
        return redisTemplate.expire(key, timeout, unit);
    }

    /**
     * 获得缓存的基本对象。
     *
     * @param key 缓存键值
     * @return 缓存键值对应的数据
     */
    public <T> T getCacheObject(final String key)
    {
        ValueOperations<String, T> operation = redisTemplate.opsForValue();
        return operation.get(key);
    }

    /**
     * 删除单个对象
     *
     * @param key
     */
    public boolean deleteObject(final String key)
    {
        return redisTemplate.delete(key);
    }

    /**
     * 删除集合对象
     *
     * @param collection 多个对象
     * @return
     */
    public long deleteObject(final Collection collection)
    {
        return redisTemplate.delete(collection);
    }
    
}

5.进度条接口

package com.cms.service.dataCenter;

import java.util.Map;

/**
 * @author daixin
 * @version 1.0
 * @description: TODO
 * @date 2024/7/26 11:43
 */
public interface IProcessService {

    /**
     * 初始化进度
     * @param totalKey
     * @param addingKey
     */
    void initProcess(String totalKey,String addingKey);

    /**
     * 加进度
     * @param addingKey
     * @param size
     */
    void addSize(String addingKey,int size);

    /**
     * 设置总进度
     * @param totalKey
     * @param size
     */
    void setTotalSize(String totalKey,int size);

    /**
     * 设置最终进度
     * @param addingKey
     * @param total
     */
    void setFinalSize(String addingKey,int total);

    /**
     * 获取进度
     * @param totalKey
     * @param addingKey
     * @return
     */
    Map<String, Object> getProcess(String totalKey, String addingKey);
}

6.进度条实现类

package com.cms.service.impl.dataCenter;

import com.cms.common.core.redis.RedisCache;
import com.cms.service.dataCenter.IProcessService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.text.NumberFormat;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;

/**
 * @author daixin
 * @version 1.0
 * @description: TODO
 * @date 2024/7/26 11:46
 */
@Service
public class ProcessServiceImpl implements IProcessService {

    @Autowired
    RedisCache redisCache;

    public void initProcess(String totalKey,String addingKey) {
        redisCache.setCacheObject(totalKey, 0, 30, TimeUnit.MINUTES);
        redisCache.setCacheObject(addingKey, 0, 30, TimeUnit.MINUTES);
    }
    public void addSize(String addingKey,int size) {
        int addingSize = redisCache.getCacheObject(addingKey);
        redisCache.setCacheObject(addingKey, addingSize + size, 30, TimeUnit.MINUTES);
    }
    public void setTotalSize(String totalKey,int size) {
        redisCache.setCacheObject(totalKey, size, 30, TimeUnit.MINUTES);
    }

    public void setFinalSize(String addingKey,int total){
        redisCache.setCacheObject(addingKey,total);
    }
    public Map<String, Object> getProcess(String totalKey, String addingKey) {
        int addingSize = redisCache.getCacheObject(addingKey) == null ? 0 : redisCache.getCacheObject(addingKey);
        int totalSize = redisCache.getCacheObject(totalKey) == null ? 0 : redisCache.getCacheObject(totalKey);

        double percent = 0.000;
        if (totalSize != 0) {
            percent = (double) addingSize / totalSize;
        }
        Map process = new HashMap<>();
        NumberFormat formatter = NumberFormat.getPercentInstance();
        formatter.setMaximumFractionDigits(2);
        process.put("percent", formatter.format(percent));
        return process;
    }
}

7.管理Redis Key

package com.cms.common.constant;

/**
 * @author daixin
 * @data 2024/1/25 17:06
 */
public class ProjectRedis {

    public static final String IMPORT_PROJECT_TOTLESIEZE_KEY = "import_project_totle";
    public static final String IMPORT_PROJECT_ADDSIZEING_KEY = "import_project_addingSize";

    public static final String EXPORT_PROJECT_TOTLESIEZE_KEY = "export_project_totle";
    public static final String EXPORT_PROJECT_ADDSIZEING_KEY = "export_project_addingSize";
}

8.进度条应用

    /**
     * 导出项目
     * @param projectIds 导出项目的id
     * @param processNum 进度条标识,本系统使用Long型时间戳
     */
    public void exportProject(Long [] projectIds,String processNum){
        final String totalKey = ProjectRedis.EXPORT_PROJECT_TOTLESIEZE_KEY + processNum;
        final String addingKey = ProjectRedis.EXPORT_PROJECT_ADDSIZEING_KEY + processNum;
        processService.initProcess(totalKey, addingKey);

        //计算总操作数,并存入redis,此处以1000为例
        int totle = 1000;
        processService.setTotalSize(totalKey,totle);
        for(Long projectId : projectIds){
            //此处省略具体导出逻辑...

            //每次循环当前操作数加1
            processService.addSize(addingKey,1);
        }
        //方法结束时,将当前操作数和总操作数记为相等
        processService.setFinalSize(addingKey,totle);
    }

9.项目导出和进度查询

前端先调用项目导出接口(/exportProject),传入进度条标识(processNum);

再循环调用获取进度接口(/exportProjectProcess),传入相同进度条标识(processNum),

即可获取当前进度进行展示。

    @SneakyThrows
    @GetMapping("/exportProject")
    public void exportProject(@RequestParam Long [] projectId,@RequestParam String processNum, HttpServletResponse response){
        response.setContentType("application/zip");
        response.setHeader("Content-Disposition", "attachment; filename=\"project.zip\"");
        String zipPath = cmsProjectService.exportProject(projectId,processNum);
        File zipFile = new File(zipPath); // 假设example.zip已经存在并且包含要下载的内容
        FileInputStream fis = new FileInputStream(zipFile);
        OutputStream os = response.getOutputStream();
        byte[] buffer = new byte[1024];
        int len;
        while ((len = fis.read(buffer)) != -1) {
            os.write(buffer, 0, len); // 将数据从输入流写入输出流
        }
        os.flush(); // 确保所有数据都被写入输出流
        os.close(); // 关闭输出流
        fis.close(); // 关闭输入流
    }
    
    @GetMapping("/exportProjectProcess")
    public AjaxResult exportProjectProcess(@RequestParam String processNum) {
        return AjaxResult.success(processService.getProcess(ProjectRedis.EXPORT_PROJECT_TOTLESIEZE_KEY + processNum,ProjectRedis.EXPORT_PROJECT_ADDSIZEING_KEY + processNum));
    }

10.不同功能应用进度条时,只需根据实际业务定义不同的Redis Key,按照上述逻辑调用方法,即可直接实现进度条复用。

;