Redis

一:Redis的用途:

我觉得从这个项目的背景来分析吧

第一个项目场景:

我项目是伙伴匹配系统,伙伴匹配系统的主页面需要去查询用户

这里就会有个问题了

当我们的用户多了,我们应该如何去操作呢,还是全部查出来嘛,这肯定是不行的。

数据查询慢怎么办?

用缓存:提前把数据取出来保存好,我们都知道MySql的数据是存储在磁盘中的,所以,你每次去查都是直接在磁盘中查,就很慢

我们提前把数据查出来放到读写更快的介质中,这样就起到了提高效率的功能。

第二个项目场景:

第二个项目场景还是这个伙伴匹配系统

这一次的场景是什么呢?

是关于一个分布式登录的cookie共享

大家设想一个场景,一个项目如果做大了,用户比较多,那肯定不可能只部署在同一台服务器上,肯定要多服务器部署,那这个时候问题就来了,我们如果一开始访问的是服务器A,我们在服务器A上登录了,浏览器生成了一个cookie,我们可以在服务器A上登录了,然后假设有一天服务器A很多人突然一起访问,非常的拥挤,根据负载均衡,我们可能会访问服务器B,那这个时候问题就出现了,我们已经在服务器A上登录了,cookie已经种在了服务器A上,那我们从服务器B上登录,那还得再登录一边嘛,那如果一个项目同时部署在50台服务器上,那我们还得登录50遍嘛,这听起来就很离谱。

所以我们的解决办法就是:将用户信息存储在一个公共存储的地方 就是下面要说的Redis

第三个项目场景:

第三个项目场景是苍穹外卖

在苍穹外卖里面用Redis,其实就是获取了一个店铺的状态,这个和第一个项目场景的思路有点像,这里就不赘述了。

二:Redis入门:

1:Redis的概念:

我觉得可以先看这一篇,讲得很有意思,也可以对这个redis有一个初步得了解

Redis是什么-CSDN博客

我觉得这个redis得功能有点像cache,用来提高访问效率得东西。

2:Redis的安装:

indows版下载地址:https://github.com/microsoftarchive/redis/releases

直接解压

启动redis:

在当前文件夹下打开cmd,

然后输入redis-server.exe redis.windows.conf即可

关闭redis:ctrl+c

连接客户端服务: 

记住连接客户端服务之前别关那个启动redis的那个命令行窗口

要不然就会报:

Could not connect to Redis at 127.0.0.1:6379: 由于目标计算机积极拒绝,无法连接。

这个错误。

连接别的redis:

 先用exit命令退出客户端服务。

然后用redis-cli.exe -h (你要连接的地址) -p (端口号)

修改redis的密码:

在redis.windows.conf中配置密码:

小技巧可以在大段的文本文件中用ctrl+f快捷键来查找。

设置完密码之后,别人想连接我们的redis服务就需要密码了

 redis的可视化工具:

三:Redis常用的数据类型:

Redis存储的是key-value结构的数据,其中key是字符串类型,value有5种常用的数据类型:

  • 字符串 string
  • 哈希 hash
  • 列表 list
  • 集合 set
  • 有序集合 sorted set / zset

redis中的数据类型,特指的是value的数据类型。 

1:字符串操作命令:

Redis 字符串类型常用命令:

  • SET key value            设置指定key的值(像mysql中的insert)
  • GET key            获取指定key的值(像mysql中的select)
  • SETEX key seconds value    设置指定key的值,并将 key 的过期时间设为 seconds 秒(短信验证码)
  • SETNX key value        只有在 key 不存在时设置 key 的值(分布式锁)

稍微说一下最后一个命令:setnx ,这个命令如果成功(说明redis里面没有你输入的键值对)

结果会返回1,反之结果返回0 

2:哈希表操作命令:

Redis hash 是一个string类型的 field 和 value 的映射表,hash特别适合用于存储对象,常用命令:

  • HSET key field value     将哈希表 key 中的字段 field 的值设为 value
  • HGET key field     获取存储在哈希表中指定字段的值
  • HDEL key field        删除存储在哈希表中的指定字段
  • HKEYS key         获取哈希表中所有字段
  • HVALS key         获取哈希表中所有值

3:列表操作命令 :

Redis 列表是简单的字符串列表,按照插入顺序排序,

  • 常用命令: LPUSH key value1 [value2]     将一个或多个值插入到列表头部(左边)
  • LRANGE key start stop         获取列表指定范围内的元素
  • RPOP key             移除并获取列表最后一个元素(右边)
  • LLEN key             获取列表长度

 

 这里插入的时候有点类似于链表的头插法

用-1 表示最后一个元素的末尾。

rpop 弹出的是最先插入的元素 

 4:集合操作命令:

Redis set 是string类型的无序集合。集合成员是唯一的,集合中不能出现重复的数据,

常用命令:

  • SADD key member1 [member2]     向集合添加一个或多个成员
  • SMEMBERS key         返回集合中的所有成员
  • SCARD key             获取集合的成员数
  • SINTER key1 [key2]         返回给定所有集合的交集
  • SUNION key1 [key2]         返回所有给定集合的并集
  • SREM key member1 [member2]     删除集合中一个或多个成员

 

 5:有序集合操作命令:

Redis有序集合是string类型元素的集合,且不允许有重复成员。每个元素都会关联一个double类型的分数。

常用命令:

  • ZADD key score1 member1 [score2 member2]     向有序集合添加一个或多个成员
  • ZRANGE key start stop [WITHSCORES]         通过索引区间返回有序集合中指定区间内的成员 ZINCRBY key increment member             有序集合中对指定成员的分数加上增量 increment ZREM key member [member ...]             移除有序集合中的一个或多个成员

这整个排序可以看成一个大根堆。 

输出从大到小

6:通用命令:

 Redis的通用命令是不分数据类型的,都可以使用的命令:

  • KEYS pattern         查找所有符合给定模式( pattern)的 key
  • EXISTS key         检查给定 key 是否存在(存在返回1,不存在返回0)
  • TYPE key         返回 key 所储存的值的类型
  • DEL key         该命令用于在 key 存在是删除 key

四:在Java中操作Redis:

Spring Data Redis:

Spring Data Redis 是 Spring 的一部分,对 Redis 底层开发包进行了高度封装。 在 Spring 项目中,可以使用Spring Data Redis来简化操作。

使用方式:

  • 操作步骤: 导入Spring Data Redis 的maven坐标
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
  • 配置Redis数据源 编写配置类,
   redis:
    host: ${sky.redis.host}
    port: ${sky.redis.port}
    password: ${sky.redis.password}
    database: ${sky.redis.password}

具体的信息配置在另一个的配置环境中。 

  • 创建RedisTemplate对象
package com.sky.config;


import lombok.extern.slf4j.Slf4j;
import lombok.val;
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.StringRedisSerializer;

@Configuration
@Slf4j
public class RedisConfiguration {
    @Bean
    public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory){
        log.info("开始创建redis对象");
        RedisTemplate redisTemplate = new RedisTemplate();
        //设置redis的连接工厂对象
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        //设置redis key的序列化器
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        return redisTemplate;
    }
}

 Redis是默认有一个序列化器的,不过那个会导致写入Redis中的key为乱码

所以我们需要单独配置一个序列化器。

  • 通过RedisTemplate对象操作Redis
package com.sky;

import io.lettuce.core.ScriptOutputType;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.*;

@SpringBootTest
@Slf4j
public class SpringDateRedis {

    @Autowired
    private RedisTemplate redisTemplate;
    @Test
    public void testRedisTemplate(){
        System.out.println(redisTemplate);
        final ValueOperations valueOperations = redisTemplate.opsForValue();//字符串对象
        final HashOperations hashOperations = redisTemplate.opsForHash();//哈希对象
        final ListOperations listOperations = redisTemplate.opsForList();//列表对象
        final SetOperations setOperations = redisTemplate.opsForSet();//set集合对象
        final ZSetOperations zSetOperations = redisTemplate.opsForZSet();//zset有序集合对象
    }
}

创建了一个测试类,通过注入的方式获取了在IOC容器中的redistemplate对象

并且通过了这个redistemplate对象的五个方法不同获取redis中的五种不同的数据类型。

 在Java中操作Redis的字符串命令:

    /*
    测试字符串操作命令
     */
    @Test
    public void testString(){
        ValueOperations valueOperations = redisTemplate.opsForValue();
        //set
        valueOperations.set("city","北京");
        //get
        final String city = (String) valueOperations.get("city");
        System.out.println(city);
        //setex
        valueOperations.set("code","123489",3, TimeUnit.MINUTES);
        //setnx
        valueOperations.setIfAbsent("lock","1");
    }

在java中有些方法和redis中的这个操作命令名称不同,注意一下就好,效果没区别。

 在Java中操作Redis的哈希表命令: 

    /*
    测试哈希操作命令
     */
    @Test
    public void testHash(){
        final HashOperations hashOperations = redisTemplate.opsForHash();//哈希对象
        //hset
        hashOperations.put("id","name","小明");
        hashOperations.put("id","idcard","123");
        hashOperations.put("id","gender","男");
        //hget
        Object name = hashOperations.get("id", "name");
        System.out.println(name);
        //hdel
        hashOperations.delete("id","name");
        //hkeys
        final Set ids = hashOperations.keys("id");
        Iterator<Integer> iterator = ids.iterator();
        while(iterator.hasNext()){
            System.out.println(iterator.next());
        }
        //hvals
        for (Object id : hashOperations.values("id")) {
            System.out.println(id);
        }
    }

  在Java中操作Redis的列表命令: 

    /*
    测试列表操作命令
     */
    @Test
    public void testList(){
        final ListOperations listOperations = redisTemplate.opsForList();//列表对象
        //lpush
        listOperations.leftPushAll("mylist","a","b","c");
        //lrange
        final List mylist = listOperations.range("mylist", 0, -1);
        System.out.println(mylist);
        //rpop
        final Object o = listOperations.rightPop("mylist");
        System.out.println(o);
        //llen
        final Long len = listOperations.size("mylist");
        System.out.println(len);
    }

  在Java中操作Redis的集合命令: 

    /*
    测试结合操作命令
     */
    @Test
    public void testSet(){
        final SetOperations setOperations = redisTemplate.opsForSet();//set集合对象
        //sadd
        setOperations.add("set1","a","b","c");
        setOperations.add("set2","a","b","x","y");
        //smembers
        final Set set1 = setOperations.members("set1");
        System.out.println(set1);
        //scard
        final Long len = setOperations.size("set1");
        System.out.println(len);
        //sinter
        final Set intersect = setOperations.intersect("set1", "set2");
        System.out.println(intersect);
        //sunion
        final Set union = setOperations.union("set1", "set2");
        System.out.println(union);
        //srem
        setOperations.remove("set1","a");
        final Set newset1 = setOperations.members("set1");
        System.out.println(newset1);
    }

   在Java中操作Redis的有序集合命令: 

     /*
    有序集合操作命令
     */
    @Test
    public void TestZSet(){
        final ZSetOperations zSetOperations = redisTemplate.opsForZSet();//zset有序集合对象
        //zadd
        zSetOperations.add("zset1","a",10.0);
        zSetOperations.add("zset1","b",10.2);
        zSetOperations.add("zset1","c",10.5);
        //zrange
        final Set zset1 = zSetOperations.range("zset1", 0, -1);
        System.out.println(zset1);
        //zincrby
        zSetOperations.incrementScore("zset1","c",10);
        //zrem
        zSetOperations.remove("zset1","a","b");
        final Set zset2 = zSetOperations.range("zset1", 0, -1);
        System.out.println(zset2);
    }

   在Java中操作Redis的通用命令:

    /*
    通用命令
     */
    @Test
    public void TestCommon(){
        //keys pattern
        final Set keys = redisTemplate.keys("*");
        System.out.println(keys);
        //exists key
        final Boolean name = redisTemplate.hasKey("name");
        System.out.println(name);
        //typekey
        for(Object key:keys){
            final DataType type = redisTemplate.type(key);
            System.out.println(type);
        }
        //del key
        redisTemplate.delete("mylist");
    }

 五:数据查询(第一个项目场景):

当分析场景之后,写代码就会简单很多了。

分为下面的四步:

1:先获取当前用户id

2:根据id进行查找缓存

3:有缓存直接返回,没缓存再查找数据库

4:写缓存(try-catch 一下)

注意这里需要设置一下缓存的过期时间 


 

 为什么要设置缓存的过期时间?

不设置过期时间的话,就会导致缓存只进不出,当Redis满了以后,就会触发Redis的淘汰机制

根据Redis的算法自动淘汰掉一些数据,这些数据如果很有用,那结果就很麻烦了


 代码:

@GetMapping("/recommend")
    public BaseResponse<Page<User>> usersRecommend(long pageSize,long pageNum,HttpServletRequest request){
        log.info("主页推荐成员");
        /*
        1:先获取当前用户id
        2:根据id进行查找缓存
        3:有缓存直接返回,没缓存再查找数据库
        4:写缓存(try-catch 一下)
        */
        User user = userService.getLoginUser(request);
        final Long userId = user.getId();
        ValueOperations valueOperations = redisTemplate.opsForValue();
        String key = String.format("shayu:user:recommend:%s",userId);
        Page<User> userList = (Page<User>)valueOperations.get(key);
        if(userList != null){
            return ResultUtils.success(userList);
        }
        QueryWrapper<User> queryWrapper = new QueryWrapper();
        Page userPage = new Page<User>(pageNum,pageSize);
        userList = userService.page(userPage,queryWrapper);
        try {
            valueOperations.set(key,userList,30000, TimeUnit.MILLISECONDS);
        } catch (Exception e) {
            log.info(String.valueOf(e));
        }
        return ResultUtils.success(userList);
    }

这个时候根据逻辑我们也可以分析出一个问题:

就是当我们查找不到这个用户id的时候,我们是不是还是得数据库中读取

那如果你是第一个用户,那你是不是一定访问不到数据,所以这里还能进行优化

缓存预热: 

问题:第一个用户访问还是很慢(加入第一个老板),也能一定程度上保护数据库

缓存预热的优点:

  1. 解决上面的问题,可以让用户始终访问很快

缺点:

  1. 增加开发成本(你要额外的开发、设计)
  2. 预热的时机和时间如果错了,有可能你缓存的数据不对或者太老
  3. 需要占用额外空间
怎么缓存预热?
  1. 定时
  2. 模拟触发(手动触发)
实现

用定时任务,每天刷新所有用户的推荐列表

注意点:

  1. 缓存预热的意义(新增少、总用户多)比如你这个系统里面有10000个人,每次都新增20个,那这样就可以理解为新增少,总用户多。
  2. 缓存的空间不能太大,要预留给其他缓存空间
  3. 缓存数据的周期(此处每天一次)

定时的方法实现缓存预热:

用这个定时的方法实现需要用到spring中的一个工具(我不知道工具这个形容是否准确,spring整合了一个定时框架)

Spring Scheduler(spring boot 默认整合了)

具体使用:

Spring Task及订单状态定时处理_java实现订单状态自动修改-CSDN博客

我这边直接贴代码:

package com.usercenter.usercenterproject.task;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.usercenter.usercenterproject.Pojo.ResultUtils;
import com.usercenter.usercenterproject.Pojo.User;
import com.usercenter.usercenterproject.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

/**
 * 预热缓存
 */
@Component
@Slf4j
public class PrecacheTask {

    @Resource
    private RedisTemplate redisTemplate;

    @Autowired
    private UserService userService;
    List<Integer> mainUserList = new ArrayList<>();

    @Scheduled(cron = "0 12 1 * * *")
    public void precachetask(){
        mainUserList.add(5,6);
        for (Integer userId : mainUserList) {
            ValueOperations valueOperations = redisTemplate.opsForValue();
            String key = String.format("shayu:user:recommend:%s",userId);
            QueryWrapper<User> queryWrapper = new QueryWrapper();
            Page userPage = new Page<User>(1,20);
            Page<User> userList = userService.page(userPage,queryWrapper);
            try {
                valueOperations.set(key,userPage,30000, TimeUnit.MILLISECONDS);
            } catch (Exception e) {
                log.info(String.valueOf(e));
            }
        }
    }
}

提醒一下:启动类添加注解 @EnableScheduling

六:分布式共享Cookie(第二个项目场景):

这个项目场景说的天花乱坠,实际代码实现起来只需要配置一下就行。

先导入maven坐标:

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

 修改spring-session 存储配置 

spring:  
    session:
        timeout: 86400
        store-type: redis

默认是 none ,表示存储在单挑服务器

 store-type: redis :表示从redis中读写 session

等配置之后,剩下的都是前端的事情了:

前端要做的事情也很简单:就是在往后端发送请求的时候携带上cookie就行

下面这个是axious的拦截器举例:

const myAxios = axios.create({
    baseURL: 'http://localhost:8080/api',
});

myAxios.defaults.withCredentials = true;

七:店铺状态设置(第三个项目场景):

接口设计:

  • 设置营业状态
  • 管理端查询营业状态
  • 用户端查询营业状态

具体的代码实现: 

因为我们用了reids缓存技术,所以我们这里就没有设计到数据库的操作,所有的操作都是在Controll层进行(也是因为这个接口功能比较简单)

服务端Controll层:

package com.sky.controller.admin;

import com.sky.result.PageResult;
import com.sky.result.Result;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.*;

@RestController("adminShopController")
@Slf4j
@RequestMapping("/admin/shop")
@Api(tags = "店铺相关接口")

public class ShopController {

    public static final String KEY =  "Shop_Status";

    @Autowired
    private RedisTemplate redisTemplate;


    /**
     * 设置营业状态
     * @param status
     * @return
     */
    @PutMapping("/{status}")
    @ApiOperation("设置营业状态")
    public Result SetShopStatus(@PathVariable Integer status){
        log.info("设置店铺的状态:{}",status==1?"营业中":"打烊中");
        redisTemplate.opsForValue().set(KEY,status);
        return Result.success();
    }

    /**
     * 管理端查询营业状态
     * @return
     */
    @GetMapping("/status")
    @ApiOperation("管理端查询营业状态")
    public Result SelectShopStatus(){
        final Integer status = (Integer) redisTemplate.opsForValue().get(KEY);
        log.info("获取店铺的状态:{}",status);
        return Result.success(status);
    }
}
  • 设置营业状态

设置营业状态用的是redis中最简单字符串,然后设置了一个key叫做Shop_Status,然后value就是status。

  • 管理端查询营业状态

查询状态因为直接放在了redis中,所有,直接用get方法获取即可

客户端Controll层:

package com.sky.controller.user;

import com.sky.result.Result;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.*;

@RestController("userShopController")
@Slf4j
@RequestMapping("/user/shop")
@Api(tags = "店铺相关接口")

public class ShopController {
    public static final String KEY =  "Shop_Status";


    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 用户端查询营业状态
     * @return
     */
    @GetMapping("/status")
    @ApiOperation("用户端查询营业状态")
    public Result SelectShopStatus(){
        final Integer status = (Integer) redisTemplate.opsForValue().get(KEY);
        log.info("获取店铺的状态:{}",status);
        return Result.success(status);
    }
}

客户端和服务端的唯一区别就是路径不同。

  • 12
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值