Springboot 集成redis
-
本地安装redis
-
redis 在springboot中的基本配置
-
application.yaml中基本配置:
- redisConfig bean配置
-
RedisConnectionFactory Bean
-
使用redis做缓存
-
注解方式使用redis做缓存
-
cacheManager Bean
* Controller
* 返回对象需要序列化
* 更新操作需要与创建(查询)操作返回类型一致
* 测试结果
* @Cacheable, CachePut支持缓存击穿- RedisTemplate方式做缓存
-
redisTemlate Bean
* 基本使用方法
* 缓存穿透 -
缓存空值
* Bloom Filter- 查找redis中key值
-
使用keys pattern
* 使用scan cursor [MATCH pattern] [COUNT count] -
使用redis做分布式锁
-
使用setnx实现
- Redisson
-
添加maven依赖
* 使用Redsson Lock
许多工程中都需要引入redis作为缓存来减轻SQL数据库的压力,本文简单介绍了工程中集成redis的基本方法,包括使用Spring提供的@Cacheable, @CachePut注解的方式,以及直接使用redisTemplate来操作redis库的方式来做缓存,并简单讲述了缓存空值和布隆过滤器的使用。
本地安装redis
在此不赘述。
redis 在springboot中的基本配置
application.yaml中基本配置:
1
2
3
4
5
6
7
8
9
10
11
12 1spring:
2 redis:
3 host: 127.0.0.1
4 port: 6379
5 timeout: 3000
6 pool:
7 max-active: 8
8 max-wait: -1
9 max-idle: 8
10 min-idle: 0
11
12
redisConfig bean配置
RedisConnectionFactory Bean
与1中的redis配置相对应,需要在Redis的Configuration文件中配置RedisConnectionFactory
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30 1 @Value("${spring.redis.host}")
2 private String host;
3 @Value("${spring.redis.port}")
4 private int port;
5 @Value("${spring.redis.timeout}")
6 private int timeout;
7 @Value("${spring.redis.pool.max-active}")
8 private int maxActive;
9 @Value("${spring.redis.pool.max-wait}")
10 private int maxWait;
11 @Value("${spring.redis.pool.max-idle}")
12 private int maxIdle;
13 @Value("${spring.redis.pool.min-idle}")
14 private int minIdle;
15
16 @Bean
17 public JedisConnectionFactory redisConnectionFactory() {
18 JedisConnectionFactory factory = new JedisConnectionFactory();
19 factory.setHostName(host);
20 factory.setPort(port);
21 factory.setTimeout(timeout);
22 factory.setPassword(password);
23 factory.getPoolConfig().setMaxIdle(maxIdle);
24 factory.getPoolConfig().setMinIdle(minIdle);
25 factory.getPoolConfig().setMaxTotal(maxActive);
26 factory.getPoolConfig().setMaxWaitMillis(maxWait);
27 return factory;
28 }
29
30
使用redis做缓存
注解方式使用redis做缓存
cacheManager Bean
1
2
3
4
5
6
7
8
9
10
11 1 @Bean
2 public CacheManager cacheManager(RedisConnectionFactory factory) {
3 RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofSeconds(300)); //缓存在redis的生存时间
4 Set<String> cacheNames = new HashSet<>();
5 cacheNames.add("user"); //Append a {@link Set} of cache names to be pre initialized with current {@link RedisCacheConfiguration}. 具体表现为cache 在redis中的key值前缀,形如 user::xxxxxx
6 cacheNames.add("auth-user");
7 return RedisCacheManager.builder(RedisCacheWriter.nonLockingRedisCacheWriter(factory))
8 .cacheDefaults(redisCacheConfiguration).initialCacheNames(cacheNames).build();
9 }
10
11
Controller
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 1 @RequestMapping("/redis-get/{id}")
2 @Cacheable(cacheNames = "user", key = "#id")
3 public User redisGet(@PathVariable int id) {
4 log.info("real process.");
5 return pgService.getUser(id);
6 }
7
8 @PostMapping("/redis-update")
9 @CachePut(cacheNames = "user", key = "#user.id")
10 public User redisUpdate(@RequestBody User user) {
11 log.info("update real process.");
12 pgService.updateUser(user);
13 return user;
14 }
15
16
返回对象需要序列化
返回值必须要序列化,否则抛异常。
1
2
3
4
5
6
7
8
9
10
11 12020-03-27 17:18:16.986 ERROR 32488 --- [nio-8184-exec-3] c.h.m.u.RestControllerExceptionAdviser : exception happen.
2
3org.springframework.data.redis.serializer.SerializationException: Cannot serialize; nested exception is org.springframework.core.serializer.support.SerializationFailedException: Failed to serialize object using DefaultSerializer; nested exception is java.lang.IllegalArgumentException: DefaultSerializer requires a Serializable payload but received an object of type [com.hxb.mybatis.entity.User]
4 at org.springframework.data.redis.serializer.JdkSerializationRedisSerializer.serialize(JdkSerializationRedisSerializer.java:96) ~[spring-data-redis-2.2.1.RELEASE.jar:2.2.1.RELEASE]
5 ... 63 common frames omitted
6Caused by: java.lang.IllegalArgumentException: DefaultSerializer requires a Serializable payload but received an object of type [com.hxb.mybatis.entity.User]
7 at org.springframework.core.serializer.DefaultSerializer.serialize(DefaultSerializer.java:43) ~[spring-core-5.2.3.RELEASE.jar:5.2.3.RELEASE]
8 at org.springframework.core.serializer.support.SerializingConverter.convert(SerializingConverter.java:63) ~[spring-core-5.2.3.RELEASE.jar:5.2.3.RELEASE]
9 ... 65 common frames omitted
10
11
更新操作需要与创建(查询)操作返回类型一致
更新操作需要与创建(查询)操作返回类型一致才能做更新。
测试结果
多次触发rest,发现只有首次触发rest才会去pg库中查询:
@Cacheable, CachePut支持缓存击穿
- 实际上注解的方式做缓存,是有防止缓存击穿的保护机制的,当在PG库中查询不到对应的值时,会保存空值到redis中。
RedisTemplate方式做缓存
redisTemlate Bean
主要需要指定ObjectMapper
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 1 @Bean
2 public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory factory) {
3 StringRedisTemplate template = new StringRedisTemplate(factory);
4 setSerializer(template); //设置ObjectMapper
5 template.afterPropertiesSet();
6 return template;
7 }
8
9 private void setSerializer(StringRedisTemplate template) {
10 Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
11 ObjectMapper om = new ObjectMapper();
12 om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
13 om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
14 jackson2JsonRedisSerializer.setObjectMapper(om);
15 template.setValueSerializer(jackson2JsonRedisSerializer);
16 }
17
18
基本使用方法
在需要操作Redis处,直接注入RedisTemplate即可。
1
2
3
4
5
6
7
8
9
10
11
12 1 @Autowired
2 private RedisTemplate redisTemplate;
3
4 public void set(User user) {
5 redisTemplate.opsForValue().set("user_" + user.getId(), user);
6 }
7
8 public User get(int key) {
9 return (User) redisTemplate.opsForValue().get("user_" + key);
10 }
11
12
缓存穿透
使用redisTemplate的方式直接操作redis需要用户在业务逻辑中自行加入缓存击穿控制的逻辑。一般两种方式:缓存空值,Bloom Filter
缓存空值
缓存空值,即在PG库中查询不到数据时,回写空值到redis库中,生存时间可以设置稍小于合法值的生存时间。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28 1 private boolean isExistsInDb(String authName, String password) {
2 Auth specifyAuthFromRedis = redisService.getSpecifyAuth(authName);
3
4 if (specifyAuthFromRedis == null) {
5 Auth specifyAuthFromPg = pgService.getSpecifyAuth(authName);
6 if (specifyAuthFromPg == null) {
7 log.warn("Auth {} does not exist in both redis and pg, insert null into redis to protect pg.", authName);
8 updateRedis(authName, null, 60);
9 return false;
10 }
11 if (specifyAuthFromPg.getPassword() != null && password.equals(specifyAuthFromPg.getPassword())) {
12 log.debug("Auth {} exists in pg, insert into redis.", authName);
13 updateRedis(authName, password, 120);
14 return true;
15 }
16 log.debug("wrong password.");
17 return false;
18 } else {
19 if (Objects.isNull(specifyAuthFromRedis.getPassword())) {
20 log.warn("DB protection.");
21 return false;
22 } else {
23 return password.equals(specifyAuthFromRedis.getPassword());
24 }
25 }
26 }
27
28
Bloom Filter
Bloom Filter是将用户key值映射到位数组LockFreeBitArray中,相较于用户自行维护一个HashTable可以大大减少内存。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 1@Service
2@Slf4j
3public class BloomFilterUtil {
4 private BloomFilter<String> bloomFilter = BloomFilter.create(Funnels.stringFunnel(Charset.defaultCharset()), 10000);
5
6 public boolean put(String key) {
7 log.debug("put key {}.", key);
8 return bloomFilter.put(key);
9 }
10
11 public boolean exists(String key) {
12 log.debug("check exists of key {}.", key);
13 return bloomFilter.mightContain(key);
14 }
15}
16
17
查找redis中key值
使用keys pattern
1
2
3
4
5 1 private Set<String> getAuthKeys(String pattern) {
2 return redisTemplate.keys(pattern);
3 }
4
5
使用scan cursor [MATCH pattern] [COUNT count]
实际项目一般不允许使用keys pattern这种命令,原因在于redis的单线程,keys指令会导致线程阻塞一段时间,直到指令执行完毕,服务才能恢复。可以使用scan多次查询来减少阻塞时间。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 1public Set<String> scan(String pattern, int count) {
2 log.info("scan for pattern {}, count {}.", pattern, count);
3 RedisConnection redisConnection = redisTemplate.getConnectionFactory().getConnection();
4 Set<String> keySet = new HashSet<>();
5 Cursor<byte[]> cursor = redisConnection.scan(new ScanOptions.ScanOptionsBuilder().match(pattern).count(count).build());
6 while (cursor.hasNext()) {
7 keySet.add(new String(cursor.next()));
8 }
9 if (keySet.isEmpty()) {
10 log.warn("There is no matched key for [{}].", pattern);
11 }
12 return keySet;
13 }
14
15
使用redis做分布式锁
使用setnx实现
-
redis中基本命令setnx,即当key值不存在时,才可以设置成功,所以可以使用setnx来实现分布式锁。
-
由于使用setnx做锁时,释放锁的操作是直接使用delete(key)来实现的,所以为了防止B线程错误释放掉A线程锁持有的锁,在加锁过程中,每个线程可以携带唯一的UUID作为setnx的value,在释放操作时,使用该uuid先做判断,再释放。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46 1@Slf4j
2@Service
3public class RedisLockUtil {
4
5 private static final String LOCK_PREFIX = "lock:";
6
7 @Autowired
8 private RedisTemplate redisTemplate;
9
10 /**
11 * @param locKName redis lock name
12 * @param acquireTimeout max time for waiting lock
13 * @param timeout release lock time
14 * @return identifier
15 */
16 public String lock(String locKName, Duration acquireTimeout, Duration timeout) {
17 String identifier = UUID.randomUUID().toString();
18 String lockKey = LOCK_PREFIX + locKName;
19
20 long end = System.currentTimeMillis() + acquireTimeout.toMillis();
21 while (System.currentTimeMillis() < end) {
22 if (Objects.equals(redisTemplate.opsForValue().setIfAbsent(lockKey, identifier, timeout), Boolean.TRUE)) {
23 return identifier;
24 }
25 try {
26 Thread.sleep(acquireTimeout.toMillis() / 10);
27 } catch (InterruptedException e) {
28 Thread.currentThread().interrupt();
29 }
30 }
31 return null;
32 }
33
34 /**
35 * Unlock according to value equals.
36 */
37 public boolean releaseLock(String lockName, String identifier) {
38 String lockKey = LOCK_PREFIX + lockName;
39 if (identifier.equals(redisTemplate.opsForValue().get(lockKey))) {
40 return redisTemplate.delete(lockKey);
41 }
42 return false;
43 }
44}
45
46
Redisson
添加maven依赖
1
2
3
4
5
6
7
8 1 <!--redisson-->
2 <dependency>
3 <groupId>org.redisson</groupId>
4 <artifactId>redisson-spring-boot-starter</artifactId>
5 <version>3.10.4</version>
6 </dependency>
7
8
使用Redsson Lock
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23 1@Slf4j
2@Service
3public class RedisLockUtil {
4
5 private static final String LOCK_SUFFIX = "lock:";
6
7 @Autowired
8 private RedissonClient redissonClient;
9
10 public void lockByRedisson(String lockName, long leaseTime) {
11 RLock lock = redissonClient.getLock(LOCK_SUFFIX + lockName);
12 lock.lock(leaseTime, TimeUnit.SECONDS);
13 log.debug("acquire lock success.");
14 }
15
16 public void releaseRedissonLock(String lockName) {
17 RLock lock = redissonClient.getLock(LOCK_SUFFIX + lockName);
18 lock.unlock();
19 log.debug("release lock success.");
20 }
21}
22
23