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