Springboot系列文章(九):添加redis,优化查询速度以及分布式锁的应用场景

在查询的时候,我们会遇到因为数据量太大,查询的数据太多,导致加载速度缓慢。给用户带来不好的体验,那么有什么方法可以解决吗?这里我们会演示两种解决方案。

分页

数据库里有多少数据,就查询多少。比如我数据库里面的数据有1千条。查询用了812ms
在这里插入图片描述

那么还有什么方法可以把查询速度提高吗?给用户更好的体验。

最能想到的方法,就是给其增加分页,限制数据量的查询。
1.使用分页,24ms,每页查询8条数据。
在这里插入图片描述

那么分页可以解决问题,为什么还要使用redis呢?看使用场景,数据量不大的情况,我们可以通过分页来解决,减少redis的成本,如果查询的数据很多,尤其是像一些数据统计,那么分页就使用不了,这个时候我们就可以使用缓存机制,redis

redis

引入依赖

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
            <version>2.6.4</version>
        </dependency>

编写配置

spring:
  redis:
    port: 6379
    host: localhost
    database: 0

接下来我们先测试一下是否可以往redis里面存储数据

1.我们使用RedisTemplate可以对redis进行操作。

 @Test
    public void testRedis(){
        //现在,我们来测试一下,如何往redis里面存储数据
        //1.使用redisTemplate获得操作redis的功能,opsForValue表示使用string数据结构类型
        ValueOperations opsForValue = redisTemplate.opsForValue();
        //2.增加数据
        opsForValue.set("name","zwl");
        opsForValue.set("age",25);
        User user = new User();
        user.setUserAccount("asdfasdf");
        opsForValue.set("user",user);
        //2.查询数据
        String name = (String)opsForValue.get("name");
        System.out.println(name);
        Object age = opsForValue.get("age");
        System.out.println(age);
        User user1 = (User)opsForValue.get("user");
        System.out.println(user1);


    }

打印:

在这里插入图片描述
我们看看redis里面是如何存储这些数据的。这里我们可以使用QuickRedis可视化工具看一下我们存储的数据,我们发现数据有点奇怪,有乱码。

在这里插入图片描述

使用命令行也查询不了数据。
在这里插入图片描述

问题肯定是落在这个乱码这里。因为我们使用了它默认的序列化器,它会默认给我们的key加入一些东西,所以我们借助一些其他工具进行查询的时候就有问题。

接下来我们可以使用自定义的序列化器。

我们发现使用StringRedisTemplate ,而不是用RedisTemplate,问题得到了解决。但是存储的内容也就只能局限string类型。

 @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Test
    public void testRedis(){
        //现在,我们来测试一下,如何往redis里面存储数据
        //1.使用redisTemplate获得操作redis的功能,opsForValue表示使用string数据结构类型
        ValueOperations opsForValue = stringRedisTemplate.opsForValue();
        //2.增加数据
        opsForValue.set("name","zwl");
        //2.查询数据
        String name = (String)opsForValue.get("name");
        System.out.println(name);
        Object age = opsForValue.get("age");
        System.out.println(age);
        User user1 = (User)opsForValue.get("user");
        System.out.println(user1);


    }

在这里插入图片描述

所以,我们也可以参考StringRedisTemplate 使用的是什么序列化器。然后对默认的进行修改。

@Configuration
public class RedisTemplateConfig {

    @Bean
    public RedisTemplate<String ,Object> redisTemplate(RedisConnectionFactory redisConnectionFactory){

        RedisTemplate<String ,Object> redisTemplate = new RedisTemplate();
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        //设置key序列化器
        //设置类型
        redisTemplate.setKeySerializer(RedisSerializer.string());
        return redisTemplate;
    }
}

在这里插入图片描述

数据有了。也可以取出来。

那么缓存应该在什么时候使用呢?接下来我们会演示一个例子,比如在查询用户数据的时候,查询一千条,看查询的耗时。

  @GetMapping("/recommend")
    public BaseResponse<Page<User>> recommendUsers(long pageSize, long pageNum, HttpServletRequest request){
        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
        /**
         * 增加缓存。 怎么理解呢???????
         * 1.首先查询缓存,如果缓存中没有,则从数据库中查询,并且存储一份到缓存,
         *  1.1 每个用户的推荐页面可能都不一样,所以我们需要根据用户的id来进行存储缓存
         * 2.如果缓存中有了。则从缓存中开始查询
         */
        User userData = getCurrentUser(request).getData();
        String key = String.format("pao:user:recommend:%s",userData.getId());
        ValueOperations opsForValue = redisTemplate.opsForValue();

        Object result = opsForValue.get(key);

        if (result!=null) {
            System.out.println("----查询redis");
            Page<User> userList = (Page<User>) result;
            return ResultUtils.success(userList);
        }
        System.out.println("----查询mysql");
        Page<User> userList = userService.page(new Page<>(pageNum, pageSize), queryWrapper);
        try{
            //缓存如果出现问题,不要影响数据库查询
            opsForValue.set(key,userList);
        }catch (Exception e){
            log.error("redis set key error",e);
        }

        return ResultUtils.success(userList);
    }

可以看到第一次的时候是140ms,第二次以后30ms,甚至20ms
在这里插入图片描述
在这里插入图片描述
上面的代码还存在一些问题。就是这个内容没有过期,那么每次用户取到的数据都是不变的,并且对我们redis内存是占用的。所以我们要加上过期时间。如何设置呢?比如下面我们设置100s

 opsForValue.set(key,userList,100000, TimeUnit.MILLISECONDS);

但是第一次还是很慢,有没有什么方法可以解决这个问题,可以提供过提前缓存,模拟有个人提前缓存过。

缓存预热

虽然缓存预热可以帮助我们解决第一次加载慢的问题,但使用缓存预热也需要考虑一些问题。

  1. 数据更新不及时。
  2. 应该什么时候使用缓存预热呢?
  3. 会占用资源,如果用户没有访问的情况下。

一般,我们会选择一个重点客户进行缓存预热,比如老板账户,根据实际场景进行缓存预热,比如数据统计,1,2个小时统计也可以(根据自己的业务场景进行调整。)

这里我们会用到定时任务。

开启定时任务注解

@SpringBootApplication
@EnableScheduling
public class UserCenterApplication {

    public static void main(String[] args) {
        SpringApplication.run(UserCenterApplication.class, args);
    }

}

开启定时任务


@Component
@Slf4j
public class PreCacheJob {

    @Resource
    private UserService userService;
    @Resource
    private RedisTemplate redisTemplate;

    private List<Long> idList = Arrays.asList(1L);

    @Scheduled(cron = "0 55 * * * ? ") // 秒 分钟 小时 日 月 周 年
    public void doCacheRecommendUser(){
        //根据id,查询指定的用户出来,然后进行数据缓存。
        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
        for (Long id : idList) {
            String key = String.format("pao:user:recommend:%s",id);
            ValueOperations opsForValue = redisTemplate.opsForValue();
            Page<User> userList = userService.page(new Page<>(1, 20), queryWrapper);
            try{
                //缓存如果出现问题,不要影响数据库查询
                opsForValue.set(key,userList,100000, TimeUnit.MILLISECONDS);
            }catch (Exception e){
                log.error("redis set key error",e);
            }
        }

    }
}

那么我们第一次获取数据也会很快。

那么到这里,大家觉得写法上面还存在什么问题???加入我部署了两个这样的项目,有些情况下,我们可以部署多个项目到服务器的。那么这个定时任务,是不是会执行两遍。有一些大公司可能会附属个100台,那么消耗量就极大了,如果这个定时任务不是查询,而是增加,那么就乱套了。

解决方案,需要用到锁,分布式锁。

分布式锁

但为什么要使用分布式锁呢,为什么不用锁呢?众所周知,普通锁只对同一个jvm有效,如果你有两台服务器,运行在不同的环境中,也就是你本来一个商场,只有一个厕所,只能一个人先上,现在两个商场了,那么就是两个厕所,就可以两个人上,所以肯定是不行。而分布式锁,则是把厕所变为一个,只有一个人能用。

具体实现,我们需要借助Redission,Redisson是一个java操作Redis的客户端,提供了大量的分布式数据集来简化对Redis的操作和使用,可以让开发者像使用本地集合一样使用Redis,完全感知不到Redis的存在。

     <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.18.0</version>
        </dependency>
/**
 * Redisson 配置
 */
@Configuration
@ConfigurationProperties(prefix = "spring.redis")
@Data
public class RedissonConfig {

    private String host;
    private String port;

    @Bean
    public RedissonClient redissonClient(){
        //1、创建配置
        Config config = new Config();
        String redisAddress = String.format("redis://%s:%s", host, port);
        config.useSingleServer().setAddress(redisAddress).setDatabase(0);

        //2.创建实例
        RedissonClient redisson = Redisson.create(config);
        return redisson;
    }
}

@Component
@Slf4j
public class PreCacheJob {

    @Resource
    private UserService userService;
    @Resource
    private RedisTemplate redisTemplate;
    
    @Resource
    private RedissonClient redissonClient;

    private List<Long> idList = Arrays.asList(1L);

    @Scheduled(cron = "0 55 * * * ? ") // 秒 分钟 小时 日 月 周 年
    public void doCacheRecommendUser(){
        RLock lock = redissonClient.getLock("pao:precachejob:docache:lock");
        try {
            if (lock.tryLock(0,30000, TimeUnit.MILLISECONDS)) {
                //根据id,查询指定的用户出来,然后进行数据缓存。
                for (Long id : idList) {
                    QueryWrapper<User> queryWrapper = new QueryWrapper<>();
                    String key = String.format("pao:user:recommend:%s",id);
                    ValueOperations opsForValue = redisTemplate.opsForValue();
                    Page<User> userList = userService.page(new Page<>(1, 20), queryWrapper);
                    try{
                        //缓存如果出现问题,不要影响数据库查询
                        opsForValue.set(key,userList,100000, TimeUnit.MILLISECONDS);
                    }catch (Exception e){
                        log.error("redis set key error",e);
                    }
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
}

其实这里还有问题,如果这个方法执行的实现很长,比如超过了30秒,那么锁不是会被释放吗?那么其他服务器的也还是会执行。那么怎么办呢?我们就需要用到这个自动续期机制,也就是给锁续过期时间,redisson默认是30秒的过期时间,每10秒续期一次。那么如何修改呢?如下,这里修改为-1即可。

@Scheduled(cron = "0 55 * * * ? ") // 秒 分钟 小时 日 月 周 年
    public void doCacheRecommendUser(){
        RLock lock = redissonClient.getLock("pao:precachejob:docache:lock");
        try {
            if (lock.tryLock(0,-1, TimeUnit.MILLISECONDS)) {
                //根据id,查询指定的用户出来,然后进行数据缓存。
                for (Long id : idList) {
                    QueryWrapper<User> queryWrapper = new QueryWrapper<>();
                    String key = String.format("pao:user:recommend:%s",id);
                    ValueOperations opsForValue = redisTemplate.opsForValue();
                    Page<User> userList = userService.page(new Page<>(1, 20), queryWrapper);
                    try{
                        //缓存如果出现问题,不要影响数据库查询
                        opsForValue.set(key,userList,100000, TimeUnit.MILLISECONDS);
                    }catch (Exception e){
                        log.error("redis set key error",e);
                    }
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }

可以看到锁,注意需要睡眠的方式,才能看到锁,如果使用断电,则不会有看到哈。这个需要注意的。
在这里插入图片描述
好了。今天的分享就到这里,希望能对大家有所帮助

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Spring Boot提供了非常简便的方式来使用Redis作为分布式锁的实现。要使用Redis分布式锁,你需要完成以下步骤: 1. 首先,确保你的Spring Boot项目中已经引入了Redis的依赖。 2. 创建一个RedisTemplate实例,用于与Redis进行交互。你可以通过在application.properties或application.yml文件中配置相关的Redis连接信息来完成这一步。 3. 创建一个分布式锁的类,可以命名为RedisLock。这个类应该包含获取锁和释放锁的方法。 4. 获取锁的方法可以使用Redis的setnx命令(set if not exists),该命令可以在Redis中设置一个key-value键值对,只有在该key不存在时才会设置成功。你可以将这个key设置为全局唯一的锁标识,value可以是当前线程的标识符。 5. 为了避免死锁,你可以设置一个过期时间(expire time)来自动释放锁。你可以使用Redis的expire命令来为锁设置过期时间。 6. 释放锁的方法可以使用Redis的del命令来删除锁标识对应的键值对。 在使用分布式锁时,你可以先尝试获取锁,如果成功获取到锁,则执行需要加锁的代码逻辑,执行完成后再释放锁。如果获取锁失败,则可以选择等待一段时间后重新尝试获取锁,或者直接放弃执行。 需要注意的是,使用Redis作为分布式锁的实现并不是绝对安全的,可能会存在一些问题,比如锁超时、锁误删等。因此在实际应用中,你需要根据具体的场景和需求来选择最适合的分布式锁方案。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值