摘要:和注解的方法返回值要一致刪除緩存在需要刪除緩存的方法上加注解,執行完這個方法之后會將中對應的記錄刪除。代表返回值,意思是當返回碼不等于時不緩存,也就是等于時才緩存。返回值特定值如果被設置了如果沒有被設置例子自動將對應到并且返回原來對應的。
本文主要講 Redis 的使用,如何與 SpringBoot 項目整合,如何使用注解方式和 RedisTemplate 方式實現緩存。最后會給一個用 Redis 實現分布式鎖,用在秒殺系統中的案例。
更多 Redis 的實際運用場景請關注開源項目 coderiver
項目地址:https://github.com/cachecats/...
一、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 | 社交網絡,推薦系統等。專注于構建關系圖譜 | 圖結構 | 利用圖結構相關算法 | 需要對整個圖做計算才能得出結果,不容易做分布式集群方案 |
易擴展
靈活的數據模型
大數據量,高性能
高可用
二、Redis 概述 Redis的應用場景緩存
任務隊列
網站訪問統計
應用排行榜
數據過期處理
分布式集群架構中的 session 分離
Redis 安裝網上有很多 Redis 的安裝教程,這里就不多說了,只說下 Docker 的安裝方法:
Docker 安裝運行 Redis
docker run -d -p 6379:6379 redis:4.0.8
如果以后想啟動 Redis 服務,打開命令行,輸入以下命令即可。
redis-server
使用前先引入依賴
三、注解方式使用 Redis 緩存org.springframework.boot spring-boot-starter-data-redis
使用緩存有兩個前置步驟
在 pom.xml 引入依賴
org.springframework.boot spring-boot-starter-data-redis
在啟動類上加注解 @EnableCaching
@SpringBootApplication @EnableCaching public class SellApplication { public static void main(String[] args) { SpringApplication.run(SellApplication.class, args); } }
常用的注解有以下幾個
@Cacheable
屬性如下圖
用于查詢和添加緩存,第一次查詢的時候返回該方法返回值,并向 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"),
cacheNames 和 key 都必須填,如果不填 key ,默認的 key 是當前的方法名,更新緩存時會因為方法名不同而更新失敗。
如在訂單列表上加緩存
@RequestMapping(value = "/list", method = RequestMethod.GET) @Cacheable(cacheNames = "product", key = "123") public ResultVO list() { // 1.查詢所有上架商品 ListproductInfoList = productInfoService.findUpAll(); // 2.查詢類目(一次性查詢) //用 java8 的特性獲取到上架商品的所有類型 List categoryTypes = productInfoList.stream().map(e -> e.getCategoryType()).collect(Collectors.toList()); List productCategoryList = categoryService.findByCategoryTypeIn(categoryTypes); List productVOList = new ArrayList<>(); //數據拼裝 for (ProductCategory category : productCategoryList) { ProductVO productVO = new ProductVO(); //屬性拷貝 BeanUtils.copyProperties(category, productVO); //把類型匹配的商品添加進去 List 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); }
可能會報如下錯誤
對象未序列化。讓對象實現 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 ListproductInfoVOList ; }
生成唯一的 id 在 IDEA 里有一個插件:GenerateSerialVersionUID 比較方便。
重啟項目訪問訂單列表,在 rdm 里查看 Redis 緩存,有 product::123 說明緩存成功。
3.2 更新緩存在需要更新緩存的方法上加注解: @CachePut(cacheNames = "prodcut", key = "123")
注意
3.3 刪除緩存cacheNames 和 key 要跟 @Cacheable() 里的一致,才會正確更新。
@CachePut() 和 @Cacheable() 注解的方法返回值要一致
在需要刪除緩存的方法上加注解:@CacheEvict(cacheNames = "prodcut", key = "123"),執行完這個方法之后會將 Redis 中對應的記錄刪除。
3.4 其他常用功能
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(); } }
Key 也可以動態設置為方法的參數
@GetMapping("/detail") @Cacheable(cacheNames = "prodcut", key = "#openid") public ResultVOdetail(@RequestParam("openid") String openid, @RequestParam("orderId") String orderId){ OrderDTO orderDTO = buyerService.findOrderOne(openid, orderId); return ResultVOUtils.success(orderDTO); }
如果參數是個對象,也可以設置對象的某個屬性為 key。比如其中一個參數是 user 對象,key 可以寫成 key="#user.id"
緩存還可以設置條件。
設置當 openid 的長度大于3時才緩存
@GetMapping("/detail") @Cacheable(cacheNames = "prodcut", key = "#openid", condition = "#openid.length > 3") public ResultVOdetail(@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四、RedisTemplate 使用 Redis 緩存detail(@RequestParam("openid") String openid, @RequestParam("orderId") String orderId){ OrderDTO orderDTO = buyerService.findOrderOne(openid, orderId); return ResultVOUtils.success(orderDTO); }
與使用注解方式不同,注解方式可以零配置,只需引入依賴并在啟動類上加上 @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 RedisTemplateredisTemplate( RedisConnectionFactory redisConnectionFactory) throws UnknownHostException { Jackson2JsonRedisSerializer
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)或者成員來獲取元素 |
RedisTemplate 對五種數據結構分別定義了操作
redisTemplate.opsForValue();
操作字符串
redisTemplate.opsForHash();
操作hash
redisTemplate.opsForList();
操作list
redisTemplate.opsForSet();
操作set
redisTemplate.opsForZSet();
操作有序set
如果操作字符串的話,建議用 StringRedisTemplate 。
StringRedisTemplate 與 RedisTemplate 的區別StringRedisTemplate 繼承了 RedisTemplate。
RedisTemplate 是一個泛型類,而 StringRedisTemplate 則不是。
StringRedisTemplate 只能對 key=String,value=String 的鍵值對進行操作,RedisTemplate 可以對任何類型的 key-value 鍵值對操作。
他們各自序列化的方式不同,但最終都是得到了一個字節數組,殊途同歸,StringRedisTemplate 使用的是 StringRedisSerializer 類;RedisTemplate 使用的是 JdkSerializationRedisSerializer 類。反序列化,則是一個得到 String,一個得到 Object
兩者的數據是不共通的,StringRedisTemplate 只能管理 StringRedisTemplate 里面的數據,RedisTemplate 只能管理 RedisTemplate中 的數據。
4.4 項目中使用在需要使用 Redis 的地方,用 @Autowired 注入進來
@Autowired RedisTemplate redisTemplate; @Autowired StringRedisTemplate stringRedisTemplate;
由于項目中暫時僅用到了 StringRedisTemplate 與 RedisTemplate 的 Hash 結構,StringRedisTemplate 比較簡單就不貼代碼了,下面僅對操作 Hash 進行舉例。
關于 RedisTemplate 的詳細用法,有一篇文章已經講的很細很好了,我覺得沒必要再去寫了。傳送門
用 RedisTemplate 操作 Hashpackage 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五、Redis 實現分布式鎖getLikedDataFromRedis() { Cursor > cursor = redisTemplate.opsForHash().scan(RedisKeyUtils.MAP_KEY_USER_LIKED, ScanOptions.NONE); List list = new ArrayList<>(); while (cursor.hasNext()) { Map.Entry 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 getLikedCountFromRedis() { Cursor > cursor = redisTemplate.opsForHash().scan(RedisKeyUtils.MAP_KEY_USER_LIKED_COUNT, ScanOptions.NONE); List list = new ArrayList<>(); while (cursor.hasNext()) { Map.Entry 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 命令:
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 中對應為 setIfAbsent 和 getAndSet
分布式鎖的實現:
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后端的全平臺型全棧項目,歡迎關注。
項目地址:https://github.com/cachecats/...
您的鼓勵是我前行最大的動力,歡迎點贊,歡迎送小星星? ~
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/72271.html
摘要:與整合默認使用的是,相較于,是一個可伸縮的,線程安全的客戶端。在處理高并發方面有更多的優勢。使用依賴主要需要的依賴為配置配置使用與整合可以在不更改現有代碼邏輯的基礎上,通過增加注解的方式,實現緩存。 springboot2.0 與redis整合默認使用的是Lettuce,相較于jedis,lettuce 是一個可伸縮的,線程安全的redis客戶端。在處理高并發方面有更多的優勢。 Red...
摘要:至此,已完成整合獨立模塊做緩存詳情請看地址相關文章系列整合獨立模塊 項目github地址:https://github.com/5-Ason/aso...具體可看 ./db/db-redis 和 ./db/db-cache 兩個模塊 // TODO 在整合redis之前需要先本地配置好redis環境,遲點有時間補一下linux下下載安裝配置redis 本文主要實現的是對數據操作進行獨立...
摘要:至此,已完成整合獨立模塊做緩存詳情請看地址相關文章系列整合獨立模塊 項目github地址:https://github.com/5-Ason/aso...具體可看 ./db/db-redis 和 ./db/db-cache 兩個模塊 // TODO 在整合redis之前需要先本地配置好redis環境,遲點有時間補一下linux下下載安裝配置redis 本文主要實現的是對數據操作進行獨立...
閱讀 1523·2023-04-26 02:03
閱讀 4707·2021-11-22 13:53
閱讀 4580·2021-09-09 11:40
閱讀 3782·2021-09-09 09:34
閱讀 2125·2019-08-30 13:18
閱讀 3501·2019-08-30 11:25
閱讀 3295·2019-08-26 14:06
閱讀 2545·2019-08-26 13:52