redis

Spring Boot EP 17:使用Redis記憶體資料庫實現快取機制

為什麼需要使用快取機制

官方網站:https://redis.io/

資料庫存放了應用系統必須要的資料,當資料越來越多、存取需求越來越大,而資料又放在相對效率較低的磁碟上,資料庫就會顯得疲態,無法滿足高I/O的需求。使用Redis作為快取,最大的好處在於速度,使用記憶體作為資料存放的載體,可想而知,速度就是比較快,再者,不是永遠都要把資料回寫至資料庫,當減少對資料庫的存取次數,也對整個系統環境有正向的影響。

從下圖來看,應用系統會先讀取Redis內的快取資料,若發現沒有想要的資料,就直接去資料庫撈取,除了應用系統使用外,也將這些資料回寫至Redis,當下次還需要的時候,就有這些資料,不需要去資料庫讀取。

實作

本篇將以讀取一段時間內的個股收盤資料為例,其流程如下圖:

步驟1:在pom.xml加入spring-boot-starter-data-redis-reactive套件。

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

步驟2:於application.properties增加連接Redis伺服器的設定。

# Redis資料庫索引(預設為0)
spring.redis.database=0
# Redis伺服器地址
spring.redis.host=localhost
# Redis伺服器連接端口
spring.redis.port=6379
# Redis伺服器連接密碼(預設為空)
spring.redis.password=
# 連接池最大連接數(使用負值表示沒有限制)
spring.redis.jedis.pool.max-active=8
# 連接池最大阻塞等待時間(使用負值表示沒有限制)
spring.redis.jedis.pool.max-wait=-1
# 連接池中的最大空閒連接
spring.redis.jedis.pool.max-idle=8
# 連接池中的最小空閒連接
spring.redis.jedis.pool.min-idle=0
# 連接超時時間(毫秒)
spring.redis.timeout=1000

步驟3:在”Repositories.StockRepository.java”加入查詢股價的SQL語法。

// 從資料庫查詢收盤價,條件為股票代碼及日期
@Query(value = "SELECT CLOSE_PRICE FROM STOCK_MARKET.STOCKS_PRICE WHERE STOCK_ID=:STOCK_ID AND DATE_OF_TRADING=:DATE_OF_TRADING", nativeQuery = true)
public double getStockClosePrice(@Param("STOCK_ID") String STOCK_ID, @Param("DATE_OF_TRADING") String DATE_OF_TRADING);

步驟4:在”Controllers.StockController.java”這個控制器中加入查詢資料庫的函數與Restful API方法。

package stockmarket.jovepater.com.stockmarket.Controllers;

import java.io.Serializable;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

import org.apache.commons.lang3.time.DateFormatUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

import stockmarket.jovepater.com.stockmarket.Classes.RspBody;
import stockmarket.jovepater.com.stockmarket.Repositories.StockRepository;

@RestController
public class StockController implements Serializable {
    @Autowired
    StockRepository stockRepository;

    @GetMapping("stocks")
    public RspBody getAllStocks() {
        return new RspBody("0000", "Success", stockRepository.findAllDetails());
    }

    @Autowired
    // 注入StringRedisTemplate類別,用來操作Redis
    StringRedisTemplate stringRedisTemplate;

    // 定義為Get API,並在URI帶入股票代碼、日期
    @GetMapping("stock/price/{id}/{date}")
    // 定義客戶端傳入股票代碼、日期
    public RspBody getStockPrice(@PathVariable("id") String strId, @PathVariable("date") String strDate)
            throws ParseException {

        // 宣告一個存放收盤價的變數,預設為0
        double doubleStockClosePrice = 0;
        // 將傳入的日期(yyyyMMdd)字串轉型為Date
        Date dateDate = new SimpleDateFormat("yyyyMMdd").parse(strDate);
        // 從Redis查詢收盤價,並將獲得的資料存放於objStockClosePrice
        Object objStockClosePrice = stringRedisTemplate.opsForHash().get(strId, strDate);
        // 若objStockClosePrice不為null,表示已經從Redis取得收盤價資料
        if (objStockClosePrice != null) {
            // 將收盤價原本的object型態轉為double
            doubleStockClosePrice = Double.valueOf(objStockClosePrice.toString());
        } else {
            // 若objStockClosePrice為null,表示沒有從Redis取得收盤價
            // 改從資料庫查詢收盤價
            doubleStockClosePrice = getStockPriceByDate(strId, dateDate);
            // 將從資料庫查詢到的收盤價回寫至資料庫
            stringRedisTemplate.opsForHash().putIfAbsent(strId, strDate, String.valueOf(doubleStockClosePrice));
        }

        if (doubleStockClosePrice > 0) {
            return new RspBody("0000", "Success", doubleStockClosePrice);
        } else {
            SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
            return new RspBody("0009", "Success", "The " + simpleDateFormat.format(dateDate)
                    + " close price of " + strId + " is not exist.");
        }
    }

    // 查詢資料庫中的收盤價
    public double getStockPriceByDate(String StockId, Date date) {

        // 將TradingDate轉為特定格式的字串
        String strTradingDate = DateFormatUtils.format(date, "yyyy-MM-dd");

        // 從資料庫讀取收盤價資料
        double stockPrice = 0;
        try {
            // 將回傳的收盤資料放入變數中
            stockPrice = stockRepository.getStockClosePrice(StockId, strTradingDate);
        } catch (Exception ex) {
            // 若資料庫中沒有收盤價,則回傳0
            stockPrice = 0;
        }

        return stockPrice;
    }
}

步驟5:透過API查詢股價。

步驟6:第一次查詢,Redis內沒有資料,必須從資料庫查詢,耗費22ms。

2022-02-25 22:40:33.163 INFO  [http-nio-8080-exec-3] stockmarket.jovepater.com.stockmarket.Controllers.StockController: Time used: 22 ms

步驟7:第二次查詢,Redis內已經有資料了,耗費7ms。

2022-02-25 22:42:38.768 INFO  [http-nio-8080-exec-5] stockmarket.jovepater.com.stockmarket.Controllers.StockController: Time used: 7 ms

結語

從上述可以發現,查詢效能比資料庫快了3倍 (7ms VS 22ms),當SQL查詢更複雜、資料量更大、TPS更高的情況下,這個差距會非常驚人,也就更能顯現出這樣的架構所帶來的好處了。

~ END ~


,

Related posts

Latest posts