新闻网站问题总结————Redis篇(点赞、点击排行、缓存)

一、环境搭建

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就毫无可读性。
redis截图
这就需要我们配置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前缀也可以在配置文件中配置。
redis数据库截图
双击shift搜索RedisSerializer接口,再点击ctrl+h也能查看redis支持的所以序列化方式
在这里插入图片描述
最后,问题来了,前面写点击的时候不是配置了序列化吗?为什么这里还要在缓存管理器配置序列化?

我们之前在redisTemplate中配置的序列化,只有在使用redisTemplate对redis数据库进行插入查找操作时,才会将数据进行相应的序列化。然而,使用缓存时,我们并没有配置序列化,所以数据都是字节码形式。

同样的,如果我们是需要向redis数据库中插入User类型的数据时,就定义一个会对User类型进行序列化的userRedisTemplate,在通过这个userRedisTemplate对数据库进行插入操作即可。

这只是SpringBoot 2.x的定义方法,在1.x中,RedisCacheManager的定义方法这完全不一样。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值