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

本文主要講 Redis 的使用,如何與 SpringBoot 項(xiàng)目整合,如何使用注解方式和 RedisTemplate 方式實(shí)現(xiàn)緩存。最后會(huì)給一個(gè)用 Redis 實(shí)現(xiàn)分布式鎖,用在秒殺系統(tǒng)中的案例。

更多 Redis 的實(shí)際運(yùn)用場(chǎng)景請(qǐng)關(guān)注開(kāi)源項(xiàng)目 coderiver

項(xiàng)目地址:https://github.com/cachecats/coderiver

一、NoSQL 概述

什么是 NoSQL ?

NoSQL(NoSQL = Not Only SQL ),意即“不僅僅是SQL”,泛指非關(guān)系型的數(shù)據(jù)庫(kù)。

為什么需要 NoSQL ?

隨著互聯(lián)網(wǎng)web2.0網(wǎng)站的興起,傳統(tǒng)的關(guān)系數(shù)據(jù)庫(kù)在應(yīng)付web2.0網(wǎng)站,特別是超大規(guī)模和高并發(fā)的SNS類型的web2.0純動(dòng)態(tài)網(wǎng)站已經(jīng)顯得力不從心,暴露了很多難以克服的問(wèn)題,而非關(guān)系型的數(shù)據(jù)庫(kù)則由于其本身的特點(diǎn)得到了非常迅速的發(fā)展。NoSQL數(shù)據(jù)庫(kù)的產(chǎn)生就是為了解決大規(guī)模數(shù)據(jù)集合多重?cái)?shù)據(jù)種類帶來(lái)的挑戰(zhàn),尤其是大數(shù)據(jù)應(yīng)用難題。 -- 百度百科

NoSQL 數(shù)據(jù)庫(kù)的四大分類

  • 鍵值(key-value)存儲(chǔ)
  • 列存儲(chǔ)
  • 文檔數(shù)據(jù)庫(kù)
  • 圖形數(shù)據(jù)庫(kù)
分類 相關(guān)產(chǎn)品 典型應(yīng)用 數(shù)據(jù)模型 優(yōu)點(diǎn) 缺點(diǎn)
鍵值(key-value) Tokyo、 Cabinet/Tyrant、Redis、Voldemort、Berkeley DB 內(nèi)容緩存,主要用于處理大量數(shù)據(jù)的高訪問(wèn)負(fù)載 一系列鍵值對(duì) 快速查詢 存儲(chǔ)的數(shù)據(jù)缺少結(jié)構(gòu)化
列存儲(chǔ)數(shù)據(jù)庫(kù) Cassandra, HBase, Riak 分布式的文件系統(tǒng) 以列簇式存儲(chǔ),將同一列數(shù)據(jù)存在一起 查找速度快,可擴(kuò)展性強(qiáng),更容易進(jìn)行分布式擴(kuò)展 功能相對(duì)局限
文檔數(shù)據(jù)庫(kù) CouchDB, MongoDB Web應(yīng)用(與Key-Value類似,value是結(jié)構(gòu)化的) 一系列鍵值對(duì) 數(shù)據(jù)結(jié)構(gòu)要求不嚴(yán)格 查詢性能不高,而且缺乏統(tǒng)一的查詢語(yǔ)法
圖形(Graph)數(shù)據(jù)庫(kù) Neo4J, InfoGrid, Infinite Graph 社交網(wǎng)絡(luò),推薦系統(tǒng)等。專注于構(gòu)建關(guān)系圖譜 圖結(jié)構(gòu) 利用圖結(jié)構(gòu)相關(guān)算法 需要對(duì)整個(gè)圖做計(jì)算才能得出結(jié)果,不容易做分布式集群方案

NoSQL 的特點(diǎn)

  • 易擴(kuò)展
  • 靈活的數(shù)據(jù)模型
  • 大數(shù)據(jù)量,高性能
  • 高可用

二、Redis 概述

Redis的應(yīng)用場(chǎng)景

  • 緩存
  • 任務(wù)隊(duì)列
  • 網(wǎng)站訪問(wèn)統(tǒng)計(jì)
  • 應(yīng)用排行榜
  • 數(shù)據(jù)過(guò)期處理
  • 分布式集群架構(gòu)中的 session 分離

Redis 安裝

網(wǎng)上有很多 Redis 的安裝教程,這里就不多說(shuō)了,只說(shuō)下 Docker 的安裝方法:

Docker 安裝運(yùn)行 Redis

docker run -d -p 6379:6379 redis:4.0.8

如果以后想啟動(dòng) Redis 服務(wù),打開(kāi)命令行,輸入以下命令即可。

redis-server

使用前先引入依賴

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

三、注解方式使用 Redis 緩存

使用緩存有兩個(gè)前置步驟

  1. pom.xml 引入依賴

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    
  2. 在啟動(dòng)類上加注解 @EnableCaching

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

常用的注解有以下幾個(gè)

  • @Cacheable

    屬性如下圖

image

用于查詢和添加緩存,第一次查詢的時(shí)候返回該方法返回值,并向 Redis 服務(wù)器保存數(shù)據(jù)。

以后調(diào)用該方法先從 Redis 中查是否有數(shù)據(jù),如果有直接返回 Redis 緩存的數(shù)據(jù),而不執(zhí)行方法里的代碼。如果沒(méi)有則正常執(zhí)行方法體中的代碼。

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

驗(yàn)證了下,value 和 cacheNames 的作用是一樣的,都是標(biāo)識(shí)主鍵。兩個(gè)屬性不能同時(shí)定義,只能定義一個(gè),否則會(huì)報(bào)錯(cuò)。

condition 和 unless 是條件,后面會(huì)講用法。其他的幾個(gè)屬性不常用,其實(shí)我也不知道怎么用…

  • @CachePut

    更新 Redis 中對(duì)應(yīng)鍵的值。屬性和 @Cacheable 相同

  • @CacheEvict

    刪除 Redis 中對(duì)應(yīng)鍵的值。

3.1 添加緩存

在需要加緩存的方法上添加注解 @Cacheable(cacheNames = "product", key = "123"),

cacheNameskey 都必須填,如果不填 key ,默認(rèn)的 key 是當(dāng)前的方法名,更新緩存時(shí)會(huì)因?yàn)榉椒煌率 ?/p>

如在訂單列表上加緩存

    @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<>();
        //數(shù)據(jù)拼裝
        for (ProductCategory category : productCategoryList) {
            ProductVO productVO = new ProductVO();
            //屬性拷貝
            BeanUtils.copyProperties(category, productVO);
            //把類型匹配的商品添加進(jìn)去
            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);
    }

可能會(huì)報(bào)如下錯(cuò)誤

image

對(duì)象未序列化。讓對(duì)象實(shí)現(xiàn) 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 里有一個(gè)插件:GenerateSerialVersionUID 比較方便。

重啟項(xiàng)目訪問(wèn)訂單列表,在 rdm 里查看 Redis 緩存,有 product::123 說(shuō)明緩存成功。

image

3.2 更新緩存

在需要更新緩存的方法上加注解: @CachePut(cacheNames = "prodcut", key = "123")

注意

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

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

3.3 刪除緩存

在需要?jiǎng)h除緩存的方法上加注解:@CacheEvict(cacheNames = "prodcut", key = "123"),執(zhí)行完這個(gè)方法之后會(huì)將 Redis 中對(duì)應(yīng)的記錄刪除。

3.4 其他常用功能

  1. cacheNames 也可以統(tǒng)一寫在類上面, @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 也可以動(dòng)態(tài)設(shè)置為方法的參數(shù)

    @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);
    }
    

    如果參數(shù)是個(gè)對(duì)象,也可以設(shè)置對(duì)象的某個(gè)屬性為 key。比如其中一個(gè)參數(shù)是 user 對(duì)象,key 可以寫成 key="#user.id"

  3. 緩存還可以設(shè)置條件。

    設(shè)置當(dāng) openid 的長(zhǎng)度大于3時(shí)才緩存

    @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 即條件不成立時(shí)緩存。#result 代表返回值,意思是當(dāng)返回碼不等于 0 時(shí)不緩存,也就是等于 0 時(shí)才緩存。

    
    @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 緩存

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

4.1 Redis 配置

第一步還是引入依賴和在啟動(dòng)類上加上 @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

然后寫個(gè) 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 的數(shù)據(jù)結(jié)構(gòu)類型

Redis 可以存儲(chǔ)鍵與5種不同數(shù)據(jù)結(jié)構(gòu)類型之間的映射,這5種數(shù)據(jù)結(jié)構(gòu)類型分別為String(字符串)、List(列表)、Set(集合)、Hash(散列)和 Zset(有序集合)。

下面來(lái)對(duì)這5種數(shù)據(jù)結(jié)構(gòu)類型作簡(jiǎn)單的介紹:

結(jié)構(gòu)類型 結(jié)構(gòu)存儲(chǔ)的值 結(jié)構(gòu)的讀寫能力
String 可以是字符串、整數(shù)或者浮點(diǎn)數(shù) 對(duì)整個(gè)字符串或者字符串的其中一部分執(zhí)行操作;對(duì)象和浮點(diǎn)數(shù)執(zhí)行自增(increment)或者自減(decrement)
List 一個(gè)鏈表,鏈表上的每個(gè)節(jié)點(diǎn)都包含了一個(gè)字符串 從鏈表的兩端推入或者彈出元素;根據(jù)偏移量對(duì)鏈表進(jìn)行修剪(trim);讀取單個(gè)或者多個(gè)元素;根據(jù)值來(lái)查找或者移除元素
Set 包含字符串的無(wú)序收集器(unorderedcollection),并且被包含的每個(gè)字符串都是獨(dú)一無(wú)二的、各不相同 添加、獲取、移除單個(gè)元素;檢查一個(gè)元素是否存在于某個(gè)集合中;計(jì)算交集、并集、差集;從集合里賣弄隨機(jī)獲取元素
Hash 包含鍵值對(duì)的無(wú)序散列表 添加、獲取、移除單個(gè)鍵值對(duì);獲取所有鍵值對(duì)
Zset 字符串成員(member)與浮點(diǎn)數(shù)分值(score)之間的有序映射,元素的排列順序由分值的大小決定 添加、獲取、刪除單個(gè)元素;根據(jù)分值范圍(range)或者成員來(lái)獲取元素

4.3 StringRedisTemplate 與 RedisTemplate

RedisTemplate 對(duì)五種數(shù)據(jù)結(jié)構(gòu)分別定義了操作

  • redisTemplate.opsForValue();

    操作字符串

  • redisTemplate.opsForHash();

    操作hash

  • redisTemplate.opsForList();

    操作list

  • redisTemplate.opsForSet();

    操作set

  • redisTemplate.opsForZSet();

    操作有序set

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

StringRedisTemplate 與 RedisTemplate 的區(qū)別

  1. StringRedisTemplate 繼承了 RedisTemplate。

  2. RedisTemplate 是一個(gè)泛型類,而 StringRedisTemplate 則不是。

  3. StringRedisTemplate 只能對(duì) key=String,value=String 的鍵值對(duì)進(jìn)行操作,RedisTemplate 可以對(duì)任何類型的 key-value 鍵值對(duì)操作。

  4. 他們各自序列化的方式不同,但最終都是得到了一個(gè)字節(jié)數(shù)組,殊途同歸,StringRedisTemplate 使用的是 StringRedisSerializer 類;RedisTemplate 使用的是 JdkSerializationRedisSerializer 類。反序列化,則是一個(gè)得到 String,一個(gè)得到 Object

  5. 兩者的數(shù)據(jù)是不共通的,StringRedisTemplate 只能管理 StringRedisTemplate 里面的數(shù)據(jù),RedisTemplate 只能管理 RedisTemplate中 的數(shù)據(jù)。

4.4 項(xiàng)目中使用

在需要使用 Redis 的地方,用 @Autowired 注入進(jìn)來(lái)

@Autowired
RedisTemplate redisTemplate;
 
@Autowired
StringRedisTemplate stringRedisTemplate;

由于項(xiàng)目中暫時(shí)僅用到了 StringRedisTemplate 與 RedisTemplate 的 Hash 結(jié)構(gòu),StringRedisTemplate 比較簡(jiǎn)單就不貼代碼了,下面僅對(duì)操作 Hash 進(jìn)行舉例。

關(guān)于 RedisTemplate 的詳細(xì)用法,有一篇文章已經(jīng)講的很細(xì)很好了,我覺(jué)得沒(méi)必要再去寫了。傳送門

用 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 對(duì)象
            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();
            //將點(diǎn)贊數(shù)量存儲(chǔ)在 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 實(shí)現(xiàn)分布式鎖

講完了基礎(chǔ)操作,再說(shuō)個(gè)實(shí)戰(zhàn)運(yùn)用,用Redis 實(shí)現(xiàn)分布式鎖 。

實(shí)現(xiàn)分布式鎖之前先看兩個(gè) Redis 命令:

  • SETNX

    key設(shè)置值為value,如果key不存在,這種情況下等同SET命令。 當(dāng)key存在時(shí),什么也不做。SETNX是”SET if Not eXists”的簡(jiǎn)寫。

    返回值

    Integer reply, 特定值:

    • 1 如果key被設(shè)置了
    • 0 如果key沒(méi)有被設(shè)置

    例子

    redis> SETNX mykey "Hello"
    (integer) 1
    redis> SETNX mykey "World"
    (integer) 0
    redis> GET mykey
    "Hello"
    redis> 
    
  • GETSET

    自動(dòng)將key對(duì)應(yīng)到value并且返回原來(lái)key對(duì)應(yīng)的value。如果key存在但是對(duì)應(yīng)的value不是字符串,就返回錯(cuò)誤。

    設(shè)計(jì)模式

    GETSET可以和INCR一起使用實(shí)現(xiàn)支持重置的計(jì)數(shù)功能。舉個(gè)例子:每當(dāng)有事件發(fā)生的時(shí)候,一段程序都會(huì)調(diào)用INCR給key mycounter加1,但是有時(shí)我們需要獲取計(jì)數(shù)器的值,并且自動(dòng)將其重置為0。這可以通過(guò)GETSET mycounter “0”來(lái)實(shí)現(xiàn):

    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>
    

這兩個(gè)命令在 java 中對(duì)應(yīng)為 setIfAbsentgetAndSet

分布式鎖的實(shí)現(xiàn):

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 當(dāng)前時(shí)間 + 超時(shí)時(shí)間
     * @return
     */
    public boolean lock(String key, String value){
        if (redisTemplate.opsForValue().setIfAbsent(key, value)){
            return true;
        }

        //解決死鎖,且當(dāng)多個(gè)線程同時(shí)來(lái)時(shí),只會(huì)讓一個(gè)線程拿到鎖
        String currentValue = redisTemplate.opsForValue().get(key);
        //如果過(guò)期
        if (!StringUtils.isEmpty(currentValue) &&
                Long.parseLong(currentValue) < System.currentTimeMillis()){
            //獲取上一個(gè)鎖的時(shí)間
            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;

    //超時(shí)時(shí)間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, "人太多了,等會(huì)兒再試吧~");
        }

        //具體的秒殺邏輯

        //解鎖
        redisLock.unlock(productId, String.valueOf(time));
    }
}

更多 Redis 的具體使用場(chǎng)景請(qǐng)關(guān)注開(kāi)源項(xiàng)目 CodeRiver,致力于打造全平臺(tái)型全棧精品開(kāi)源項(xiàng)目。

coderiver 中文名 河碼,是一個(gè)為程序員和設(shè)計(jì)師提供項(xiàng)目協(xié)作的平臺(tái)。無(wú)論你是前端、后端、移動(dòng)端開(kāi)發(fā)人員,或是設(shè)計(jì)師、產(chǎn)品經(jīng)理,都可以在平臺(tái)上發(fā)布項(xiàng)目,與志同道合的小伙伴一起協(xié)作完成項(xiàng)目。

coderiver河碼 類似程序員客棧,但主要目的是方便各細(xì)分領(lǐng)域人才之間技術(shù)交流,共同成長(zhǎng),多人協(xié)作完成項(xiàng)目。暫不涉及金錢交易。

計(jì)劃做成包含 pc端(Vue、React)、移動(dòng)H5(Vue、React)、ReactNative混合開(kāi)發(fā)、Android原生、微信小程序、java后端的全平臺(tái)型全棧項(xiàng)目,歡迎關(guān)注。

項(xiàng)目地址:https://github.com/cachecats/coderiver


您的鼓勵(lì)是我前行最大的動(dòng)力,歡迎點(diǎn)贊,歡迎送小星星? ~

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容