苍穹外卖总结(面试题)

如有不对的地方请在评论区指正。

1、项目简介

管理端使用vue开发,主要是商家来使用,提供餐品的管理功能,主要有下面几个模块:

  • 员工模块,提供员工账号的登录功能和管理功能

  • 分类、菜品、套餐模块,分别对分类、菜品和套餐进行增删改查和启用禁用

  • 订单模块,可以搜索和查看订单,变更订单状态

  • 统计模块,统计营业额、用户、订单和销量排名,还有Excel报表导出功能

  • 工作台模块,提供今日运营数据数据以及订单、菜品、套餐的总览

用户端使用微信小程序开发,主要是给用户提供点餐功能

  • 登录模块,调用微信小程序登录接口实现登录功能

  • 菜品、套餐模块,用于查询菜品、套餐的信息

  • 购物车模块,在购物车中添加或删除套餐菜品

  • 订单模块,提供下单、微信支付、查询订单、取消订单、再来一单、催单功能。

介绍一下苍穹外卖项目

本项目是专门为一家餐厅定制的一款软件产品,主要包含包括 系统管理端小程序端 两部分

系统管理端提供给餐饮企业内部员工使用,可以对餐厅的分类、菜品、套餐、订单、员工等进行管理维护

小程序端提供给消费者使用,可以在线浏览菜品、添加购物车、下单、支付、催单等操作

我在这个项目中主要负责后端分类、套餐、菜品模块和小程序端的所有功能

2、管理端登录

登录的本质就是对员工表进行查询操作

  1. 前端传过来的数据,我们通过数据库判断该用户是否存在

  2. 如果用户存在,现在生成JWT令牌

  3. 我们通过JWT工具类生成令牌,但在生成的过程中一定要将用户id交给他

  4. 我们向前端返回数据,带上我们之前生成的token

3、拦截器JWT验证

  1. 为了简化开发,我们在拦截器中进行JWT验证

  2. 我们根据前端传过来的请求头获取token

  3. 然后通过JWT工具解析token,得到用户ID

  4. 我们为了简化开发,使用ThreadLocal,在上面的方法中将用户ID存入ThreadLocal中

4、JWT

为什么需要JWT?

答:HTTP协议是无状态的,也就是说,如果我们已经认证了一个用户,那么他下一次请求的时候,服务器不知道我是谁,我们必须再次认证

JWT和Session有什么区别?

答:相同点是,它们都是存储用户信息;然而,Session是在服务器端的,而JWT是在客户端的。Session方式存储用户信息的最大问题在于要占用大量服务器内存,增加服务器的开销。而JWT方式将用户状态分散到了客户端中,可以明显减轻服务端的内存压力。

JWT是如何工作的?

用户携带用户名和密码请求访问登录接口,如果没有任何问题,我使用用户名和密码生成token。然后每次浏览器访问网站的请求,都会带上token。

注意:每一次请求中的token都会放在请求头中。

JWT使用步骤?

1. 自定义一个JWT工具类

/**
 * JwtUtil用于生成 jwt令牌 和校验 jwt令牌
 * 我们要明白除了/user/login 和/user/regist这两个请求不需要拦截,
 * 其他的请求必须要拦截,那我们在每个请求方法里面校验jwt,这显然不合适
 * 我们想到一个办法:每次请求之前设置一个拦截器JwtTokenAdminInterceptor,
 * 用于进行jwt的校验。
 *
 * 在后面的业务中发现,我们需要用户id,每次都从JWT中获取非常麻烦,所以我们想到了
 * ThreadLocal(BaseContext),我们将当前登陆用户的id存储在这里面,每次需要id就上前取。那么,
 * 我们在哪里将id存储在THreadLocal中,答案:拦截器。代码如下:
 * BaseContext.setCurrentId(empId);
 */
public class JwtUtil {
    /**
     * 生成jwt
     * 使用Hs256算法, 私匙使用固定秘钥
     *
     * @param secretKey jwt秘钥
     * @param ttlMillis jwt过期时间(毫秒)
     * @param claims    设置的信息(里面一般存放用户ID)
     */
    public static String createJWT(String secretKey, long ttlMillis, Map<String, Object> claims) {
        // 指定签名的时候使用的签名算法,也就是header那部分
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
​
        // 生成JWT的时间
        long expMillis = System.currentTimeMillis() + ttlMillis;
        Date exp = new Date(expMillis);
​
        // 设置jwt的body
        JwtBuilder builder = Jwts.builder()
                // 如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的
                .setClaims(claims)
                // 设置签名使用的签名算法和签名使用的秘钥
                .signWith(signatureAlgorithm, secretKey.getBytes(StandardCharsets.UTF_8))
                // 设置过期时间
                .setExpiration(exp);
​
        return builder.compact();
    }
​
    /**
     * Token解密
     *
     * @param secretKey jwt秘钥 此秘钥一定要保留好在服务端, 不能暴露出去, 否则sign就可以被伪造, 如果对接多个客户端建议改造成多个
     * @param token     加密后的token
     */
    public static Claims parseJWT(String secretKey, String token) {
        // 得到DefaultJwtParser
        Claims claims = Jwts.parser()
                // 设置签名的秘钥
                .setSigningKey(secretKey.getBytes(StandardCharsets.UTF_8))
                // 设置需要解析的jwt
                .parseClaimsJws(token).getBody();
        return claims;
    }
​
}

2. 在登录接口处,创建token并返回

@Autowired
private JwtProperties jwtProperties;
​
//登录成功后,生成jwt令牌
Map<String, Object> claims = new HashMap<>();
claims.put(JwtClaimsConstant.EMP_ID, employee.getId());
String token = JwtUtil.createJWT(
    jwtProperties.getAdminSecretKey(),
    jwtProperties.getAdminTtl(),
    claims);

sky:
  jwt:
    # 设置jwt签名加密时使用的秘钥
    admin-secret-key: itcast
    # 设置jwt过期时间
    admin-ttl: 7200000
    # 设置前端传递过来的令牌名称
    admin-token-name: token

@Component
@ConfigurationProperties(prefix = "sky.jwt")
@Data
public class JwtProperties {
    /**
     * 管理端员工生成jwt令牌相关配置
     */
    private String adminSecretKey;//加密的密钥
    private long adminTtl;
    private String adminTokenName;//前端发过来的jwt令牌在请求头中的参数名
}

5、拦截器

拦截器的使用场景有哪些?
  • 权限检查:一般为登录权限检查

  • 性能检测:有事系统在某段时间莫名其妙很慢,可以通过拦截器在进入程序之前计时,进入程序之后结束计时。用以计算程序运行时间

拦截器的前置知识有哪些?
  • SpringBoot定义了HandlerInterceptor接口。接口中定义了拦截器行为:preHandle(请求前)、postHandle(请求后)、afterCompletion

实现JWT拦截的步骤?

1. 自定义拦截器类

它必须实现HandlerInterceptor接口

@Component
public class JwtTokenAdminInterceptor implements HandlerInterceptor {
​
    @Autowired
    private JwtProperties jwtProperties;
​
    /**
     * 校验jwt:在拦截方法之前
     */
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //判断当前拦截到的是Controller的方法还是其他资源
        if (!(handler instanceof HandlerMethod)) {
            //当前拦截到的不是动态方法,直接放行
            return true;
        }
​
        //1、从请求头中获取令牌
        String token = request.getHeader(jwtProperties.getAdminTokenName());
​
        //2、校验令牌
        try {
            Claims claims = 
                JwtUtil.parseJWT(jwtProperties.getAdminSecretKey(), token);
            Long empId = Long.valueOf
                (claims.get(JwtClaimsConstant.EMP_ID).toString());
​
            //将当前用户id存储到ThreadLocal
            BaseContext.setCurrentId(empId);
​
            //3、通过,放行
            return true;
        } catch (Exception ex) {
            //4、不通过,响应401状态码
            response.setStatus(401);
            return false;
        }
    }
}

2. 配置拦截器

我们必须告诉项目我们配置的拦截器是哪个?拦截的地址是啥?放行的地址又是啥?

  • addPathPatterns方法定义拦截的地址

  • excludePathPatterns定义排除某些地址

@Configuration
@Slf4j
public class WebMvcConfiguration extends WebMvcConfigurationSupport {
​
    @Autowired
    private JwtTokenAdminInterceptor jwtTokenAdminInterceptor;//我们自定义拦截器
​
    /**
     * 注册自定义拦截器
     *
     * @param registry
     */
    protected void addInterceptors(InterceptorRegistry registry) {
        
        registry.addInterceptor(jwtTokenAdminInterceptor)
                .addPathPatterns("/admin/**")
                //该请求路径/admin/employee/login不需要拦截
                .excludePathPatterns("/admin/employee/login");
    }
}

6、ThreadLocal

上面的例子里有ThreadLocal,你看到了吗?没看到就是没有仔细看代码,给我回去再看一遍!

ThreadLocal是啥?

从名字我们就可以看到ThreadLocal 叫做本地线程变量,意思是说,ThreadLocal 中填充的的是当前线程的变量,该变量对其他线程而言是封闭且隔离的,ThreadLocal 为变量在每个线程中创建了一个副本,这样每个线程都可以访问自己内部的副本变量。

如何使用ThreadLocal?

1. 自定义ThreadLocal工具类

/**
 * ThreadLocal 工具类
 */
public class BaseContext {
​
    public static ThreadLocal<Long> threadLocal = new ThreadLocal<>();
​
    //下面的这些方法都可以自定义
    public static void setCurrentId(Long id) {
        threadLocal.set(id);
    }
​
    public static Long getCurrentId() {
        return threadLocal.get();
    }
​
    public static void removeCurrentId() {
        threadLocal.remove();
    }
​
}

上面的代码表示当前ThreadLocal的功能只有一个:对用户的ID进行存储

2. 用法在上面代码

7、nginx反向代理和负载均衡

反向代理

就是将前端发送的动态请求由 nginx 转发到后端服务器

nginx 反向代理的好处:

  • 提高访问速度

    因为nginx本身可以进行缓存,如果访问的同一接口,并且做了数据缓存,nginx就直接可把数据返回,不需要真正地访问服务端,从而提高访问速度。

  • 进行负载均衡

    所谓负载均衡,就是把大量的请求按照我们指定的方式均衡的分配给集群中的每台服务器。

  • 保证后端服务安全

    因为一般后台服务地址不会暴露,所以使用浏览器不能直接访问,可以把nginx作为请求访问的入口,请求到达nginx后转发到具体的服务中,从而保证后端服务的安全。

nginx 反向代理的配置方式:

server{
    listen 80;
    server_name localhost;
    
    location /api/{
        proxy_pass http://localhost:8080/admin/; #反向代理
    }
}

proxy_pass:该指令是用来设置代理服务器的地址,可以是主机名称,IP地址加端口号等形式。

如上代码的含义是:监听80端口号, 然后当我们访问 http://localhost:80/api/../..这样的接口的时候,它会通过 location /api/ {} 这样的反向代理到 http://localhost:8080/admin/上来。

负载均衡

所谓负载均衡,就是把大量的请求按照我们指定的方式均衡的分配给集群中的每台服务器。

nginx 负载均衡的配置方式:

upstream webservers{
    server 192.168.100.128:8080;
    server 192.168.100.129:8080;
}
server{
    listen 80;
    server_name localhost;
    
    location /api/{
        proxy_pass http://webservers/admin;#负载均衡
    }
}

upstream:如果代理服务器是一组服务器的话,我们可以使用upstream指令配置后端服务器组。

如上代码的含义是:监听80端口号, 然后当我们访问 http://localhost:80/api/../..这样的接口的时候,它会通过 location /api/ {} 这样的反向代理到 http://webservers/admin,根据webservers名称找到一组服务器,根据设置的负载均衡策略(默认是轮询)转发到具体的服务器。

nginx 负载均衡策略:

名称说明
轮询默认方式
weight权重方式,默认为1,权重越高,被分配的客户端请求就越多
ip_hash依据ip分配方式,这样每个访客可以固定访问一个后端服务
least_conn依据最少连接方式,把请求优先分配给连接数少的后端服务
url_hash依据url分配方式,这样相同的url会被分配到同一个后端服务
fair依据响应时间方式,响应时间短的服务将会被优先分配

具体配置方式:

轮询:

upstream webservers{
    server 192.168.100.128:8080;
    server 192.168.100.129:8080;
}

weight:

upstream webservers{
    server 192.168.100.128:8080 weight=90;
    server 192.168.100.129:8080 weight=10;
}

ip_hash:

upstream webservers{
    ip_hash;
    server 192.168.100.128:8080;
    server 192.168.100.129:8080;
}

least_conn:

upstream webservers{
    least_conn;
    server 192.168.100.128:8080;
    server 192.168.100.129:8080;
}

url_hash:

upstream webservers{
    hash &request_uri;
    server 192.168.100.128:8080;
    server 192.168.100.129:8080;
}

fair:

upstream webservers{
    server 192.168.100.128:8080;
    server 192.168.100.129:8080;
    fair;
}

8、Swagger

5.1 介绍

Swagger 在开发阶段使用的框架,帮助后端开发人员做后端的接口测试。目前,一般都使用knife4j框架。

knife4j是Swagger的增强解决方案

5.2 使用步骤
  1. 导入 knife4j 的maven坐标

    在pom.xml中添加依赖

    <dependency>
       <groupId>com.github.xiaoymin</groupId>
       <artifactId>knife4j-spring-boot-starter</artifactId>
    </dependency>

  2. 新建配置类

    • 新建一个config包再新建一个配置类来配置swagger

    • 该类继承WebMvcConfigurationSupport类使后续设置的静态资源生效

    • 再在该类上加个ConfigurationSlf4j注解

  3. 在配置类中加入 knife4j 相关配置

    @Configuration
    @Slf4j
    public class WebMvcConfiguration extends WebMvcConfigurationSupport {
        /**
         * 通过knife4j生成接口文档
         * @return
         */
        @Bean
        public Docket docket1() {
            ApiInfo apiInfo = new ApiInfoBuilder()
                    .title("苍穹外卖项目接口文档")
                    .version("2.0")
                    .description("苍穹外卖项目接口文档")
                    .build();
            Docket docket = new Docket(DocumentationType.SWAGGER_2)
                    .groupName("管理端接口")
                    .apiInfo(apiInfo)
                    .select()
                    .apis(RequestHandlerSelectors
                          .basePackage("com.sky.controller.admin"))//这里千万不可以出错
                    .paths(PathSelectors.any())
                    .build();
            return docket;
        }
    }

  4. 设置静态资源映射,否则接口文档页面无法访问

    WebMvcConfiguration.java

    /**
    * 设置静态资源映射
    * @param registry
    */
    protected void addResourceHandlers(ResourceHandlerRegistry registry) {
            registry.addResourceHandler("/doc.html")
                .addResourceLocations("classpath:/META-INF/resources/");
            registry.addResourceHandler("/webjars/**")
                .addResourceLocations("classpath:/META-INF/resources/webjars/");
    }

knife4j的工作流程

进行上面的配置之后,在docket( )中,knife4j 就会扫描包com.sky.controller,然后通过反射生成接口文档。当我们在浏览器中访问addResourceHandlers( )中的doc.html文档时,就会看到接口文档

9、时间格式设置

问题引入

不难发现,最后操作时间格式不清晰,在代码完善中解决。

解决方式:

1). 方式一

在属性上加上注解,对日期进行格式化

但这种方式,需要在每个时间属性上都要加上该注解,使用较麻烦,不能全局处理。

2). 方式二(推荐 )

使用对象映射器(后面提到)

10、对象映射器

对象映射器:基于jackson将Java对象转为json,或者将json转为Java对象

对象映射器的作用

  1. 将JSON解析为Java对象的过程称为 [从JSON反序列化Java对象]

  2. 从Java对象生成JSON的过程称为 [序列化Java对象到JSON]

将前端发送的数据过于长超过16位时,long的精度为16位,导致精度不准确,例如id为雪花算法的自动生成,导致前端发出的请求后端的接收的数据精度受到影响,转换为json格式,就解决了这个问题,包括日期型的相关转化(js对于long类型会造成精度损失)。

上图显示,后端日期转发给前端时,出现了精度损失

对象映射器使用步骤

1. 自定义一个对象映射器

package com.sky.json;

/**
 * 对象映射器:基于jackson将Java对象转为json,或者将json转为Java对象
 * 将JSON解析为Java对象的过程称为 [从JSON反序列化Java对象]
 * 从Java对象生成JSON的过程称为 [序列化Java对象到JSON]
 */
public class JacksonObjectMapper extends ObjectMapper {

    /**
    * DEFAULT_DATE_TIME_FORMAT:解决上文的时间格式问题
    */
    public static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd";
    public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm";
    public static final String DEFAULT_TIME_FORMAT = "HH:mm:ss";

    public JacksonObjectMapper() {
        super();
        //收到未知属性时不报异常
        this.configure(FAIL_ON_UNKNOWN_PROPERTIES, false);

        //反序列化时,属性不存在的兼容处理
        this.getDeserializationConfig()
            .withoutFeatures(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);

        
        SimpleModule simpleModule = new SimpleModule()
                .addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
                .addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
                .addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)))
                .addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
                .addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
                .addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));

        //注册功能模块 例如,可以添加自定义序列化器和反序列化器
        this.registerModule(simpleModule);
    }
}

2. WebMvcConfig里进行配置:

@Configuration
public class WebMvcConfiguration extends WebMvcConfigurationSupport {
    
     /**
     * 扩展mvc框架的消息转换器
     * @param converters the list of configured converters to extend
     */
    
    @Override
    protected void extendMessageConverters
        (List<HttpMessageConverter<?>> converters) {
        // 添加一个转换器,除自带八大转换器外,将Long装成String
        //创建消息转换器对象
        MappingJackson2HttpMessageConverter converter = 
            new MappingJackson2HttpMessageConverter();
        
        //设置对象转换器,底层使用Jackson将Java对象转换为json
        converter.setObjectMapper(new JacksonObjectMapper());
        
        //将上面的消息转换器对象追加到mvc框架的转换器集合中
        converters.add(0,converter);
    }
}

时间格式修改成功!!!添加后,再次测试

11、公共字段自动填充

在项目中我们发现我先很多重复字段:

新增操作:设置create_time、create_user、update_time、update_user

修改操作:设置 update_time、update_user

我们使用AOP切面编程,实现功能增强,来完成公共字段自动填充功能

实现步骤:

1). 自定义注解 AutoFill,用于标识需要进行公共字段自动填充的方法

/**
 * 自定义注解,用于标识某个方法需要进行功能字段自动填充处理
 */
@Target(ElementType.METHOD)//表示该段注解只能加在方法上面
@Retention(RetentionPolicy.RUNTIME)//表示该注解在程序运行时仍然可以被读取和处理
public @interface AutoFill {

    OperationType value();//成员变量 value,其类型是 OperationType 枚举类型

}

2). 自定义切面类 AutoFillAspect,统一拦截加入了 AutoFill 注解的方法,通过反射为公共字段赋值

/**
 * 自定义切面,实现公共字段自动填充处理逻辑
 */
@Aspect
@Component
@Slf4j
public class AutoFillAspect {
    
    面向切面编程,我们需要干嘛?
        1. 我们只需要实现切面类
        2. 然后通过配置文件配置通知(对应的方法在什么时候插入)、配置切入点(对应的方法在哪里插入) 

    /**
     * 切入点
     */
    @Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill)")
    public void autoFillPointCut(){}

    /**
     * 前置通知,在通知中进行公共字段的赋值
     */
    @Before("autoFillPointCut()")
    public void autoFill(JoinPoint joinPoint){
        log.info("公共字段代码请看项目");
    }
}

3). 在 Mapper 的方法上加入 AutoFill 注解

@Mapper
public interface CategoryMapper {
    /**
     * 根据id修改分类
     * @param category
     */
    @AutoFill(value = OperationType.UPDATE)
    void update(Category category);
}

12、阿里云

实现步骤:

1. 定义OSS相关配置

sky:
  alioss:
    endpoint: oss-cn-hangzhou.aliyuncs.com
    access-key-id: LTAI5tPeFLzsPPT8gG3LPW64
    access-key-secret: U6k1brOZ8gaOIXv3nXbulGTUzy6Pd7
    bucket-name: sky-take-out

2. 读取OSS配置

@Component
@ConfigurationProperties(prefix = "sky.alioss")
@Data
public class AliOssProperties {

    private String endpoint;
    private String accessKeyId;
    private String accessKeySecret;
    private String bucketName;

}

3. 生成OSS工具类对象

/**
 * 配置类,用于创建AliOssUtil对象
 */

@Configuration
@Slf4j
public class OssConfiguration {
    /**
    * AliOssProperties aliOssProperties
    * 此处不需要使用注解@Autowired,会自动加入@Autowired
    */

    @Bean
    @ConditionalOnMissingBean
    public AliOssUtil aliOssUtil(AliOssProperties aliOssProperties){
        log.info("开始创建阿里云文件上传工具类对象:{}",aliOssProperties);
        return new AliOssUtil(aliOssProperties.getEndpoint(),
                aliOssProperties.getAccessKeyId(),
                aliOssProperties.getAccessKeySecret(),
                aliOssProperties.getBucketName());
    }
}
@Data
@AllArgsConstructor
@Slf4j
public class AliOssUtil {

    private String endpoint;
    private String accessKeyId;
    private String accessKeySecret;
    private String bucketName;

    /**
     * 文件上传
     */
    public String upload(byte[] bytes, String objectName) {

        // 创建OSSClient实例。
        OSS ossClient = new OSSClientBuilder()
            .build(endpoint, accessKeyId, accessKeySecret);

        try {
            // 创建PutObject请求。
            ossClient.putObject(bucketName,
                                objectName, 
                                new ByteArrayInputStream(bytes));
            
        } catch (OSSException oe) {
            System.out.println
                ("Caught an OSSException, which means your request made it to OSS, "
                 + "but was rejected with an error response for some reason.");
            
            System.out.println("Error Message:" + oe.getErrorMessage());
            System.out.println("Error Code:" + oe.getErrorCode());
            System.out.println("Request ID:" + oe.getRequestId());
            System.out.println("Host ID:" + oe.getHostId());
        } catch (ClientException ce) {
            System.out.println("Caught an ClientException, which means the client encountered "
                    + "a serious internal problem while trying to communicate with OSS, "
                    + "such as not being able to access the network.");
            System.out.println("Error Message:" + ce.getMessage());
        } finally {
            if (ossClient != null) {
                ossClient.shutdown();
            }
        }

        //文件访问路径规则 https://BucketName.Endpoint/ObjectName
        StringBuilder stringBuilder = new StringBuilder("https://");
        stringBuilder
                .append(bucketName)
                .append(".")
                .append(endpoint)
                .append("/")
                .append(objectName);

        log.info("文件上传到:{}", stringBuilder.toString());

        return stringBuilder.toString();
    }
}

4. 定义文件上传接口

/**
 * 通用接口
 */
@RestController
@RequestMapping("/admin/common")
@Api(tags = "通用接口")
@Slf4j
public class CommonController {

    @Autowired
    private AliOssUtil aliOssUtil;

    /**
     * 文件上传
     * @param file
     * @return
     */
    @PostMapping("/upload")
    @ApiOperation("文件上传")
    public Result<String> upload(MultipartFile file){
        log.info("文件上传:{}",file);

        try {
            //原始文件名
            String originalFilename = file.getOriginalFilename();
            //截取原始文件名的后缀   dfdfdf.png
            String extension = originalFilename.substring(originalFilename.lastIndexOf("."));
            //构造新文件名称
            String objectName = UUID.randomUUID().toString() + extension;

            //文件的请求路径
            String filePath = aliOssUtil.upload(file.getBytes(), objectName);
            return Result.success(filePath);
        } catch (IOException e) {
            log.error("文件上传失败:{}", e);
        }

        return Result.error(MessageConstant.UPLOAD_FAILED);
    }
}

13、Redis

主要特点:
  • 基于内存存储,读写性能高

  • 适合存储热点数据(热点商品、资讯、新闻)

五种常用数据类型介绍

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

  • 字符串 string

  • 哈希 hash

  • 列表 list

  • 集合 set

  • 有序集合 sorted set / zset

3. Redis常用命令

3.1 字符串操作命令

Redis 中字符串类型常用命令:

  • SET key value 设置指定key的值

  • GET key 获取指定key的值

  • SETEX key seconds value 设置指定key的值,并将 key 的过期时间设为 seconds 秒

  • SETNX key value 只有在 key 不存在时设置 key 的值

3.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.3 列表操作命令

Redis 列表是简单的字符串列表,按照插入顺序排序,常用命令:

  • LPUSH key value1 [value2] 将一个或多个值插入到列表头部

  • LRANGE key start stop 获取列表指定范围内的元素

  • RPOP key 移除并获取列表最后一个元素

  • LLEN key 获取列表长度

  • BRPOP key1 [key2 ] timeout 移出并获取列表的最后一个元素, 如果列表没有元素会阻塞列表直到等待超 时或发现可弹出元素为止

3.4 集合操作命令

Redis set 是string类型的无序集合。集合成员是唯一的,这就意味着集合中不能出现重复的数据,常用命令:

  • SADD key member1 [member2] 向集合添加一个或多个成员

  • SMEMBERS key 返回集合中的所有成员

  • SCARD key 获取集合的成员数

  • SINTER key1 [key2] 返回给定所有集合的交集

  • SUNION key1 [key2] 返回所有给定集合的并集

  • SREM key member1 [member2] 移除集合中一个或多个成员

3.5 有序集合操作命令

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

常用命令:

  • ZADD key score1 member1 [score2 member2] 向有序集合添加一个或多个成员

  • ZRANGE key start stop [WITHSCORES] 通过索引区间返回有序集合中指定区间内的成员

  • ZINCRBY key increment member 有序集合中对指定成员的分数加上增量 increment

  • ZREM key member [member ...] 移除有序集合中的一个或多个成员

3.6 通用命令

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

  • KEYS pattern 查找所有符合给定模式( pattern)的 key

  • EXISTS key 检查给定 key 是否存在

  • TYPE key 返回 key 所储存的值的类型

  • DEL key 该命令用于在 key 存在是删除 key

4.在Java中操作Redis

4.1 Redis的Java客户端

前面我们讲解了Redis的常用命令,这些命令是我们操作Redis的基础,那么我们在java程序中应该如何操作Redis呢?这就需要使用Redis的Java客户端,就如同我们使用JDBC操作MySQL数据库一样。

Redis 的 Java 客户端很多,常用的几种:

  • Jedis

  • Lettuce

  • Spring Data Redis

Spring 对 Redis 客户端进行了整合,提供了 Spring Data Redis,在Spring Boot项目中还提供了对应的Starter,即 spring-boot-starter-data-redis。

我们重点学习Spring Data Redis

4.2 Spring Data Redis使用方式

1. 导入依赖

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

2. 配置Redis数据源

sky:
  redis:
    host: localhost
    port: 6379
    password: 123456
    database: 10
# database:指定使用Redis的哪个数据库,Redis服务启动后默认有16个数据库,编号分别是从0到15

3. 编写配置类,创建RedisTemplate对象

编写配置类,创建RedisTemplate对象,将创建的对象注入容器中,方便接口直接调用

@Configuration
@Slf4j
public class RedisConfiguration {

    @Bean
    public RedisTemplate redisTemplate
        (RedisConnectionFactory redisConnectionFactory){

        RedisTemplate redisTemplate = new RedisTemplate();
        //设置redis的连接工厂对象
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        //设置redis key的序列化器
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        return redisTemplate;
    }
}

解释说明:

当前配置类不是必须的,因为 Spring Boot 框架会自动装配 RedisTemplate 对象,但是默认的key序列化器为

JdkSerializationRedisSerializer,导致我们存到Redis中后的数据和原始数据有差别,故设置为

StringRedisSerializer序列化器。

4. 通过RedisTemplate对象操作Redis

@SpringBootTest
public class SpringDataRedisTest {
    @Autowired
    private RedisTemplate redisTemplate;

    @Test
    public void testRedisTemplate(){
        System.out.println(redisTemplate);
        //string数据操作
        ValueOperations valueOperations = redisTemplate.opsForValue();
        //hash类型的数据操作
        HashOperations hashOperations = redisTemplate.opsForHash();
        //list类型的数据操作
        ListOperations listOperations = redisTemplate.opsForList();
        //set类型数据操作
        SetOperations setOperations = redisTemplate.opsForSet();
        //zset类型数据操作
        ZSetOperations zSetOperations = redisTemplate.opsForZSet();
    }
}

Spring Data Redis中提供了一个高度封装的类:RedisTemplate,对相关api进行了归类封装,将同一类型操作封装为operation接口,具体分类如下:

  • ValueOperations:string数据操作

  • SetOperations:set类型数据操作

  • ZSetOperations:zset类型数据操作

  • HashOperations:hash类型的数据操作

  • ListOperations:list类型的数据操作

4.3 操作常见类型数据

1. 操作字符串类型数据

/**
* 操作字符串类型的数据
*/
@Test  
public void testString(){
    // set get setex setnx
    redisTemplate.opsForValue().set("name","小明");
    String city = (String) redisTemplate.opsForValue().get("name");
    System.out.println(city);
    redisTemplate.opsForValue().set("code","1234",3, TimeUnit.MINUTES);
    redisTemplate.opsForValue().setIfAbsent("lock","1");
    redisTemplate.opsForValue().setIfAbsent("lock","2");
}

2. 操作哈希类型数据

/**
* 操作哈希类型的数据
*/
@Test
public void testHash(){
    //hset hget hdel hkeys hvals
    HashOperations hashOperations = redisTemplate.opsForHash();

    hashOperations.put("100","name","tom");
    hashOperations.put("100","age","20");

    String name = (String) hashOperations.get("100", "name");
    System.out.println(name);

    Set keys = hashOperations.keys("100");
    System.out.println(keys);

    List values = hashOperations.values("100");
    System.out.println(values);

    hashOperations.delete("100","age");
}

3. 操作列表类型数据

/**
* 操作列表类型的数据
*/
@Test
public void testList(){
    //lpush lrange rpop llen
    ListOperations listOperations = redisTemplate.opsForList();

    listOperations.leftPushAll("mylist","a","b","c");
    listOperations.leftPush("mylist","d");

    List mylist = listOperations.range("mylist", 0, -1);
    System.out.println(mylist);

    listOperations.rightPop("mylist");

    Long size = listOperations.size("mylist");
    System.out.println(size);
}

4. 操作集合类型数据

/**
     * 操作集合类型的数据
     */
@Test
public void testSet(){
    //sadd smembers scard sinter sunion srem
    SetOperations setOperations = redisTemplate.opsForSet();

    setOperations.add("set1","a","b","c","d");
    setOperations.add("set2","a","b","x","y");

    Set members = setOperations.members("set1");
    System.out.println(members);

    Long size = setOperations.size("set1");
    System.out.println(size);

    Set intersect = setOperations.intersect("set1", "set2");
    System.out.println(intersect);

    Set union = setOperations.union("set1", "set2");
    System.out.println(union);

    setOperations.remove("set1","a","b");
}

5). 操作有序集合类型数据

/**
* 操作有序集合类型的数据
*/
@Test
public void testZset(){
    //zadd zrange zincrby zrem
    ZSetOperations zSetOperations = redisTemplate.opsForZSet();

    zSetOperations.add("zset1","a",10);
    zSetOperations.add("zset1","b",12);
    zSetOperations.add("zset1","c",9);

    Set zset1 = zSetOperations.range("zset1", 0, -1);
    System.out.println(zset1);

    zSetOperations.incrementScore("zset1","c",10);

    zSetOperations.remove("zset1","a","b");
}

6). 通用命令操作

/**
     * 通用命令操作
     */
@Test
public void testCommon(){
    //keys exists type del
    Set keys = redisTemplate.keys("*");
    System.out.println(keys);

    Boolean name = redisTemplate.hasKey("name");
    Boolean set1 = redisTemplate.hasKey("set1");

    for (Object key : keys) {
        DataType type = redisTemplate.type(key);
        System.out.println(type.name());
    }

    redisTemplate.delete("mylist");
}

店铺营业状态

营业状态数据存储方式:基于Redis的字符串来进行存储。约定:1表示营业 0表示打烊

实现步骤

1. 设置营业状态

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

    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 设置店铺的营业状态
     * @param status
     * @return
     */
    @PutMapping("/{status}")
    @ApiOperation("设置店铺的营业状态")
    public Result setStatus(@PathVariable Integer status){
        redisTemplate.opsForValue().set("SHOP_STATUS",status);
        return Result.success();
    }
}

2. 查询营业状态

/**
* 获取店铺的营业状态
* @return
*/
@GetMapping("/status")
@ApiOperation("获取店铺的营业状态")
public Result<Integer> getStatus(){
    Integer status = (Integer) redisTemplate.opsForValue().get(KEY);
    return Result.success(status);
}

缓存菜品

缓存逻辑分析:

  • 每个分类下的菜品保存一份缓存数据

  • 数据库中菜品数据有变更时清理缓存数据

实现步骤

1. 用户端查询菜品接口中判断redis是否有数据

@Autowired
private RedisTemplate redisTemplate;
	/**
     * 根据分类id查询菜品
     */
@GetMapping("/list")
@ApiOperation("根据分类id查询菜品")
public Result<List<DishVO>> list(Long categoryId) {

    //构造redis中的key,规则:dish_分类id
    String key = "dish_" + categoryId;

    //查询redis中是否存在菜品数据
    List<DishVO> list = (List<DishVO>) redisTemplate.opsForValue().get(key);
    if(list != null && list.size() > 0){ //如果存在,直接返回,无须查询数据库
        return Result.success(list);
    }

    Dish dish = new Dish();
    dish.setCategoryId(categoryId);
    dish.setStatus(StatusConstant.ENABLE);//查询起售中的菜品

    //如果不存在,查询数据库,将查询到的数据放入redis中
    list = dishService.listWithFlavor(dish);
    redisTemplate.opsForValue().set(key, list);

    return Result.success(list);
}

2. 完善代码

为了使数据库中的菜品数据和redis中的菜品数据一致,我么需要修改如下接口

  • 新增菜品

  • 修改菜品

  • 批量删除菜品

  • 起售、停售菜品

1. 定义方法:清空缓存

/**
* 清理缓存数据
* @param pattern 可是是具体的dish_2 也可以是dish_*
*/
private void cleanCache(String pattern){
    Set keys = redisTemplate.keys(pattern);
    redisTemplate.delete(keys);
}

2. 新增菜品优化

/**
* 新增菜品
*/
@PostMapping
@ApiOperation("新增菜品")
public Result save(@RequestBody DishDTO dishDTO) {
    log.info("新增菜品:{}", dishDTO);
    dishService.saveWithFlavor(dishDTO);

    //清理缓存数据
    String key = "dish_" + dishDTO.getCategoryId();
    cleanCache(key);
    return Result.success();
}

3. 菜品批量删除优化

/**
* 菜品批量删除
*/
@DeleteMapping
@ApiOperation("菜品批量删除")
public Result delete(@RequestParam List<Long> ids) {
    log.info("菜品批量删除:{}", ids);
    dishService.deleteBatch(ids);

    //将所有的菜品缓存数据清理掉,所有以dish_开头的key
    cleanCache("dish_*");

    return Result.success();
}

等等,这里不一 一举了

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值