整合 Redis & 非同步任務

2022-09-06 18:02:27

SpringBoot 2.7 .2實戰基礎 - 09 - 整合 Redis & 非同步任務

1 整合Redis

《docker 安裝 MySQL 和 Redis》一文已介紹如何在 Docker 中安裝 Redis,本文就看看 SpringBoot 如何整合 Redis。SpringBoot 提供了整合 Redis 的 starter,使用非常簡單。

1.1 新增依賴

在 pom.xml 中新增 redis 的 starter:

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

1.2 設定 Redis

修改 application.yml 檔案,新增 Redis 的設定:

spring:
  redis:
    host: 127.0.0.1
    port: 6379
    username:
    password:
    timeout: 5000
    jedis:
      pool:
        max-active: 3
        max-idle: 3
        min-idle: 1
        max-wait: -1

1.3 新增設定

com.yygnb.demo.config 中建立 RedisConfig,處理一些中文亂碼問題。

com.yygnb.demo.config.RedisConfig

@Configuration
public class RedisConfig {

    private final RedisTemplate redisTemplate;

    public RedisConfig(RedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    /**
     * 解決redis插入中文亂碼
     * @return
     */
    @Bean
    public RedisTemplate<Serializable, Object> redisTemplateInit() {
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        redisTemplate.setKeySerializer(stringRedisSerializer);
        redisTemplate.setHashKeySerializer(stringRedisSerializer);

        GenericJackson2JsonRedisSerializer genericJackson2JsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
        redisTemplate.setValueSerializer(genericJackson2JsonRedisSerializer);
        redisTemplate.setHashValueSerializer(genericJackson2JsonRedisSerializer);
        return redisTemplate;
    }

}

1.4 封裝 Redis 操作

可封裝一些 Redis 的常見操作。

com.yygnb.demo.utils.RedisUtils

@RequiredArgsConstructor
@Component
public class RedisUtils {

    private final RedisTemplate redisTemplate;

    /**
     * 指定快取失效時間
     * @param key
     * @param time 單位 秒
     */
    public void expire(Serializable key, long time) {
        if (time > 0) {
            redisTemplate.expire(key, time, TimeUnit.SECONDS);
        }
    }

    /**
     * 根據key 獲取過期時間
     * @param key 鍵 不能為null
     * @return 時間(秒) 返回0代表為永久有效
     */
    public long getExpire(String key) {
        return redisTemplate.getExpire(key, TimeUnit.SECONDS);
    }

    /**
     * 判斷key是否存在
     * @param key 鍵
     * @return true 存在 false不存在
     */
    public boolean hasKey(String key) {
        try {
            return redisTemplate.hasKey(key);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 刪除快取
     * @param key 可以傳一個值 或多個
     */
    @SuppressWarnings("unchecked")
    public void del(String... key) {
        if (key != null && key.length > 0) {
            if (key.length == 1) {
                redisTemplate.delete(key[0]);
            } else {
                redisTemplate.delete(CollectionUtils.arrayToList(key));
            }
        }
    }

    /**
     * 普通快取獲取
     * @param key 鍵
     * @return 值
     */
    public Object get(String key) {
        return key == null ? null : redisTemplate.opsForValue().get(key);
    }

    /**
     * 普通快取放入
     * @param key 鍵
     * @param value 值
     * @return true成功 false失敗
     */
    public boolean set(String key, Object value) {
        try {
            redisTemplate.opsForValue().set(key, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 普通快取放入並設定時間
     * @param key 鍵
     * @param value 值
     * @param time 時間(秒) time要大於0 如果time小於等於0 將設定無限期
     * @return true成功 false 失敗
     */
    public boolean set(String key, Serializable value, long time) {
        try {
            if (time > 0) {
                redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
            } else {
                set(key, value);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
}

操作太多,這裡簡單放了幾個常見的方法。

1. 5 測試

新建 controller 測試 Redis 的操作。

com.yygnb.demo.controller.RedisDemoController

@Tag(name = "Redis測試介面")
@RequiredArgsConstructor
@RestController
@RequestMapping("/redis")
public class RedisDemoController {

    private final RedisUtils redisUtils;

    @Operation(summary = "存值")
    @PostMapping()
    public void save(@RequestBody Map<String, Object> map) {
        Set<String> keys = map.keySet();
        for (String key : keys) {
            redisUtils.set(key, map.get(key));
        }
    }

    @Operation(summary = "取值")
    @GetMapping()
    public Object get(String key) {
        return redisUtils.get(key);
    }
}

2 非同步任務

非同步任務在後端開發中很常見,如生成報表,前端呼叫後端一個介面,如果資料量較大,後端生成報表會非常耗時,這時候如果是同步任務,等後端報表已經生成後,估計已經請求超時了。通常情況下,後端觸發一個非同步任務,成功觸發任務後,後端就返回前端,無需等待報表生成成功。類似場景如同時給使用者傳送郵件和簡訊。

2.1 準備工作

(1) 兩個 Service

分別編寫模擬傳送郵件和簡訊的 Service,這裡只是演示使用,故 Service 省略了介面定義,直接編寫實現類:

com.yygnb.demo.service.impl.EmailService

@Slf4j
@Service
public class EmailService {

    public void sendEmail(String msg) {
        log.info("開始傳送郵件: {}", msg);
        int i = new Random().nextInt(5);
        try {
            Thread.sleep(i * 1000);
            log.info("郵件傳送成功");
        } catch (InterruptedException e) {
            log.error("郵件傳送失敗");
            e.printStackTrace();
        }
    }
}

傳送簡訊的 Service 與郵件 service 類似。

com.yygnb.demo.service.impl.SmsService

@Slf4j
@Service
public class SmsService {

    public void sendSms(String msg) {
        log.info("開始傳送簡訊: {}", msg);
        int i = new Random().nextInt(5);
        try {
            Thread.sleep(i * 1000);
            log.info("簡訊傳送成功");
        } catch (InterruptedException e) {
            log.error("簡訊傳送失敗");
            e.printStackTrace();
        }
    }
}

(2) 介面開發

建立 DemoService,在 DemoService 中呼叫上面兩個 Service:

@Slf4j
@RequiredArgsConstructor
@Service
public class DemoServiceImpl implements DemoService {

    private final EmailService emailService;
    private final SmsService smsService;

    @Override
    public void send(String msg) {
        log.info("發別傳送簡訊和郵件");
        smsService.sendSms(msg);
        emailService.sendEmail(msg);
        log.info("Demo Service 結束");
    }
}

在 DemoController 中新增介面:

@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("demo")
public class DemoController {

    private final DemoService demoService;

    @GetMapping("async")
    public void asyncDemo(String msg) {
        demoService.send(msg);
    }
}

(3) 執行

請求該介面:

http://localhost:9099/demo/async?msg=hello

由於現在是同步執行,需要簡訊和郵件兩個service都執行完後才會返回結果。而且輸出的紀錄檔順序是固定的:

2.2 非同步任務

接下來進行非同步任務的改造。

(1) @EnableAsync

首先在啟動類上新增註解 @EnableAsync 開啟非同步任務。

@EnableAsync
@MapperScan("com.yygnb.demo.mapper")
@SpringBootApplication
public class DemoApplication {
  ...
}

(2)@Async

在需要非同步執行的方法上新增註解 @Async。在 sendSmssendEmail 兩個方法上新增該註解:

...
    @Async
    public void sendEmail(String msg) {
...
    }
...

(3)注意事項

呼叫非同步任務的方法與非同步任務的方法,不能在同一個 Service 中,即上面的 demo中,send 方法與 sendSms 不能在同一個 Service中。

2.3 自定義執行緒池

非同步任務本質上是在子執行緒中執行的。可以自定義執行緒池。

(1)定義設定的實體類

建立執行緒池設定的實體類:

com.yygnb.demo.config.ThreadPoolInfo

@Data
@Component
@ConfigurationProperties(prefix = "thread-pool")
public class ThreadPoolInfo {

    private int corePoolSize = 1;

    private int maxPoolSize = Integer.MAX_VALUE;

    private int keepAliveSeconds = 60;

    private int queueCapacity = Integer.MAX_VALUE;

    private String threadNamePrefix = "thread-";
}

(2)設定類

com.yygnb.demo.config.AsyncConfig

@RequiredArgsConstructor
@Configuration
public class AsyncConfig {

    private final ThreadPoolInfo info;

    @Bean("asyncExecutor")
    public Executor asyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(info.getCorePoolSize());
        executor.setMaxPoolSize(info.getMaxPoolSize());
        executor.setQueueCapacity(info.getQueueCapacity());
        executor.setThreadNamePrefix(info.getThreadNamePrefix());
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        executor.initialize();
        return executor;
    }
}

(3)設定 YML

在 application.yml 中設定執行緒池:

thread-pool:
  core-pool-size: 3
  max-pool-size: 5
  thread-name-prefix: yyg-async-

重啟服務,存取上面的介面,紀錄檔如下:

感謝你花費寶貴的時間閱讀本文,如果本文給了你一點點幫助或者啟發,還請三連支援一下,點贊、關注、收藏,作者會持續與大家分享更多幹貨