Springboot 集成redis

释放双眼,带上耳机,听听看~!

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库中查询:
Springboot 集成redis
Springboot 集成redis

@Cacheable, CachePut支持缓存击穿

  1. 实际上注解的方式做缓存,是有防止缓存击穿的保护机制的,当在PG库中查询不到对应的值时,会保存空值到redis中。Springboot 集成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实现

  1. redis中基本命令setnx,即当key值不存在时,才可以设置成功,所以可以使用setnx来实现分布式锁。

  2. 由于使用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

给TA打赏
共{{data.count}}人
人已打赏
安全技术

C++ lambda表达式

2022-1-11 12:36:11

安全资讯

硅谷和伦敦也能看到小黄车了? ofo称将出海

2016-12-23 20:58:47

个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索