本文主要講 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è)前置步驟
-
在
pom.xml引入依賴<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> -
在啟動(dòng)類上加注解
@EnableCaching@SpringBootApplication @EnableCaching public class SellApplication { public static void main(String[] args) { SpringApplication.run(SellApplication.class, args); } }
常用的注解有以下幾個(gè)
-
@Cacheable屬性如下圖
用于查詢和添加緩存,第一次查詢的時(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"),
cacheNames 和 key 都必須填,如果不填 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ò)誤
對(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ō)明緩存成功。
3.2 更新緩存
在需要更新緩存的方法上加注解: @CachePut(cacheNames = "prodcut", key = "123")
注意
cacheNames和key要跟@Cacheable()里的一致,才會(huì)正確更新。
@CachePut()和@Cacheable()注解的方法返回值要一致
3.3 刪除緩存
在需要?jiǎng)h除緩存的方法上加注解:@CacheEvict(cacheNames = "prodcut", key = "123"),執(zhí)行完這個(gè)方法之后會(huì)將 Redis 中對(duì)應(yīng)的記錄刪除。
3.4 其他常用功能
-
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(); } } -
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" -
緩存還可以設(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ū)別
StringRedisTemplate 繼承了 RedisTemplate。
RedisTemplate 是一個(gè)泛型類,而 StringRedisTemplate 則不是。
StringRedisTemplate 只能對(duì) key=String,value=String 的鍵值對(duì)進(jìn)行操作,RedisTemplate 可以對(duì)任何類型的 key-value 鍵值對(duì)操作。
他們各自序列化的方式不同,但最終都是得到了一個(gè)字節(jié)數(shù)組,殊途同歸,StringRedisTemplate 使用的是 StringRedisSerializer 類;RedisTemplate 使用的是 JdkSerializationRedisSerializer 類。反序列化,則是一個(gè)得到 String,一個(gè)得到 Object
兩者的數(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)為 setIfAbsent 和 getAndSet
分布式鎖的實(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)贊,歡迎送小星星? ~