Redis詳解 - SpringBoot整合Redis,RedisTemplate和註解兩種方式的使用

solocoder發表於2018-11-16

本文主要講 Redis 的使用,如何與 SpringBoot 專案整合,如何使用註解方式和 RedisTemplate 方式實現快取。最後會給一個用 Redis 實現分散式鎖,用在秒殺系統中的案例。

更多 Redis 的實際運用場景請關注開源專案 coderiver

專案地址:github.com/cachecats/c…

一、NoSQL 概述

什麼是 NoSQL ?

NoSQL(NoSQL = Not Only SQL ),意即“不僅僅是SQL”,泛指非關係型的資料庫。

為什麼需要 NoSQL ?

隨著網際網路web2.0網站的興起,傳統的關聯式資料庫在應付web2.0網站,特別是超大規模和高併發的SNS型別的web2.0純動態網站已經顯得力不從心,暴露了很多難以克服的問題,而非關係型的資料庫則由於其本身的特點得到了非常迅速的發展。NoSQL資料庫的產生就是為了解決大規模資料集合多重資料種類帶來的挑戰,尤其是大資料應用難題。 -- 百度百科

NoSQL 資料庫的四大分類

  • 鍵值(key-value)儲存
  • 列儲存
  • 文件資料庫
  • 圖形資料庫
分類 相關產品 典型應用 資料模型 優點 缺點
鍵值(key-value) Tokyo、 Cabinet/Tyrant、Redis、Voldemort、Berkeley DB 內容快取,主要用於處理大量資料的高訪問負載 一系列鍵值對 快速查詢 儲存的資料缺少結構化
列儲存資料庫 Cassandra, HBase, Riak 分散式的檔案系統 以列簇式儲存,將同一列資料存在一起 查詢速度快,可擴充套件性強,更容易進行分散式擴充套件 功能相對侷限
文件資料庫 CouchDB, MongoDB Web應用(與Key-Value類似,value是結構化的) 一系列鍵值對 資料結構要求不嚴格 查詢效能不高,而且缺乏統一的查詢語法
圖形(Graph)資料庫 Neo4J, InfoGrid, Infinite Graph 社交網路,推薦系統等。專注於構建關係圖譜 圖結構 利用圖結構相關演算法 需要對整個圖做計算才能得出結果,不容易做分散式叢集方案

NoSQL 的特點

  • 易擴充套件
  • 靈活的資料模型
  • 大資料量,高效能
  • 高可用

二、Redis 概述

Redis的應用場景

  • 快取
  • 任務佇列
  • 網站訪問統計
  • 應用排行榜
  • 資料過期處理
  • 分散式叢集架構中的 session 分離

Redis 安裝

網上有很多 Redis 的安裝教程,這裡就不多說了,只說下 Docker 的安裝方法:

Docker 安裝執行 Redis

docker run -d -p 6379:6379 redis:4.0.8
複製程式碼

如果以後想啟動 Redis 服務,開啟命令列,輸入以下命令即可。

redis-server
複製程式碼

使用前先引入依賴

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
複製程式碼

三、註解方式使用 Redis 快取

使用快取有兩個前置步驟

  1. pom.xml 引入依賴

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    複製程式碼
  2. 在啟動類上加註解 @EnableCaching

    @SpringBootApplication
    @EnableCaching
    public class SellApplication {
    	public static void main(String[] args) {
    		SpringApplication.run(SellApplication.class, args);
    	}
    }
    複製程式碼

常用的註解有以下幾個

  • @Cacheable

    屬性如下圖

Redis詳解 - SpringBoot整合Redis,RedisTemplate和註解兩種方式的使用

用於查詢和新增快取,第一次查詢的時候返回該方法返回值,並向 Redis 伺服器儲存資料。

以後呼叫該方法先從 Redis 中查是否有資料,如果有直接返回 Redis 快取的資料,而不執行方法裡的程式碼。如果沒有則正常執行方法體中的程式碼。

value 或 cacheNames 屬性做鍵,key 屬性則可以看作為 value 的子鍵, 一個 value 可以有多個 key 組成不同值存在 Redis 伺服器。

驗證了下,value 和 cacheNames 的作用是一樣的,都是標識主鍵。兩個屬性不能同時定義,只能定義一個,否則會報錯。

condition 和 unless 是條件,後面會講用法。其他的幾個屬性不常用,其實我也不知道怎麼用…

  • @CachePut

    更新 Redis 中對應鍵的值。屬性和 @Cacheable 相同

  • @CacheEvict

    刪除 Redis 中對應鍵的值。

3.1 新增快取

在需要加快取的方法上新增註解 @Cacheable(cacheNames = "product", key = "123"),

cacheNameskey 都必須填,如果不填 key ,預設的 key 是當前的方法名,更新快取時會因為方法名不同而更新失敗。

如在訂單列表上加快取

	@RequestMapping(value = "/list", method = RequestMethod.GET)
    @Cacheable(cacheNames = "product", key = "123")
    public ResultVO list() {

        // 1.查詢所有上架商品
        List<ProductInfo> productInfoList = productInfoService.findUpAll();

        // 2.查詢類目(一次性查詢)
        //用 java8 的特性獲取到上架商品的所有型別
        List<Integer> categoryTypes = productInfoList.stream().map(e -> e.getCategoryType()).collect(Collectors.toList());
        List<ProductCategory> productCategoryList = categoryService.findByCategoryTypeIn(categoryTypes);

        List<ProductVO> productVOList = new ArrayList<>();
        //資料拼裝
        for (ProductCategory category : productCategoryList) {
            ProductVO productVO = new ProductVO();
            //屬性拷貝
            BeanUtils.copyProperties(category, productVO);
            //把型別匹配的商品新增進去
            List<ProductInfoVO> productInfoVOList = new ArrayList<>();
            for (ProductInfo productInfo : productInfoList) {
                if (productInfo.getCategoryType().equals(category.getCategoryType())) {
                    ProductInfoVO productInfoVO = new ProductInfoVO();
                    BeanUtils.copyProperties(productInfo, productInfoVO);
                    productInfoVOList.add(productInfoVO);
                }
            }
            productVO.setProductInfoVOList(productInfoVOList);
            productVOList.add(productVO);
        }

        return ResultVOUtils.success(productVOList);
    }
複製程式碼

可能會報如下錯誤

Redis詳解 - SpringBoot整合Redis,RedisTemplate和註解兩種方式的使用

物件未序列化。讓物件實現 Serializable 方法即可

@Data
public class ProductVO implements Serializable {
    
    private static final long serialVersionUID = 961235512220891746L;

    @JsonProperty("name")
    private String categoryName;

    @JsonProperty("type")
    private Integer categoryType;

    @JsonProperty("foods")
    private List<ProductInfoVO> productInfoVOList ;
}
複製程式碼

生成唯一的 id 在 IDEA 裡有一個外掛:GenerateSerialVersionUID 比較方便。

重啟專案訪問訂單列表,在 rdm 裡檢視 Redis 快取,有 product::123 說明快取成功。

Redis詳解 - SpringBoot整合Redis,RedisTemplate和註解兩種方式的使用

3.2 更新快取

在需要更新快取的方法上加註解: @CachePut(cacheNames = "prodcut", key = "123")

注意

  1. cacheNameskey 要跟 @Cacheable() 裡的一致,才會正確更新。

  2. @CachePut()@Cacheable() 註解的方法返回值要一致

3.3 刪除快取

在需要刪除快取的方法上加註解:@CacheEvict(cacheNames = "prodcut", key = "123"),執行完這個方法之後會將 Redis 中對應的記錄刪除。

3.4 其他常用功能

  1. cacheNames 也可以統一寫在類上面, @CacheConfig(cacheNames = "product") ,具體的方法上就不用寫啦。

    @CacheConfig(cacheNames = "product")
    public class BuyerOrderController {
        @PostMapping("/cancel")
    	@CachePut(key = "456")
        public ResultVO cancel(@RequestParam("openid") String openid,
                               @RequestParam("orderId") String orderId){
            buyerService.cancelOrder(openid, orderId);
            return ResultVOUtils.success();
        }
    }
    複製程式碼
  2. Key 也可以動態設定為方法的引數

    @GetMapping("/detail")
    @Cacheable(cacheNames = "prodcut", key = "#openid")
    public ResultVO<OrderDTO> detail(@RequestParam("openid") String openid,
                                 @RequestParam("orderId") String orderId){
        OrderDTO orderDTO = buyerService.findOrderOne(openid, orderId);
        return ResultVOUtils.success(orderDTO);
    }
    複製程式碼

    如果引數是個物件,也可以設定物件的某個屬性為 key。比如其中一個引數是 user 物件,key 可以寫成 key="#user.id"

  3. 快取還可以設定條件。

    設定當 openid 的長度大於3時才快取

    @GetMapping("/detail")
    @Cacheable(cacheNames = "prodcut", key = "#openid", condition = "#openid.length > 3")
    public ResultVO<OrderDTO> detail(@RequestParam("openid") String openid,
                                     @RequestParam("orderId") String orderId){
        OrderDTO orderDTO = buyerService.findOrderOne(openid, orderId);
        return ResultVOUtils.success(orderDTO);
    }
    複製程式碼

    還可以指定 unless 即條件不成立時快取。#result 代表返回值,意思是當返回碼不等於 0 時不快取,也就是等於 0 時才快取。

    
    @GetMapping("/detail")
    @Cacheable(cacheNames = "prodcut", key = "#openid", condition = "#openid.length > 3", unless = "#result.code != 0")
    public ResultVO<OrderDTO> detail(@RequestParam("openid") String openid,
                                     @RequestParam("orderId") String orderId){
        OrderDTO orderDTO = buyerService.findOrderOne(openid, orderId);
        return ResultVOUtils.success(orderDTO);
    }
    複製程式碼

四、RedisTemplate 使用 Redis 快取

與使用註解方式不同,註解方式可以零配置,只需引入依賴並在啟動類上加上 @EnableCaching 註解就可以使用;而使用 RedisTemplate 方式麻煩些,需要做一些配置。

4.1 Redis 配置

第一步還是引入依賴和在啟動類上加上 @EnableCaching 註解。

然後在 application.yml 檔案中配置 Redis

spring:
  redis:
    port: 6379
    database: 0
    host: 127.0.0.1
    password:
    jedis:
      pool:
        max-active: 8
        max-wait: -1ms
        max-idle: 8
        min-idle: 0
    timeout: 5000ms
複製程式碼

然後寫個 RedisConfig.java 配置類

package com.solo.coderiver.user.config;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
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.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;

import java.net.UnknownHostException;


@Configuration
public class RedisConfig {

    @Bean
    @ConditionalOnMissingBean(name = "redisTemplate")
    public RedisTemplate<String, Object> redisTemplate(
            RedisConnectionFactory redisConnectionFactory)
            throws UnknownHostException {

        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<Object>(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);

        RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
        template.setConnectionFactory(redisConnectionFactory);
        template.setKeySerializer(jackson2JsonRedisSerializer);
        template.setValueSerializer(jackson2JsonRedisSerializer);
        template.setHashKeySerializer(jackson2JsonRedisSerializer);
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        template.afterPropertiesSet();
        return template;
    }

    @Bean
    @ConditionalOnMissingBean(StringRedisTemplate.class)
    public StringRedisTemplate stringRedisTemplate(
            RedisConnectionFactory redisConnectionFactory)
            throws UnknownHostException {
        StringRedisTemplate template = new StringRedisTemplate();
        template.setConnectionFactory(redisConnectionFactory);
        return template;
    }
}
複製程式碼

Redis 的配置就完成了。

4.2 Redis 的資料結構型別

Redis 可以儲存鍵與5種不同資料結構型別之間的對映,這5種資料結構型別分別為String(字串)、List(列表)、Set(集合)、Hash(雜湊)和 Zset(有序集合)。

下面來對這5種資料結構型別作簡單的介紹:

結構型別 結構儲存的值 結構的讀寫能力
String 可以是字串、整數或者浮點數 對整個字串或者字串的其中一部分執行操作;物件和浮點數執行自增(increment)或者自減(decrement)
List 一個連結串列,連結串列上的每個節點都包含了一個字串 從連結串列的兩端推入或者彈出元素;根據偏移量對連結串列進行修剪(trim);讀取單個或者多個元素;根據值來查詢或者移除元素
Set 包含字串的無序收集器(unorderedcollection),並且被包含的每個字串都是獨一無二的、各不相同 新增、獲取、移除單個元素;檢查一個元素是否存在於某個集合中;計算交集、並集、差集;從集合裡賣弄隨機獲取元素
Hash 包含鍵值對的無序雜湊表 新增、獲取、移除單個鍵值對;獲取所有鍵值對
Zset 字串成員(member)與浮點數分值(score)之間的有序對映,元素的排列順序由分值的大小決定 新增、獲取、刪除單個元素;根據分值範圍(range)或者成員來獲取元素

4.3 StringRedisTemplate 與 RedisTemplate

RedisTemplate 對五種資料結構分別定義了操作

  • redisTemplate.opsForValue();

    操作字串

  • redisTemplate.opsForHash();

    操作hash

  • redisTemplate.opsForList();

    操作list

  • redisTemplate.opsForSet();

    操作set

  • redisTemplate.opsForZSet();

    操作有序set

如果操作字串的話,建議用 StringRedisTemplate

StringRedisTemplate 與 RedisTemplate 的區別

  1. StringRedisTemplate 繼承了 RedisTemplate。

  2. RedisTemplate 是一個泛型類,而 StringRedisTemplate 則不是。

  3. StringRedisTemplate 只能對 key=String,value=String 的鍵值對進行操作,RedisTemplate 可以對任何型別的 key-value 鍵值對操作。

  4. 他們各自序列化的方式不同,但最終都是得到了一個位元組陣列,殊途同歸,StringRedisTemplate 使用的是 StringRedisSerializer 類;RedisTemplate 使用的是 JdkSerializationRedisSerializer 類。反序列化,則是一個得到 String,一個得到 Object

  5. 兩者的資料是不共通的,StringRedisTemplate 只能管理 StringRedisTemplate 裡面的資料,RedisTemplate 只能管理 RedisTemplate中 的資料。

4.4 專案中使用

在需要使用 Redis 的地方,用 @Autowired 注入進來

@Autowired
RedisTemplate redisTemplate;
 
@Autowired
StringRedisTemplate stringRedisTemplate;
複製程式碼

由於專案中暫時僅用到了 StringRedisTemplate 與 RedisTemplate 的 Hash 結構,StringRedisTemplate 比較簡單就不貼程式碼了,下面僅對操作 Hash 進行舉例。

關於 RedisTemplate 的詳細用法,有一篇文章已經講的很細很好了,我覺得沒必要再去寫了。傳送門

用 RedisTemplate 操作 Hash

package com.solo.coderiver.user.service.impl;

import com.solo.coderiver.user.dataobject.UserLike;
import com.solo.coderiver.user.dto.LikedCountDTO;
import com.solo.coderiver.user.enums.LikedStatusEnum;
import com.solo.coderiver.user.service.LikedService;
import com.solo.coderiver.user.service.RedisService;
import com.solo.coderiver.user.utils.RedisKeyUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.Cursor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ScanOptions;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

@Service
@Slf4j
public class RedisServiceImpl implements RedisService {

    @Autowired
    RedisTemplate redisTemplate;

    @Autowired
    LikedService likedService;

    @Override
    public void saveLiked2Redis(String likedUserId, String likedPostId) {
        String key = RedisKeyUtils.getLikedKey(likedUserId, likedPostId);
        redisTemplate.opsForHash().put(RedisKeyUtils.MAP_KEY_USER_LIKED, key, LikedStatusEnum.LIKE.getCode());
    }

    @Override
    public void unlikeFromRedis(String likedUserId, String likedPostId) {
        String key = RedisKeyUtils.getLikedKey(likedUserId, likedPostId);
        redisTemplate.opsForHash().put(RedisKeyUtils.MAP_KEY_USER_LIKED, key, LikedStatusEnum.UNLIKE.getCode());
    }

    @Override
    public void deleteLikedFromRedis(String likedUserId, String likedPostId) {
        String key = RedisKeyUtils.getLikedKey(likedUserId, likedPostId);
        redisTemplate.opsForHash().delete(RedisKeyUtils.MAP_KEY_USER_LIKED, key);
    }

    @Override
    public void incrementLikedCount(String likedUserId) {
        redisTemplate.opsForHash().increment(RedisKeyUtils.MAP_KEY_USER_LIKED_COUNT, likedUserId, 1);
    }

    @Override
    public void decrementLikedCount(String likedUserId) {
        redisTemplate.opsForHash().increment(RedisKeyUtils.MAP_KEY_USER_LIKED_COUNT, likedUserId, -1);
    }

    @Override
    public List<UserLike> getLikedDataFromRedis() {
        Cursor<Map.Entry<Object, Object>> cursor = redisTemplate.opsForHash().scan(RedisKeyUtils.MAP_KEY_USER_LIKED, ScanOptions.NONE);
        List<UserLike> list = new ArrayList<>();
        while (cursor.hasNext()) {
            Map.Entry<Object, Object> entry = cursor.next();
            String key = (String) entry.getKey();
            //分離出 likedUserId,likedPostId
            String[] split = key.split("::");
            String likedUserId = split[0];
            String likedPostId = split[1];
            Integer value = (Integer) entry.getValue();

            //組裝成 UserLike 物件
            UserLike userLike = new UserLike(likedUserId, likedPostId, value);
            list.add(userLike);

            //存到 list 後從 Redis 中刪除
            redisTemplate.opsForHash().delete(RedisKeyUtils.MAP_KEY_USER_LIKED, key);
        }

        return list;
    }

    @Override
    public List<LikedCountDTO> getLikedCountFromRedis() {
        Cursor<Map.Entry<Object, Object>> cursor = redisTemplate.opsForHash().scan(RedisKeyUtils.MAP_KEY_USER_LIKED_COUNT, ScanOptions.NONE);
        List<LikedCountDTO> list = new ArrayList<>();
        while (cursor.hasNext()) {
            Map.Entry<Object, Object> map = cursor.next();
            //將點贊數量儲存在 LikedCountDT
            String key = (String) map.getKey();
            LikedCountDTO dto = new LikedCountDTO(key, (Integer) map.getValue());
            list.add(dto);
            //從Redis中刪除這條記錄
            redisTemplate.opsForHash().delete(RedisKeyUtils.MAP_KEY_USER_LIKED_COUNT, key);
        }
        return list;
    }
}
複製程式碼

五、Redis 實現分散式鎖

講完了基礎操作,再說個實戰運用,用Redis 實現分散式鎖 。

實現分散式鎖之前先看兩個 Redis 命令:

  • SETNX

    key設定值為value,如果key不存在,這種情況下等同SET命令。 當key存在時,什麼也不做。SETNX是”SET if Not eXists”的簡寫。

    返回值

    Integer reply, 特定值:

    • 1 如果key被設定了
    • 0 如果key沒有被設定

    例子

    redis> SETNX mykey "Hello"
    (integer) 1
    redis> SETNX mykey "World"
    (integer) 0
    redis> GET mykey
    "Hello"
    redis> 
    複製程式碼
  • GETSET

    自動將key對應到value並且返回原來key對應的value。如果key存在但是對應的value不是字串,就返回錯誤。

    設計模式

    GETSET可以和INCR一起使用實現支援重置的計數功能。舉個例子:每當有事件發生的時候,一段程式都會呼叫INCR給key mycounter加1,但是有時我們需要獲取計數器的值,並且自動將其重置為0。這可以通過GETSET mycounter “0”來實現:

    INCR mycounter
    GETSET mycounter "0"
    GET mycounter
    複製程式碼

    返回值

    bulk-string-reply: 返回之前的舊值,如果之前Key不存在將返回nil

    例子

    redis> INCR mycounter
    (integer) 1
    redis> GETSET mycounter "0"
    "1"
    redis> GET mycounter
    "0"
    redis>
    複製程式碼

這兩個命令在 java 中對應為 setIfAbsentgetAndSet

分散式鎖的實現:

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

@Component
@Slf4j
public class RedisLock {

    @Autowired
    StringRedisTemplate redisTemplate;

    /**
     * 加鎖
     * @param key
     * @param value 當前時間 + 超時時間
     * @return
     */
    public boolean lock(String key, String value){
        if (redisTemplate.opsForValue().setIfAbsent(key, value)){
            return true;
        }

        //解決死鎖,且當多個執行緒同時來時,只會讓一個執行緒拿到鎖
        String currentValue = redisTemplate.opsForValue().get(key);
        //如果過期
        if (!StringUtils.isEmpty(currentValue) &&
                Long.parseLong(currentValue) < System.currentTimeMillis()){
            //獲取上一個鎖的時間
            String oldValue = redisTemplate.opsForValue().getAndSet(key, value);
            if (StringUtils.isEmpty(oldValue) && oldValue.equals(currentValue)){
                return true;
            }
        }

        return false;
    }

    /**
     * 解鎖
     * @param key
     * @param value
     */
    public void unlock(String key, String value){

        try {
            String currentValue = redisTemplate.opsForValue().get(key);
            if (!StringUtils.isEmpty(currentValue) && currentValue.equals(value)){
                redisTemplate.opsForValue().getOperations().delete(key);
            }
        }catch (Exception e){
            log.error("【redis鎖】解鎖失敗, {}", e);
        }
    }
}
複製程式碼

使用:


/**
 * 模擬秒殺
 */
public class SecKillService {

    @Autowired
    RedisLock redisLock;

    //超時時間10s
    private static final int TIMEOUT = 10 * 1000;

    public void secKill(String productId){
        long time = System.currentTimeMillis() + TIMEOUT;
        //加鎖
        if (!redisLock.lock(productId, String.valueOf(time))){
            throw new SellException(101, "人太多了,等會兒再試吧~");
        }

        //具體的秒殺邏輯

        //解鎖
        redisLock.unlock(productId, String.valueOf(time));
    }
}
複製程式碼

更多 Redis 的具體使用場景請關注開源專案 CodeRiver,致力於打造全平臺型全棧精品開源專案。

coderiver 中文名 河碼,是一個為程式設計師和設計師提供專案協作的平臺。無論你是前端、後端、移動端開發人員,或是設計師、產品經理,都可以在平臺上釋出專案,與志同道合的小夥伴一起協作完成專案。

coderiver河碼 類似程式設計師客棧,但主要目的是方便各細分領域人才之間技術交流,共同成長,多人協作完成專案。暫不涉及金錢交易。

計劃做成包含 pc端(Vue、React)、移動H5(Vue、React)、ReactNative混合開發、Android原生、微信小程式、java後端的全平臺型全棧專案,歡迎關注。

專案地址:github.com/cachecats/c…


您的鼓勵是我前行最大的動力,歡迎點贊,歡迎送小星星✨ ~

相關文章