springboot+redis实现文章浏览量记录

springboot+redis实现文章浏览量记录

​ 之前,个人博客网站里的文章访问量都是存在MySQL中的。每次访问都需要通过文章id查数据库,获得浏览量后+1操作。之后在更新数据库,为了实时显示文章浏览量还要在读一遍数据库。访问量多了之后就带来了线程安全问题:两个线程同时获得数量,各自+1后更新,这个访问量就有问题了。对于这个情况,之前直接用synchronized粗暴的对操作加锁。这带来一个问题,多个线程只能有一个线程来获取文章信息,其他线程阻塞,。后来又想到另一个解决方案,浏览量的增加独立出来,不和读取文章信息绑定。每读取一个文章信息后返回调用一个接口来增加数量。这有一个缺陷就是显示的文章访问量不是实时的。

​ 最后决定使用redis来存储文章的访问量,redis的关键语句是incr key。作用是key的value+1;key和文章id绑定。由于redis是单线程的,单个语句的执行是线程安全的。

​ 理想很丰满,但操作的时候还是遇到了不少问题。

		<dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-data-redisartifactId>
        dependency>

设置redis配置

redis:
    database: 0
    # Redis服务器地址 写你的ip
    host: XXX.XX.XX.XXX
    # Redis服务器连接端口
    port: 你的端口号
    # Redis服务器连接密码(默认为空)
    password: 你的密码
    timeout: 6000
    lettcue:
      pool:
        # 连接池中的最大空闲连接,默认值也是8。
        max-idle: 100
        # 连接池中的最小空闲连接,默认值也是0。
        min-idle: 10
        # 如果赋值为-1,则表示不限制;如果pool已经分配了maxActive个jedis实例,则此时pool的状态为exhausted(耗尽)
        max-active: 8
        # 等待可用连接的最大时间,单位毫秒,默认值为-1,表示永不超时。如果超过等待时间,则直接抛出JedisConnectionException
        max-wait: 2000

如果不设置redis的连接池属性的话默认只有一个连接,多线程来使用redisTemplate会报”redis连接异常“。

设置redisTemplate类模板


@Bean
    public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory connectionFactory) {
        // 配置redisTemplate
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(connectionFactory);
        redisTemplate.setKeySerializer(new StringRedisSerializer());//key序列化
        redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<Object>(Object.class));//value序列化
        return redisTemplate;
    }    

JUnit测试类:

 	@Resource
    RedisUtils redisUtils;
    @Test
    public void redisTest(){
        try {
            Runnable runnable = new Runnable(){
                @SneakyThrows
                @Override
                public void run() {
                    for(int i=0;i<500;i++){
                        Thread.sleep(1);
                        System.out.println(Thread.currentThread().getName()+": "+addReadNums());
                    }
                }
            };
            ExecutorService executorService = Executors.newFixedThreadPool(5);
            executorService.execute(runnable);//线程1
            executorService.execute(runnable);//线程2
            System.out.println("@Test线程执行完毕");
        } catch (Exception e) {
            System.out.println(e);
        }
    }
    public Long addReadNums(){
        return redisUtils.incrBy("test", 1);
    }

进行多线程模拟测试的时候就报错了,lettuce线程池在多线程下有问题,查找资料后都说用Jedis作为redis的线程池。

修改后

修改后的依赖:在spring-boot-starter-data-redis去除lettuce线程池,导入jedis


        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-data-redisartifactId>
            <exclusions>
                <exclusion>
                    <groupId>io.lettucegroupId>
                    <artifactId>lettuce-coreartifactId>
                exclusion>
            exclusions>
        dependency>
        <dependency>
            <groupId>org.apache.commonsgroupId>
            <artifactId>commons-pool2artifactId>
        dependency>
        <dependency>
            <groupId>redis.clientsgroupId>
            <artifactId>jedisartifactId>
            <version>3.3.0version>
        dependency>

这里要注意Jedis的版本和spring-boot-starter-data-redis版本,版本不一致会导致redisTemplate创建失败。

参考:【异常】nested exception is java.lang.NoClassDefFoundError: redis/clients/jedis/util/SafeEncoder

还要修改redisTemplate类创建方式

@Bean
public RedisTemplate<String, Object> redisTemplate(JedisConnectionFactory connectionFactory) {
    // 配置redisTemplate
    RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
    redisTemplate.setConnectionFactory(connectionFactory);
    redisTemplate.setKeySerializer(new StringRedisSerializer());//key序列化
    redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<Object>(Object.class));//value序列化
    return redisTemplate;
}

这时候测试发现,线程运行一会就打印:Shutting down ExecutorService ‘applicationTaskExecutor‘ 。之后线程就停止了。发现是JUnit单元测试的坑:它会在主线程结束后调用相关的System.exit()方法,将JVM关闭,所以,子线程被动挂了。

参考:JUnit单元测试中多线程的坑

修改后的测试类:

@Test
public void redisTest(){
    try {
        Runnable runnable = new Runnable(){
            @SneakyThrows
            @Override
            public void run() {
                for(int i=0;i<500;i++){
                    Thread.sleep(1);
                    System.out.println(Thread.currentThread().getName()+": "+addReadNums());
                }
            }
        };
        ExecutorService executorService = Executors.newFixedThreadPool(5);
        executorService.execute(runnable);//线程1
        executorService.execute(runnable);//线程2
        try {
            TimeUnit.SECONDS.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("@Test线程执行完毕");
    } catch (Exception e) {
        System.out.println(e);
    }
}
public Long addReadNums(){
    return redisUtils.incrBy("test", 1);
}
springboot+redis实现文章浏览量记录_第1张图片

发现两个线程下新增500还是没有问题的。

看一下redis确实是1000。

最后对于文章浏览量,只要key传入的是文章的id即可。如果担心key与其他的内容重复,添加标识符区别即可。

你可能感兴趣的:(Redis笔记,redis,spring,boot,数据库)