Spring Boot - 數據庫集成04 - 集成Redis

Spring boot集成Redis

文章目錄

  • Spring boot集成Redis
    • 一:redis基本集成
      • 1:RedisTemplate + Jedis
        • 1.1:RedisTemplate
        • 1.2:實現案例
          • 1.2.1:依賴引入和屬性配置
          • 1.2.2:redisConfig配置
          • 1.2.3:基礎使用
      • 2:RedisTemplate+Lettuce
        • 2.1:什么是Lettuce
        • 2.2:為何能干掉Jedis成為默認
        • 2.3:Lettuce的基本的API方式
        • 2.4:實現案例
        • 2.5:數據類封裝
    • 二:集成redisson
      • 1:單機可重入鎖
      • 2:紅鎖(Red Lock)
        • 2.1:環境準備
        • 2.2:配置編寫
        • 2.3:使用測試
      • 3:讀寫鎖
        • 3.1:讀鎖lock.readLock()
        • 3.2:寫鎖lock.writeLock()
      • 4:Semaphore和countDownLatch
        • 4.1:Semaphore
        • 4.2:閉鎖CountDownLatch

一:redis基本集成

首先對redis來說,所有的key(鍵)都是字符串。

我們在談基礎數據結構時,討論的是存儲值的數據類型,主要包括常見的5種數據類型,分別是:String、List、Set、Zset、Hash。

在這里插入圖片描述

結構類型結構存儲的值結構的讀寫能力
String可以是字符串、整數或浮點數對整個字符串或字符串的一部分進行操作;對整數或浮點數進行自增或自減操作;
List一個鏈表,鏈表上的每個節點都包含一個字符串對鏈表的兩端進行push和pop操作,讀取單個或多個元素;根據值查找或刪除元素;
Hash包含鍵值對的無序散列表包含方法有添加、獲取、刪除單個元素
Set包含字符串的無序集合字符串的集合,包含基礎的方法有看是否存在添加、獲取、刪除;
還包含計算交集、并集、差集等
Zset和散列一樣,用于存儲鍵值對字符串成員與浮點數分數之間的有序映射;
元素的排列順序由分數的大小決定;
包含方法有添加、獲取、刪除單個元素以及根據分值范圍或成員來獲取元素

1:RedisTemplate + Jedis

Jedis是Redis的Java客戶端,在SpringBoot 1.x版本中也是默認的客戶端。

在SpringBoot 2.x版本中默認客戶端是Luttuce。

1.1:RedisTemplate

Spring 通過模板方式(RedisTemplate)提供了對Redis的數據查詢和操作功能。

什么是模板方法模式

模板方法模式(Template pattern): 在一個方法中定義一個算法的骨架, 而將一些步驟延遲到子類中.

模板方法使得子類可以在不改變算法結構的情況下, 重新定義算法中的某些步驟。
在這里插入圖片描述

RedisTemplate對于Redis5種基礎類型的操作

redisTemplate.opsForValue(); // 操作字符串
redisTemplate.opsForHash(); // 操作hash
redisTemplate.opsForList(); // 操作list
redisTemplate.opsForSet(); // 操作set
redisTemplate.opsForZSet(); // 操作zset

對HyperLogLogs(基數統計)類型的操作

redisTemplate.opsForHyperLogLog();

對geospatial (地理位置)類型的操作

redisTemplate.opsForGeo();

對于BitMap的操作,也是在opsForValue()方法返回類型ValueOperations中

Boolean setBit(K key, long offset, boolean value);
Boolean getBit(K key, long offset);

對于Stream的操作

redisTemplate.opsForStream();
1.2:實現案例

本例子主要基于SpringBoot2+ 使用Jedis客戶端,通過RedisTemplate模板方式訪問Redis數據。

其他實體類結構請看(集成Jpa)

1.2.1:依賴引入和屬性配置
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId><!-- 暫時先排除lettuce-core,使用jedis --><!-- jedis是spring-boot 1.x的默認,lettuce是spring-boot 2.x的默認 --><exclusions><exclusion><artifactId>lettuce-core</artifactId><groupId>io.lettuce</groupId></exclusion></exclusions>
</dependency><!-- 格外使用jedis -->
<dependency><groupId>redis.clients</groupId><artifactId>jedis</artifactId>
</dependency><!-- commons-pools,連接池 -->
<dependency><groupId>org.apache.commons</groupId><artifactId>commons-pool2</artifactId><version>2.9.0</version>
</dependency>
spring:# swagger配置mvc:path match:# 由于 springfox 3.0.x 版本 和 Spring Boot 2.6.x 版本有沖突,所以還需要先解決這個 bugmatching-strategy: ANT_PATH_MATCHER# 數據源配置datasource:url: jdbc:mysql://127.0.0.1:3306/mytest?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghaidriver-class-name: com.mysql.cj.jdbc.Driver # 8.0 +username: rootpassword: bnm314159# JPA 配置jpa:generate-ddl: false # 是否自動創建數據庫表show-sql: true # 是否打印生成的 sqlproperties:hibernate:dialect: org.hibernate.dialect.MySQL8Dialect # 數據庫方言 mysql8format_sql: true # 是否格式化 sqluse_new_id_generator_mappings: true # 是否使用新的 id 生成器# redis 配置redis:database: 0 # redis數據庫索引(默認為0)host: 127.0.0.1 # redis服務器地址port: 6379 # redis服務器連接端口jedis:pool:min-idle: 0 # 連接池中的最小空閑連接max-active: 8 # 連接池最大連接數(使用負值表示沒有限制)max-idle: 8 # 連接池中的最大空閑連接max-wait: -1ms # 連接池最大阻塞等待時間(使用負值表示沒有限制)connect-timeout: 30000ms # 連接超時時間(毫秒)timeout: 30000ms # 讀取超時時間(毫秒)
1.2.2:redisConfig配置
package com.cui.jpa_demo.config;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.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;/*** @author cui haida* 2025/1/25*/
@Configuration
public class RedisConfig {/*** 配置RedisTemplate以支持鍵值對存儲* 該方法在Spring框架中定義了一個Bean,用于創建和配置RedisTemplate實例* RedisTemplate用于與Redis數據庫進行交互,支持數據的存儲和檢索** @param factory RedisConnectionFactory實例,用于連接Redis服務器* @return 配置好的RedisTemplate實例,用于執行鍵值對操作*/@Beanpublic RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {// 創建RedisTemplate實例,并指定鍵和值的類型RedisTemplate<String, Object> template = new RedisTemplate<>();// 設置連接工廠,用于建立與Redis服務器的連接template.setConnectionFactory(factory);// 配置鍵的序列化方式為StringRedisSerializer// 這是為了確保鍵以字符串形式存儲和檢索template.setKeySerializer(new StringRedisSerializer());// 配置哈希鍵的序列化方式為StringRedisSerializer// 這適用于哈希表中的鍵值對操作template.setHashKeySerializer(new StringRedisSerializer());// 配置值的序列化方式為GenericJackson2JsonRedisSerializer// 使用Jackson庫將對象序列化為JSON格式存儲template.setValueSerializer(new GenericJackson2JsonRedisSerializer());// 配置哈希表值的序列化方式為GenericJackson2JsonRedisSerializer// 同樣使用Jackson庫將對象序列化為JSON格式存儲template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());// 初始化RedisTemplate,確保所有必需的屬性都已設置template.afterPropertiesSet();// 返回配置好的RedisTemplate實例return template;}
}
1.2.3:基礎使用
package com.cui.jpa_demo.controller;import com.cui.jpa_demo.entity.bean.UserQueryBean;
import com.cui.jpa_demo.entity.model.User;
import com.cui.jpa_demo.entity.response.ResponseResult;
import com.cui.jpa_demo.service.IUserService;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.*;import javax.annotation.Resource;
import java.time.LocalDateTime;/*** @author cui haida* 2025/1/23*/
@RestController
@RequestMapping("/user")
public class UserController {private final IUserService userService;@Resourceprivate RedisTemplate<String, User> redisTemplate;public UserController(IUserService userService) {this.userService = userService;}@PostMapping("add")public ResponseResult<User> add(User user) {if (user.getId()==null || !userService.exists(user.getId())) {user.setCreateTime(LocalDateTime.now());user.setUpdateTime(LocalDateTime.now());userService.save(user);} else {user.setUpdateTime(LocalDateTime.now());userService.update(user);}return ResponseResult.success(userService.find(user.getId()));}/*** @return user list*/@GetMapping("edit/{userId}")public ResponseResult<User> edit(@PathVariable("userId") Long userId) {return ResponseResult.success(userService.find(userId));}/*** @return user list*/@GetMapping("list")public ResponseResult<Page<User>> list(@RequestParam int pageSize, @RequestParam int pageNumber) {return ResponseResult.success(userService.findPage(UserQueryBean.builder().build(), PageRequest.of(pageNumber, pageSize)));}@PostMapping("/redis/add")public ResponseResult<User> addIntoRedis(User user) {redisTemplate.opsForValue().set(String.valueOf(user.getId()), user);return ResponseResult.success(redisTemplate.opsForValue().get(String.valueOf(user.getId())));}@GetMapping("/redis/get/{userId}")public ResponseResult<User> getFromRedis(@PathVariable("userId") Long userId) {return ResponseResult.success(redisTemplate.opsForValue().get(String.valueOf(userId)));}
}

2:RedisTemplate+Lettuce

2.1:什么是Lettuce

Lettuce 是一個可伸縮線程安全的 Redis 客戶端。多個線程可以共享同一個 RedisConnection。

它利用優秀 netty NIO 框架來高效地管理多個連接。

Lettuce的特性:

  • 支持 同步、異步、響應式 的方式
  • 支持 Redis Sentinel
  • 支持 Redis Cluster
  • 支持 SSL 和 Unix Domain Socket 連接
  • 支持 Streaming API
  • 支持 CDI 和 Spring 的集成
  • 支持 Command Interfaces
  • 兼容 Java 8+ 以上版本
2.2:為何能干掉Jedis成為默認

除了上述特性的支持性之外,最為重要的是Lettuce中使用了Netty框架,使其具備線程共享和異步的支持性。

線程共享

Jedis 是直連模式,在多個線程間共享一個 Jedis 實例時是線程不安全的

如果想要在多線程環境下使用 Jedis,需要使用連接池,每個線程都去拿自己的 Jedis 實例,當連接數量增多時,物理連接成本就較高了。

Lettuce 是基于 netty 的,連接實例可以在多個線程間共享,所以,一個多線程的應用可以使用一個連接實例,而不用擔心并發線程的數量。

異步和反應式

Lettuce 從一開始就按照非阻塞式 IO 進行設計,是一個純異步客戶端,對異步和反應式 API 的支持都很全面。

即使是同步命令,底層的通信過程仍然是異步模型,只是通過阻塞調用線程來模擬出同步效果而已。

在這里插入圖片描述

2.3:Lettuce的基本的API方式

依賴POM包

<dependency><groupId>io.lettuce</groupId><artifactId>lettuce-core</artifactId><version>x.y.z.BUILD-SNAPSHOT</version>
</dependency>

基礎用法

// 聲明redis-client
RedisClient client = RedisClient.create("redis://localhost");
// 創建連接
StatefulRedisConnection<String, String> connection = client.connect();
// 同步命令
RedisStringCommands sync = connection.sync();
// 執行get方法
String value = sync.get("key");

異步方式

StatefulRedisConnection<String, String> connection = client.connect();
// 異步命令
RedisStringAsyncCommands<String, String> async = connection.async();
// 異步set & get
RedisFuture<String> set = async.set("key", "value")
RedisFuture<String> get = async.get("key")async.awaitAll(set, get) == trueset.get() == "OK"
get.get() == "value"
  • 響應式
StatefulRedisConnection<String, String> connection = client.connect();
RedisStringReactiveCommands<String, String> reactive = connection.reactive();
Mono<String> set = reactive.set("key", "value");
Mono<String> get = reactive.get("key");
// 訂閱
set.subscribe();get.block() == "value"
2.4:實現案例

依賴和配置

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId>
</dependency><!-- 一定要加入這個,否則連接池用不了 --> 
<dependency><groupId>org.apache.commons</groupId><artifactId>commons-pool2</artifactId>
</dependency>

配置

  # redis 配置redis:host: 127.0.0.1 # 地址port: 6379 # 端口database: 0 # redis 數據庫索引# 如果是集群模式,需要配置如下
#    cluster:
#      nodes:
#        - 127.0.0.1:7000
#        - 127.0.0.1:7001
#        - 127.0.0.1:7002lettuce:pool:max-wait: -1 # 最大連接等待時間, 默認 -1 表示沒有限制max-active: 8 # 最大連接數, 默認8max-idle: 8 # 最大空閑連接數, 默認8min-idle: 0 # 最小空閑連接數, 默認0
#    password: 123456 # 密碼
#    timeout: 10000ms # 超時時間
#    ssl: false # 是否啟用 SSL
#    sentinel:
#      master: mymaster # 主節點名稱
#      nodes: 127.0.0.1:26379,127.0.0.1:26380,127.0.0.1:26381 # 哨兵節點

序列化配置

redis的序列化也是我們在使用RedisTemplate的過程中需要注意的事情。

如果沒有特殊設置redis的序列化方式,那么它其實使用的是默認的序列化方式【JdkSerializationRedisSerializer】。

這種序列化最大的問題就是存入對象后,我們很難直觀看到存儲的內容,很不方便我們排查問題

RedisTemplate這個類的泛型是<String,Object>, 也就是他是支持寫入Object對象的,那么這個對象采取什么方式序列化存入內存中就是它的序列化方式。

Redis本身提供了以下幾種序列化的方式:

  • GenericToStringSerializer: 可以將任何對象泛化為字符串并序列化
  • Jackson2JsonRedisSerializer: 跟JacksonJsonRedisSerializer實際上是一樣的 <---- 我們要換成這個
  • JacksonJsonRedisSerializer: 序列化object對象為json字符串
  • JdkSerializationRedisSerializer: 序列化java對象【默認的】
  • StringRedisSerializer: 簡單的字符串序列化 JSON 方式序列化成字符串,存儲到 Redis 中 。我們查看的時候比較直觀
package com.study.study_demo_of_spring_boot.redis_study.config;import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
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.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;/*** <p>* 功能描述:redis 序列化配置類* </p>** @author cui haida* @date 2024/04/13/19:52*/
@Configuration
public class RedisConfig {/*** 創建并配置RedisTemplate,用于操作Redis數據庫。** @param factory Redis連接工廠,用于創建Redis連接。* @return 配置好的RedisTemplate對象,可以用于執行Redis操作。*/@Bean(name = "redisTemplate")public RedisTemplate<String, Object> getRedisTemplate(RedisConnectionFactory factory) {RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();redisTemplate.setConnectionFactory(factory);// 配置Key的序列化方式為StringRedisSerializerStringRedisSerializer stringRedisSerializer = new StringRedisSerializer();redisTemplate.setKeySerializer(stringRedisSerializer);// 配置Value的序列化方式為Jackson2JsonRedisSerializerJackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);ObjectMapper objectMapper = new ObjectMapper();objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);jackson2JsonRedisSerializer.setObjectMapper(objectMapper);redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);// 配置Hash的Key和Value的序列化方式redisTemplate.setHashKeySerializer(stringRedisSerializer);redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);// 初始化RedisTemplateredisTemplate.afterPropertiesSet();return redisTemplate;}
}

業務類調用

import io.swagger.annotations.ApiOperation;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.*;
import tech.pdai.springboot.redis.lettuce.entity.User;
import tech.pdai.springboot.redis.lettuce.entity.response.ResponseResult;import javax.annotation.Resource;@RestController
@RequestMapping("/user")
public class UserController {// 注意:這里@Autowired是報錯的,因為@Autowired按照類名注入的@Resourceprivate RedisTemplate<String, User> redisTemplate;/*** @param user user param* @return user*/@ApiOperation("Add")@PostMapping("add")public ResponseResult<User> add(User user) {redisTemplate.opsForValue().set(String.valueOf(user.getId()), user);return ResponseResult.success(redisTemplate.opsForValue().get(String.valueOf(user.getId())));}/*** @return user list*/@ApiOperation("Find")@GetMapping("find/{userId}")public ResponseResult<User> edit(@PathVariable("userId") String userId) {return ResponseResult.success(redisTemplate.opsForValue().get(userId));}
}
2.5:數據類封裝

RedisTemplate中的操作和方法眾多,為了程序保持方法使用的一致性,屏蔽一些無關的方法以及對使用的方法進一步封裝。

import org.springframework.data.redis.core.RedisCallback;import java.util.Collection;
import java.util.Set;/*** 可能只關注這些方法*/
public interface IRedisService<T> {void set(String key, T value);void set(String key, T value, long time);T get(String key);void delete(String key);void delete(Collection<String> keys);boolean expire(String key, long time);Long getExpire(String key);boolean hasKey(String key);Long increment(String key, long delta);Long decrement(String key, long delta);void addSet(String key, T value);Set<T> getSet(String key);void deleteSet(String key, T value);T execute(RedisCallback<T> redisCallback);
}

RedisService的實現類

import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import tech.pdai.springboot.redis.lettuce.enclosure.service.IRedisService;import javax.annotation.Resource;
import java.util.Collection;
import java.util.Set;
import java.util.concurrent.TimeUnit;@Service
public class RedisServiceImpl<T> implements IRedisService<T> {@Resourceprivate RedisTemplate<String, T> redisTemplate;@Overridepublic void set(String key, T value, long time) {redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);}@Overridepublic void set(String key, T value) {redisTemplate.opsForValue().set(key, value);}@Overridepublic T get(String key) {return redisTemplate.opsForValue().get(key);}@Overridepublic void delete(String key) {redisTemplate.delete(key);}@Overridepublic void delete(Collection<String> keys) {redisTemplate.delete(keys);}@Overridepublic boolean expire(String key, long time) {return redisTemplate.expire(key, time, TimeUnit.SECONDS);}@Overridepublic Long getExpire(String key) {return redisTemplate.getExpire(key, TimeUnit.SECONDS);}@Overridepublic boolean hasKey(String key) {return redisTemplate.hasKey(key);}@Overridepublic Long increment(String key, long delta) {return redisTemplate.opsForValue().increment(key, delta);}@Overridepublic Long decrement(String key, long delta) {return redisTemplate.opsForValue().increment(key, -delta);}@Overridepublic void addSet(String key, T value) {redisTemplate.opsForSet().add(key, value);}@Overridepublic Set<T> getSet(String key) {return redisTemplate.opsForSet().members(key);}@Overridepublic void deleteSet(String key, T value) {redisTemplate.opsForSet().remove(key, value);}@Overridepublic T execute(RedisCallback<T> redisCallback) {return redisTemplate.execute(redisCallback);}
}

RedisService的調用

@RestController
@RequestMapping("/user")
public class UserController {@Autowiredprivate IRedisService<User> redisService;//...
}

二:集成redisson

1:單機可重入鎖

redisson-spring-boot-starter依賴于與最新版本的spring-boot兼容的redisson-spring數據模塊。

redisson-spring-data module namespring boot version
redisson-spring-data-161.3.y
redisson-spring-data-171.4.y
redisson-spring-data-181.5.y
redisson-spring-data-2x2.x.y
redisson-spring-data-3x3.x.y
<!-- redisson -->
<dependency><groupId>org.redisson</groupId><artifactId>redisson-spring-boot-starter</artifactId><version>3.16.2</version>
</dependency>
package com.study.study_demo_of_spring_boot.redis_study.config;import lombok.Data;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;import java.io.IOException;/*** <p>* 功能描述:redis client 配置* </p>** @author cui haida* @date 2024/04/14/7:24*/
@Configuration
@ConfigurationProperties(prefix = "spring.redis")
@Data
public class MyRedissonConfig {private String host;private int port;@Bean(destroyMethod = "shutdown")RedissonClient redisson() throws IOException {Config config = new Config();config.useSingleServer().setAddress("redis://" + host + ":" + port);return Redisson.create(config);}
}

加鎖解鎖測試

package com.study.study_demo_of_spring_boot.redis_study.use;import com.study.study_demo_of_spring_boot.redis_study.config.MyRedissonConfig;
import com.study.study_demo_of_spring_boot.redis_study.util.RedisUtil;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;import java.util.concurrent.TimeUnit;/*** <p>* 功能描述:redis test* </p>** @author cui haida* @date 2024/04/14/7:28*/
@RunWith(SpringRunner.class)
@SpringBootTest
public class UseTest {@Autowiredprivate RedisUtil redisUtil;@Autowiredprivate RedissonClient redissonClient;@Testpublic void redisNormalTest() {redisUtil.set("name", "張三");}@Testpublic void redissonTest() {RLock lock = redissonClient.getLock("global_lock_key");try {System.out.println(lock);// 加鎖30mslock.lock(30, TimeUnit.MILLISECONDS);if (lock.isLocked()) {System.out.println("獲取到了");} else {System.out.println("未獲取到");}} catch (Exception e) {e.printStackTrace();} finally {if (lock.isHeldByCurrentThread()) {lock.unlock();System.out.println("解鎖成功");}}}
}
  • lock.lock()即沒有指定鎖的過期時間,就是用30s,即看門狗的默認時間,只要占鎖成功,就會啟動一個定時任務,每隔10秒就會自動續期到30秒。
  • lock.lock(10, TimeUnit.xxx),默認鎖的過期時間就是我們指定的時間。

2:紅鎖(Red Lock)

紅鎖其實就是對多個redission節點同時加鎖

2.1:環境準備

用docker啟動三個redis實例,模擬redLock

docker run -itd  # -d 后臺啟動, -it shell交互--name redlock-1  # 這個容器的名稱-p 6380:6379 # 端口映射 redis的6379 <-> 容器的6380映射redis:7.0.8 # 鏡像名稱,如果沒有下載對應的redis鏡像,將會先進行拉取--requirepass 123456 # redis密碼123456
docker run -itd --name redlock-2 -p 6381:6379 redis:7.0.8 --requirepass 123456
docker run -itd --name redlock-3 -p 6382:6379 redis:7.0.8 --requirepass 123456

在這里插入圖片描述

2.2:配置編寫
package com.study.study_demo_of_spring_boot.redis_study.config;import lombok.Data;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;import java.io.IOException;/*** <p>* 功能描述:redis client 配置* </p>** @author cui haida* @date 2024/04/14/7:24*/
@Configuration
@ConfigurationProperties(prefix = "spring.redis")
@Data
public class MyRedissonConfig {private String host;private int port;@Bean(name = "normalRedisson", destroyMethod = "shutdown")RedissonClient redisson() {Config config = new Config();config.useSingleServer().setAddress("redis://" + host + ":" + port);return Redisson.create(config);}@Bean(name = "redLock1")RedissonClient redissonClient1(){Config config = new Config();config.useSingleServer().setAddress("redis://ip:6380").setDatabase(0).setPassword("123456");return Redisson.create(config);}@Bean(name = "redLock2")RedissonClient redissonClient2(){Config config = new Config();config.useSingleServer().setAddress("redis://ip:6381").setDatabase(0).setPassword("123456");return Redisson.create(config);}@Bean(name = "redLock3")RedissonClient redissonClient3(){Config config = new Config();config.useSingleServer().setAddress("redis://ip:6382").setDatabase(0).setPassword("123456");return Redisson.create(config);}
}
2.3:使用測試
package com.study.study_demo_of_spring_boot.redis_study.use;import com.study.study_demo_of_spring_boot.redis_study.config.MyRedissonConfig;
import com.study.study_demo_of_spring_boot.redis_study.util.RedisUtil;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.redisson.RedissonRedLock;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;import java.util.concurrent.TimeUnit;/*** <p>* 功能描述:* </p>** @author cui haida* @date 2024/04/14/7:28*/
@RunWith(SpringRunner.class)
@SpringBootTest
public class UseTest {@Autowired@Qualifier("redLock1")private RedissonClient redLock1;@Autowired@Qualifier("redLock2")private RedissonClient redLock2;@Autowired@Qualifier("redLock3")private RedissonClient redLock3;@Testpublic void redLockTest() {RLock lock1 = redLock1.getLock("global_lock_key");RLock lock2 = redLock2.getLock("global_lock_key");RLock lock3 = redLock3.getLock("global_lock_key");// 三個構成red lockRedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);//定義獲取鎖標志位,默認是獲取失敗boolean isLockBoolean = false;try {// 等待獲取鎖的最長時間。如果在等待時間內無法獲取鎖,并且沒有其他鎖釋放,則返回 false。如果 waitTime < 0,則無限期等待,直到獲得鎖定。int waitTime = 1;// 就是redis key的過期時間,鎖的持有時間,可以使用 ttl  查看過期時間。int leaseTime = 20;// 如果在持有時間結束前鎖未被釋放,則鎖將自動過期,沒有進行key續期,并且其他線程可以獲得此鎖。如果 leaseTime = 0,則鎖將永久存在,直到被顯式釋放。isLockBoolean = redLock.tryLock(waitTime, leaseTime, TimeUnit.SECONDS);System.out.printf("線程:"+Thread.currentThread().getId()+",是否拿到鎖:" +isLockBoolean +"\n");if (isLockBoolean) {System.out.println("線程:"+Thread.currentThread().getId() + ",加鎖成功,進入業務操作");try {//業務邏輯,40s模擬,超過了key的過期時間TimeUnit.SECONDS.sleep(40);} catch (InterruptedException e) {e.printStackTrace();}}} catch (Exception e) {System.err.printf("線程:"+Thread.currentThread().getId()+"發生異常,加鎖失敗");e.printStackTrace();} finally {// 無論如何,最后都要解鎖redLock.unlock();}System.out.println(isLockBoolean);}
}

3:讀寫鎖

基于Redis的Redisson分布式可重入讀寫鎖RReadWriteLock

Java對象實現了java.util.concurrent.locks.ReadWriteLock接口。其中讀鎖和寫鎖都繼承了RLock接口。

分布式可重入讀寫鎖允許同時有多個讀鎖和一個寫鎖處于加鎖狀態。

3.1:讀鎖lock.readLock()
@GetMapping("/read")
public String readValue() {// 聲明一個可重入讀寫鎖RReadWriteLock lock = redissonClient.getReadWriteLock("rw-lock");String s = "";//加讀鎖RLock rLock = lock.readLock();rLock.lock();try {System.out.println("讀鎖加鎖成功"+Thread.currentThread().getId());                                           // 拿到values = redisTemplate.opsForValue().get("writeValue");Thread.sleep(30000);} catch (Exception e) {e.printStackTrace();} finally {// 解鎖rLock.unlock();System.out.println("讀鎖釋放"+Thread.currentThread().getId());}return  s;
}
3.2:寫鎖lock.writeLock()
@GetMapping("/write")
public String writeValue(){// 獲取一把鎖RReadWriteLock lock = redissonClient.getReadWriteLock("rw-lock");String s = "";// 加寫鎖RLock rLock = lock.writeLock();try {//1、改數據加寫鎖,讀數據加讀鎖rLock.lock();System.out.println("寫鎖加鎖成功..."+Thread.currentThread().getId());s = UUID.randomUUID().toString();Thread.sleep(30000);// 寫入redis中redisTemplate.opsForValue().set("writeValue",s);} catch (Exception e) {e.printStackTrace();} finally {rLock.unlock();System.out.println("寫鎖釋放"+Thread.currentThread().getId());}return  s;
}
  • 先加寫鎖,后加讀鎖,此時并不會立刻給數據加讀鎖,而是需要等待寫鎖釋放后,才能加讀鎖
  • 先加讀鎖,再加寫鎖:有讀鎖,寫鎖需要等待
  • 先加讀鎖,再加讀鎖:并發讀鎖相當于無鎖模式,會同時加鎖成功

只要有寫鎖的存在,都必須等待,寫鎖是一個排他鎖,只能有一個寫鎖存在,讀鎖是一個共享鎖,可以有多個讀鎖同時存在

源碼在這里:https://blog.csdn.net/meser88/article/details/116591953

4:Semaphore和countDownLatch

4.1:Semaphore

基本使用

基于RedisRedisson的分布式信號量(Semaphore

Java對象RSemaphore采用了與java.util.concurrent.Semaphore相似的接口和用法

Semaphore是信號量,可以設置許可的個數,表示同時允許多個線程使用這個信號量(acquire()獲取許可)

  • 如果沒有許可可用就線程阻塞,并且通過AQS進行排隊
  • 可以使用release()釋放許可,當某一個線程釋放了某一個許可之后,將會從AQS中依次喚醒,直到沒有空閑許可。
@Test
public void semaphoreTest() throws InterruptedException {RSemaphore semaphore = redissonClient.getSemaphore("semaphore");// 同時最多允許3個線程獲取鎖semaphore.trySetPermits(3);for(int i = 0; i < 10; i++) {new Thread(() -> {try {System.out.println(new Date() + ":線程[" + Thread.currentThread().getName() + "]嘗試獲取Semaphore鎖");semaphore.acquire();System.out.println(new Date() + ":線程[" + Thread.currentThread().getName() + "]成功獲取到了Semaphore鎖,開始工作");Thread.sleep(3000);semaphore.release();System.out.println(new Date() + ":線程[" + Thread.currentThread().getName() + "]釋放Semaphore鎖");} catch (Exception e) {e.printStackTrace();}}).start();}Thread.sleep(5000);
}

源碼分析 - trySetPermits

@Override
public boolean trySetPermits(int permits) {return get(trySetPermitsAsync(permits));
}@Override
public RFuture<Boolean> trySetPermitsAsync(int permits) {RFuture<Boolean> future = commandExecutor.evalWriteAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,"local value = redis.call('get', KEYS[1]); " +"if (value == false or value == 0) then "+ "redis.call('set', KEYS[1], ARGV[1]); "+ "redis.call('publish', KEYS[2], ARGV[1]); "+ "return 1;"+ "end;"+ "return 0;",Arrays.asList(getRawName(), getChannelName()), permits);// other....完成的時候打日志
}
  1. get semaphore,獲取到一個當前的值
  2. 第一次數據為0, 然后使用set semaphore 3,將這個信號量同時能夠允許獲取鎖的客戶端的數量設置為3
  3. 然后發布一些消息,返回1

源碼分析 -> acquire

@Override
public void acquire(int permits) throws InterruptedException {// try - acquire ?if (tryAcquire(permits)) {return;}RFuture<RedissonLockEntry> future = subscribe();commandExecutor.syncSubscriptionInterrupted(future);try {while (true) {if (tryAcquire(permits)) {return;}future.getNow().getLatch().acquire();}} finally {unsubscribe(future);}// get(acquireAsync(permits));
}@Override
public boolean tryAcquire(int permits) {return get(tryAcquireAsync(permits));
}@Override
public RFuture<Boolean> tryAcquireAsync(int permits) {if (permits < 0) {throw new IllegalArgumentException("Permits amount can't be negative");}if (permits == 0) {return RedissonPromise.newSucceededFuture(true);}return commandExecutor.evalWriteAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,"local value = redis.call('get', KEYS[1]); " +"if (value ~= false and tonumber(value) >= tonumber(ARGV[1])) then " +"local val = redis.call('decrby', KEYS[1], ARGV[1]); " +"return 1; " +"end; " +"return 0;",Collections.<Object>singletonList(getRawName()), permits);
}
  1. get semaphore,獲取到一個當前的值,比如說是3,3 > 1
  2. decrby semaphore 1,將信號量允許獲取鎖的客戶端的數量遞減1,變成2
  3. decrby semaphore 1
  4. decrby semaphore 1
  5. 執行3次加鎖后,semaphore值為0

此時如果再來進行加鎖則直接返回0,然后進入死循環去獲取鎖

源碼分析 -> release

@Override
public RFuture<Void> releaseAsync(int permits) {if (permits < 0) {throw new IllegalArgumentException("Permits amount can't be negative");}if (permits == 0) {return RedissonPromise.newSucceededFuture(null);}RFuture<Void> future = commandExecutor.evalWriteAsync(getRawName(), StringCodec.INSTANCE, RedisCommands.EVAL_VOID,"local value = redis.call('incrby', KEYS[1], ARGV[1]); " +"redis.call('publish', KEYS[2], value); ",Arrays.asList(getRawName(), getChannelName()), permits);if (log.isDebugEnabled()) {future.onComplete((o, e) -> {if (e == null) {log.debug("released, permits: {}, name: {}", permits, getName());}});}return future;
}
  1. incrby semaphore 1,每次一個客戶端釋放掉這個鎖的話,就會將信號量的值累加1,信號量的值就不是0了
4.2:閉鎖CountDownLatch

基于RedissonRedisson分布式閉鎖(CountDownLatch

Java對象RCountDownLatch采用了與java.util.concurrent.CountDownLatch相似的接口和用法。

countDownLatch是計數器,可以設置一個數字,一個線程如果調用countDownLatch的await()將會發生阻塞

其他的線程可以調用countDown()對數字進行減一,數字成為0之后,阻塞的線程就會被喚醒。

底層原理就是,調用了await()的方法會利用AQS進行排隊。一旦數字成為0。AQS中的內容將會被依次喚醒。

@Test
public void countDownLatchTest() throws InterruptedException {RCountDownLatch latch = redissonClient.getCountDownLatch("anyCountDownLatch");latch.trySetCount(3);System.out.println(new Date() + ":線程[" + Thread.currentThread().getName() + "]設置了必須有3個線程執行countDown,進入等待中。。。");for(int i = 0; i < 3; i++) {new Thread(() -> {try {System.out.println(new Date() + ":線程[" + Thread.currentThread().getName() + "]在做一些操作,請耐心等待。。。。。。");Thread.sleep(3000);RCountDownLatch localLatch = redissonClient.getCountDownLatch("anyCountDownLatch");localLatch.countDown();System.out.println(new Date() + ":線程[" + Thread.currentThread().getName() + "]執行countDown操作");} catch (Exception e) {e.printStackTrace();}}).start();}// 一等多模型,主線程阻塞等子線程執行完畢,將countdown -> 0,主線程才能往下走latch.await();System.out.println(new Date() + ":線程[" + Thread.currentThread().getName() + "]收到通知,有3個線程都執行了countDown操作,可以繼續往下走");
}

先分析 trySetCount() 方法邏輯:

@Override
public RFuture<Boolean> trySetCountAsync(long count) {return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,"if redis.call('exists', KEYS[1]) == 0 then "+ "redis.call('set', KEYS[1], ARGV[2]); "+ "redis.call('publish', KEYS[2], ARGV[1]); "+ "return 1 "+ "else "+ "return 0 "+ "end",Arrays.<Object>asList(getName(), getChannelName()), newCountMessage, count);
}
  1. exists anyCountDownLatch,第一次肯定是不存在的
  2. set redisson_countdownlatch__channel__anyCountDownLatch 3
  3. 返回1

接著分析 latch.await()方法

@Override
public void await() throws InterruptedException {if (getCount() == 0) {return;}RFuture<RedissonCountDownLatchEntry> future = subscribe();try {commandExecutor.syncSubscriptionInterrupted(future);while (getCount() > 0) {// waiting for open statefuture.getNow().getLatch().await();}} finally {unsubscribe(future);}
}

這個方法其實就是陷入一個while true死循環,不斷的get anyCountDownLatch的值

如果這個值還是大于0那么就繼續死循環,否則的話呢,就退出這個死循環

最后分析 localLatch.countDown()方法

@Override
public RFuture<Void> countDownAsync() {return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,"local v = redis.call('decr', KEYS[1]);" +"if v <= 0 then redis.call('del', KEYS[1]) end;" +"if v == 0 then redis.call('publish', KEYS[2], ARGV[1]) end;",Arrays.<Object>asList(getName(), getChannelName()), zeroCountMessage);
}

decr anyCountDownLatch,就是每次一個客戶端執行countDown操作,其實就是將這個cocuntDownLatch的值遞減1

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/web/67163.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/67163.shtml
英文地址,請注明出處:http://en.pswp.cn/web/67163.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

STM32使用VScode開發

文章目錄 Makefile形式創建項目新建stm項目下載stm32cubemx新建項目IED makefile保存到本地arm gcc是編譯的工具鏈G++配置編譯Cmake +vscode +MSYS2方式bilibiliMSYS2 統一環境配置mingw32-make -> makewindows環境變量Cmake CmakeListnijia 編譯輸出elfCMAKE_GENERATOR查詢…

Oracle 12c 中的 CDB和PDB的啟動和關閉

一、簡介 Oracle 12c引入了多租戶架構&#xff0c;允許一個容器數據庫&#xff08;Container Database, CDB&#xff09;托管多個獨立的可插拔數據庫&#xff08;Pluggable Database, PDB&#xff09;。本文檔旨在詳細描述如何啟動和關閉CDB及PDB。 二、容器數據庫 (CDB) 2.1…

網絡仿真工具Core環境搭建

目錄 安裝依賴包 源碼下載 Core安裝 FAQ 下載源碼TLS出錯誤 問題 解決方案 找不到dbus-launch 問題 解決方案 安裝依賴包 調用以下命令安裝依賴包 apt-get install -y ca-certificates git sudo wget tzdata libpcap-dev libpcre3-dev \ libprotobuf-dev libxml2-de…

FPGA實現任意角度視頻旋轉(二)視頻90度/270度無裁剪旋轉

本文主要介紹如何基于FPGA實現視頻的90度/270度無裁剪旋轉&#xff0c;旋轉效果示意圖如下&#xff1a; 為了實時對比旋轉效果&#xff0c;采用分屏顯示進行處理&#xff0c;左邊代表旋轉前的視頻在屏幕中的位置&#xff0c;右邊代表旋轉后的視頻在屏幕中的位置。 分屏顯示的…

JavaEE:多線程進階

JavaEE&#xff1a;多線程進階 一、對比不同鎖策略之間的應用場景及其區別1. 悲觀鎖 和 樂觀鎖1.1 定義和原理1.2 應用場景1.3 示例代碼 2. 重量級鎖 和 輕量級鎖2.1 定義和原理2.2 應用場景2.3 示例代碼 3. 掛起等待鎖 和 自旋鎖3.1 定義和原理3.2 應用場景3.3 示例代碼 4. 幾…

董事會辦公管理系統的需求設計和實現

該作者的原創文章目錄&#xff1a; 生產制造執行MES系統的需求設計和實現 企業后勤管理系統的需求設計和實現 行政辦公管理系統的需求設計和實現 人力資源管理HR系統的需求設計和實現 企業財務管理系統的需求設計和實現 董事會辦公管理系統的需求設計和實現 公司組織架構…

pytest自動化測試 - pytest夾具的基本概念

<< 返回目錄 1 pytest自動化測試 - pytest夾具的基本概念 夾具可以為測試用例提供資源(測試數據)、執行預置條件、執行后置條件&#xff0c;夾具可以是函數、類或模塊&#xff0c;使用pytest.fixture裝飾器進行標記。 1.1 夾具的作用范圍 夾具的作用范圍&#xff1a; …

esp32-C3 實現DHT11(溫濕度)

安裝DHT傳感器庫&#xff1a; 在Arduino IDE中&#xff0c;進入項目 > 加載庫 > 管理庫。搜索DHT sensor library并安裝。 編寫代碼 定義引腳和傳感器類型初始化傳感器判斷傳感器是否正常讀取數據 源碼 #include <DHT.h> #include <DHT_U.h>// 定義DHT傳感器…

java構建工具之Gradle

自定義任務 任務定義方式&#xff0c;總體分為兩大類:一種是通過 Project 中的task()方法,另一種是通過tasks 對象的 create 或者register 方法。 //任務名稱,閉包都作為參數println "taskA..." task(A,{ }) //閉包作為最后一個參數可以直接從括號中拿出來println …

【Pytest】生成html報告中,中文亂碼問題解決方案

import pytestif __name__ "__main__":# 只運行 tests 目錄下的測試用例&#xff0c;并生成 HTML 報告pytest.main([-v, -s, --htmlreport.html, tests])可以以上方式生成&#xff0c;也可以在pytest.ini中設置 [pytest] addopts --htmlreport.html --self-contai…

MyBatis最佳實踐:提升數據庫交互效率的秘密武器

第一章&#xff1a;框架的概述&#xff1a; MyBatis 框架的概述&#xff1a; MyBatis 是一個優秀的基于 Java 的持久框架&#xff0c;內部對 JDBC 做了封裝&#xff0c;使開發者只需要關注 SQL 語句&#xff0c;而不關注 JDBC 的代碼&#xff0c;使開發變得更加的簡單MyBatis 通…

《Java程序設計》課程考核試卷

一、單項選擇題&#xff08;本大題共10個小題&#xff0c;每小題2分&#xff0c;共20分&#xff09; 1.下列用來編譯Java源文件為字節碼文件的工具是&#xff08; &#xff09;。 A.java B.javadoc C.jar D.javac 2…

【25考研】人大計算機考研復試該怎么準備?有哪些注意事項?

人大畢竟是老牌985&#xff0c;復試難度不會太低&#xff01;建議同學認真復習&#xff01;沒有機試還是輕松一些的&#xff01; 一、復試內容 由公告可見&#xff0c;復試包含筆試及面試&#xff0c;沒有機試&#xff01; 二、參考書目 官方無給出參考書目&#xff0c;可參照…

vue3中Teleport的用法以及使用場景

1. 基本概念 Teleport 是 Vue3 提供的一個內置組件&#xff0c;它可以將組件的內容傳送到 DOM 樹的任何位置&#xff0c;而不受組件層級的限制。這在處理模態框、通知、彈出菜單等需要突破組件層級限制的場景中特別有用。 1.1 基本語法 <template><teleport to&quo…

使用openwrt搭建ipsec隧道

背景&#xff1a;最近同事遇到了個ipsec問題&#xff0c;做的ipsec特性&#xff0c;ftp下載ipv6性能只有100kb, 正面定位該問題也蠻久了&#xff0c;項目沒有用openwrt, 不過用了開源組件strongswan, 加密算法這些也是內核自帶的&#xff0c;想著開源的不太可能有問題&#xff…

基于AnolisOS 8.6安裝GmSSL 3.1.1及easy_gmssl庫測試國密算法

測試環境 Virtual Box&#xff0c;AnolisOS-8.6-x86_64-minimal.iso&#xff0c;4 vCPU, 8G RAM, 60 vDisk。最小化安裝。需聯網。 系統環境 關閉防火墻 systemctl stop firewalld systemctl disable firewalld systemctl status firewalld selinux關閉 cat /etc/selinux/co…

HTML從入門到精通:鏈接與圖像標簽全解析

系列文章目錄 01-從零開始學 HTML&#xff1a;構建網頁的基本框架與技巧 02-HTML常見文本標簽解析&#xff1a;從基礎到進階的全面指南 03-HTML從入門到精通&#xff1a;鏈接與圖像標簽全解析 文章目錄 系列文章目錄前言一、鏈接與圖像標簽&#xff08;HTML 標簽基礎&#xff…

[STM32 - 野火] - - - 固件庫學習筆記 - - -十一.電源管理系統

一、電源管理系統簡介 電源管理系統是STM32硬件設計和系統運行的基礎&#xff0c;它不僅為芯片本身提供穩定的電源&#xff0c;還通過多種電源管理功能優化功耗、延長電池壽命&#xff0c;并確保系統的可靠性和穩定性。 二、電源監控器 作用&#xff1a;保證STM32芯片工作在…

數字圖像處理:實驗六

uu們&#xff01;大家好&#xff0c;2025年的新年就要到來&#xff0c;咸魚哥在這里祝大家在2025年每天開心快樂&#xff0c;天天掙大錢&#xff0c;自由自在&#xff0c;健健康康&#xff0c;萬事如意&#xff01;&#xff08;要是咸魚哥嘴笨的話&#xff0c;還望大家多多包涵…

Langchain+文心一言調用

import osfrom langchain_community.llms import QianfanLLMEndpointos.environ["QIANFAN_AK"] "" os.environ["QIANFAN_SK"] ""llm_wenxin QianfanLLMEndpoint()res llm_wenxin.invoke("中國國慶日是哪一天?") print(…