文章目录
一、环境搭建
SpringBoot 2.2.5 + Redis-x64-3.2
1、导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2、配置文件
这里有一个小问题,到了SpringBoot 2.x之后,Redis客户端使用的就是Lettuce,但是使用Lettuce配置连接池会报一个有关于pool2的错误,没细看,可能是要导pool2的依赖什么的,知道的评论告诉我一下,谢谢。我还是使用了jedis,感觉没什么问题。
spring:
redis:
host: localhost
port: 6379
database: 0
jedis:
pool:
max-active: 8
二、Redis做点赞功能
因为redis的底层使用二进制存储的,所以使用setbit和getbit方法。这样开销更小,运行速度也胜于关系型数据库。
//点赞会话类
public class Talk {
private Integer newsId;
private Integer userId;
private Long count;
private Boolean status;
public Talk(Integer newsId,Integer userId){
this.newsId=newsId;
this.userId=userId;
}
}
/**
* 点赞部分实现代码
*/
public Boolean isLike(Talk talk){
return (Boolean) redisTemplate.execute(new RedisCallback<Boolean>() {
@Override
public Boolean doInRedis(RedisConnection redisConnection) throws DataAccessException {
Boolean heart=redisConnection.getBit(talk.getNewsId().toString().getBytes(),talk.getUserId());
redisConnection.close();
return heart;
}
});
}
public Long countLike(Talk talk){
Object execute = redisTemplate.execute(new RedisCallback<Long>() {
@Override
public Long doInRedis(RedisConnection redisConnection) throws DataAccessException {
Long count = redisConnection.bitCount(talk.getNewsId().toString().getBytes());
redisConnection.close();
return count;
}
});
return (Long) execute;
}
public Boolean likeAndCancelLike(Talk talk){
Boolean execute=true;
try{
redisTemplate.execute(new RedisCallback<Boolean>() {
@Override
public Boolean doInRedis(RedisConnection redisConnection) throws DataAccessException {
Boolean aBoolean=redisConnection.setBit(talk.getNewsId().toString().getBytes(),talk.getUserId(),
(talk.getStatus()==false?true:false));
if(talk.getStatus()==false){
talk.setStatus(true);
talk.setCount(talk.getCount()+1);
}else{
talk.setStatus(false);
talk.setCount(talk.getCount()-1);
}
redisConnection.close();
return aBoolean;
}
});
}catch(Exception e){
execute=false;
}
return execute;
}
语法:setbit key offset
setbit会为数据库添加一段二进制码,新闻id作为key,用户id作为偏移量,可以唯一对应这段二进制码的某一位,此位设定0为dislike,1为like。获取点赞总数使用count(key)统计1的个数即可。
因为直接使用的二进制,可以使用redisConnection而不是redisTemplate。有一点要注意,setbit在(key or offset?)已经存在的情况下会返回false,所以不报错就设定为setbit成功。
三、Redis的ZSet做点击记录和点击排行
因为ZSet是有序表,redis内置的方法也适合做这个(详细内容看这里),所以使用ZSet作为数据结构。ZSet结构基于跳跃表和哈希表,这个说一下。
这里由于不再是直接操作二进制了,所以我们不使用redisConnection,改为使用SpringBoot封装的redisTemplate。可是,这样就出现了一个问题,我们使用Redis Dasktop Manager会发现存储的是二进制码,没有可读性。这是因为这是SpringBoot默认的Jdkxxxx一大堆的那个序列化器,所以我们需要对redisTemplate进行自定义的序列化配置。
这是亲测在SpringBoot 2.2.5 版本可用的配置方法,但只是自定义序列化的配置,不是用来做缓存的配置,后面会细讲。
package com.ssm.news.config;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
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;
@Configuration
public class MyRedisConfig {
@Bean
public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory){
RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
// 使用Jackson2JsonRedisSerialize 替换默认序列化
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
//objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
// 设置value的序列化规则和 key的序列化规则
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}
另一个问题,查找topNews插入式总报错null
原因:topNews一开始没有初始化,而且注意,List的初始化需要用ArrayList。
@Scheduled用来设定定时刷新任务。
@Autowired
private NewsMapper newsMapper;
@Autowired
private RedisTemplate redisTemplate;
List<News> topNews;
//新闻top榜(前3位)
@Scheduled(cron = "*/10 * * * * 0-7") //cron以空格分隔
public void refreshTopNews(){
Boolean execute;
Long fieldNum = redisTemplate.opsForZSet().zCard("click");
if(fieldNum<=0){
execute=false;
return ;
}else{
/**
* 问题记录:List的初始化方法不同以往,但一定要初始化!!!!这才是一直报null的原因。
* 用静态变量修饰topNews也不行。
*/
topNews=new ArrayList<>();
Set<Integer> range = redisTemplate.opsForZSet().reverseRange("click",0,2);
for(Integer i:range){
topNews.add(newsMapper.getNewsByNewsId(i));
}
}
//使用RedisConnection
/*Object execute = redisTemplate.execute(new RedisCallback<Set<byte[]>>() {
@Override
public Set<byte[]> doInRedis(RedisConnection redisConnection) throws DataAccessException {
Long aLong = redisConnection.zCard("click".getBytes());
if(aLong<=0) {
redisConnection.close();
return null;
}
else{
Set<byte[]> bytes = redisConnection.zRevRange("click".getBytes(), 0, 2);
redisConnection.close();
return bytes;
}
}
});*/
}
四、Redis做缓存
整合Spring-Cache的注解进行。
1、导入依赖
Redis我们上面已经导过了,如果没有导Redis的话,系统会默认使用currentMap来存储缓存数据。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
2、Spring缓存抽象
Application->CachingProvider(管理缓存管理器)->CacheManger(管理缓存)->Cache(缓存组件,直接对缓存进行crud操作,且每个缓存组件有 唯一 的名字)->Entry<key,value>(实体)->Enpiry(过期时间)
3、缓存注解
keyGenerator:缓存key的生成策略
serializer:缓存value的序列化策略
@EnableCaching:开启基于注解的缓存
@Cacheable:主要针对方法配置,根据方法参数对结果进行缓存(调用时机:先看缓存,再看方法)
cacheName/value:指定缓存组件的名字
key:缓存组件使用的key,默认使用方法参数,可编写SpEL表达式
keyGenerator:key生成器,自己指定生成器组件的id
keyGenerator和key二选一。
cacheManager:指定缓存管理器;cacheResolver:指定获取缓存解析器
condition:指定缓存前的判断条件
unless:与condition相反,可以获取到方法结果进行缓存
sync:是否使用异步
@CachePut:既调用方法,又更新缓存,大多用于更新操作(调用时机:先调方法,再更新缓存)
@CacheEvict:清空缓存
allEntries:默认为false,为true时清空cache组件中的所有缓存,慎用
beforeInvocation:默认为false,表示缓存的清除是否在方法之前
@Caching:able、put、evict的整合
4、实际应用
我们已经用Redis写了点赞、点击的功能,现在是缓存。
简单一点,对登录的用户进行缓存,保存到名字为cacheName的Cache组件中。
package com.ssm.news.service;
import com.ssm.news.mapper.UserMapper;
import com.ssm.news.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
//登录
/**
* @Cacheable 支持SpEL表达式
* 执行顺序:先根据cacheName找缓存组件cache,再根据key或者keyGenerator生成key来查找缓存实体
* 找不到再进入方法查找,最后根据condition判断是否加入缓存
* @param loginName
* @param password
* @return
*/
@Cacheable(cacheNames = "user",keyGenerator = "myKeyGenerator")
//keyGenerator是我自定义的key生成器
public User toLogin(String loginName,String password){
User user=userMapper.selectUser(loginName,password);
System.out.println("调用方法了...");
return user;
}
}
如果我们不进行CacheManager配置的话,就会出现这个情况。key由于生成器没有显示字节码,但是value就毫无可读性。
这就需要我们配置RedisCacheManager,还是写在MyRedisConfig中,加上之后MyRedisConfig就变成了这样
package com.ssm.news.config;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ssm.news.pojo.User;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
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.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.lang.reflect.Method;
import java.net.UnknownHostException;
import java.time.Duration;
import java.util.Arrays;
@Configuration
public class MyRedisConfig {
@Bean
public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory){
RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
// 使用Jackson2JsonRedisSerialize 替换默认序列化
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
//objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
// 设置value的序列化规则和 key的序列化规则
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
//本来是打算用来配置user对象的序列化
/*@Bean
public RedisTemplate<Object, User> userRedisTemplate(
RedisConnectionFactory redisConnectionFactory)
throws UnknownHostException {
RedisTemplate<Object, User> template = new RedisTemplate<Object,User>();
template.setConnectionFactory(redisConnectionFactory);
Jackson2JsonRedisSerializer<User> userJackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<User>(User.class);
template.setDefaultSerializer(userJackson2JsonRedisSerializer);
return template;
}*/
@Bean
public RedisCacheManager userRedisCacheManager(RedisConnectionFactory redisConnectionFactory) {
RedisCacheConfiguration cacheConfiguration =
RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofDays(1)) // 设置缓存过期时间为一天
.disableCachingNullValues() // 禁用缓存空值,不缓存null校验
.serializeKeysWith(RedisSerializationContext //设置键的序列化方式
.SerializationPair.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext // 设置CacheManager的值序列化方式为json序列化,value中要存什么类型的值,就在参数值写什么样的值
.SerializationPair.fromSerializer(new Jackson2JsonRedisSerializer<User>(User.class)));
return RedisCacheManager.builder(redisConnectionFactory).cacheDefaults(cacheConfiguration).build(); // 设置默认的cache组件
}
//自定义key生成器
@Bean("myKeyGenerator")
public KeyGenerator keyGenerator(){
return new KeyGenerator(){
@Override
public Object generate(Object o, Method method, Object... params) {
return method.getName()+'['+ Arrays.asList(params).toString()+']';
}
};
}
}
User类也要实现Serializable接口,哪个类要序列化,哪个类就实现接口。
public class User implements Serializable {
private Integer userId;
private String userName;
private String loginName;
private String password;
private String tel;
//private Date registerTime;
private String status;
private Integer roleId;
}
在RedisCacheManager中,对key和value的序列化分别配置StringRedisSerializer()和new Jackson2JsonRedisSerializer(User.class)),Redis数据库中value就变为了json格式。
其实这些禁用缓存空值、缓存key前缀也可以在配置文件中配置。
双击shift搜索RedisSerializer接口,再点击ctrl+h也能查看redis支持的所以序列化方式
最后,问题来了,前面写点击的时候不是配置了序列化吗?为什么这里还要在缓存管理器配置序列化?
我们之前在redisTemplate中配置的序列化,只有在使用redisTemplate对redis数据库进行插入查找操作时,才会将数据进行相应的序列化。然而,使用缓存时,我们并没有配置序列化,所以数据都是字节码形式。
同样的,如果我们是需要向redis数据库中插入User类型的数据时,就定义一个会对User类型进行序列化的userRedisTemplate,在通过这个userRedisTemplate对数据库进行插入操作即可。
这只是SpringBoot 2.x的定义方法,在1.x中,RedisCacheManager的定义方法这完全不一样。