Redis项目实战

news2024/9/26 3:32:15

本文用用代码演示Redis实现分布式缓存、分布式锁、接口幂等性、接口防刷的功能。

课程地址:Redis实战系列-课程大纲_哔哩哔哩_bilibili

目录

一. 新建springBoot项目整合Redis

二. Redis实现分布式缓存

2.1 原理及好处

2.2 数据准备

2.3 Redis实现分布式缓存

2.4 优雅实现分布式缓存(Redis+AOP+自定义注解)

第0步:准备RedisTool工具类

第一步:导入AOP依赖

第二步:自定义注解

第三步:业务类代码

第四步:编写切面类MyCacheAop

三、Redis实现分布式锁

3.1 原理

3.2 初始化库存

3.3 Redis实现分布式锁

3.4 JMeter工具测试

3.5 优雅实现分布式锁(Redis+AOP+自定义注解)

第一步:自定义注解

第二步:抽取加锁释放锁的公共代码

四、Redis+Token机制实现接口幂等性校验

4.1 接口幂等性校验使用场景

4.2 原理图

4.3 编写一般业务代码

4.4 接口幂等性实现步骤

第一步:自定义注解

第二步:定义拦截器

第三步:注册拦截器

第四步:测试

幂等性总结★★★

五、接口防刷功能

5.1 防刷概述

5.2 自定义注解

5.3 拦截器

 5.4 配置拦截器

5.5 业务接口&测试

5.6 延伸:@Resource和@Autowired的区别


一. 新建springBoot项目整合Redis

新建一个基于maven构建的项目,加入SpringBoot和Redis相关依赖,写一个接口进行测试,看是否可以对Redisi进行存值和取值。

项目结构:

pom文件内容如下: 

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>org.wuya</groupId>
  <artifactId>springbootRedisDemo</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>jar</packaging>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>

  <!-- springboot相关的jar包 -->
  <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-parent -->
  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.5.14</version>
  </parent>

  <dependencies>
    <!-- web依赖-->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

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

    <!-- lombok-->
    <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
    </dependency>
    
    <!-- fastjson-->
    <dependency>
      <groupId>com.alibaba.fastjson2</groupId>
      <artifactId>fastjson2</artifactId>
      <version>2.0.43</version>
    </dependency>
  </dependencies>
</project>

启动类:

package org.wuya;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

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

测试类:

package org.wuya.controller;

import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

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

@RestController
@RequestMapping("/mytest")
public class FirstController {

    @Resource
    private RedisTemplate redisTemplate;

    /**
     * 测试Redis是否可以正常存取值
     */
    @GetMapping("/redisTest/{value}")
    public String redisTest(@PathVariable String value) {
        redisTemplate.opsForValue().set("food", value, 20, TimeUnit.MINUTES);
        return (String) redisTemplate.opsForValue().get("food");
    }

    /**
     * 测试SpringBoot环境
     */
    @GetMapping("/test")
    public String testSpringBoot() {
        return "SpringBoot项目搭建成功";
    }
}

 application.yaml配置文件:

server:
  port: 8081

spring:
  redis:
    #Redis服务器IP地址(centos105虚拟机)
    host: 192.168.6.105
    port: 6379
    #Redis服务器连接密码(默认为空)
    #password: 123456
    #Redis数据库索引(默认为0)
    database: 0
    #连接超时时间(毫秒)
    timeout: 2000000
    jedis:
      pool:
        #连接池最大连接数(使用负值表示没有限制)
        max-active: 20
        #连接池最大阻塞等待时间(使用负值表示没有限制)
        max-wait: -1
        #连接池中的最大空闲连接
        max-idle: 10
        #连接池中的最小空闲连接
        min-idle: 0

CacheConfig配置类(非必需):

package org.wuya.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CachingConfigurerSupport;
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.GenericJackson2JsonRedisSerializer;

/**
 * Redis配置类,目的是做序列化(Redis会默认使用JdkSerializationRedisSerializer序列化器)
 */
@Configuration
public class CacheConfig extends CachingConfigurerSupport {

    @Autowired
    private RedisConnectionFactory factory;

    /**
     * 向Spring容器注入一个RedisTemplate对象,采用GenericJackson2JsonRedisSerializer这个序列化器进行序列化
     */
    @Bean
    public RedisTemplate<Object, Object> redisTemplate() {
        RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(factory);
        //序列化器
        GenericJackson2JsonRedisSerializer myRedisSerializer = new GenericJackson2JsonRedisSerializer();
        //String类型数据key、value的序列化
        redisTemplate.setKeySerializer(myRedisSerializer);
        redisTemplate.setValueSerializer(myRedisSerializer);
        //hash结构key、value的序列化
        redisTemplate.setHashKeySerializer(myRedisSerializer);
        redisTemplate.setHashValueSerializer(myRedisSerializer);
        return redisTemplate;
    }
}

启动Redis服务端,再运行SpringBoot启动类App.java,然后在浏览器进行访问:

http://localhost:8081/mytest/test

localhost:8081/mytest/redisTest/张三333

二. Redis实现分布式缓存

2.1 原理及好处

优点:

  • 使用Redis作为共享缓存,解决缓存不同步问题
  • Redis是独立的服务,缓存不用占应用本身的内存空间

什么样的数据适合放到缓存中呢?(同时满足以下两个条件)

  • 经常要查询的数据
  • 不经常改变的数据

2.2 数据准备

创建domain包,并创建SystemInfo实体类

package org.wuya.domain;

import lombok.Data;

@Data
public class SystemInfo {
    private Long id;
    private String key;
    private String value;
}

创建SystemController 

package org.wuya.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.wuya.domain.SystemInfo;
import org.wuya.service.SystemService;

import java.util.List;

@RestController
@RequestMapping("/system")
public class SystemController {

    @Autowired
    private SystemService systemService;

    //访问 http://localhost:8081/system/querySystemInfo
    @GetMapping("/querySystemInfo")
    public List<SystemInfo> querySystemInfo() {
        //模拟从数据库中查询数据
        List<SystemInfo> systemInfoList = systemService.querySystemInfo();
        //TODO 页面多次访问上面地址,只要打印一次这句话,表示数据是查询的MySQL数据库
        System.out.println("从数据库中查询到数据~");
        return systemInfoList;
    }
}

创建service包,并创建SystemService,用于准备数据

package org.wuya.service;

import org.springframework.stereotype.Service;
import org.wuya.domain.SystemInfo;

import java.util.ArrayList;
import java.util.List;

@Service
public class SystemService {
    public List<SystemInfo> querySystemInfo() {
        //造10条数据,模拟从数据库中查询数据
        List<SystemInfo> list = new ArrayList<>();
        for (long i = 1; i <= 10; i++) {
            SystemInfo systemInfo = new SystemInfo();
            systemInfo.setId(i);
            systemInfo.setKey("key" + i);
            systemInfo.setValue("波哥" + i);
            list.add(systemInfo);
        }
        return list;
    }
}

测试:

访问上面controller中地址,每刷新一次,控制台打印一次“从数据库中查询到数据~”这句话,表示都是查询的数据库。

2.3 Redis实现分布式缓存

只改动SystemController中的代码即可,具体如下:

package org.wuya.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.wuya.domain.SystemInfo;
import org.wuya.service.SystemService;

import java.util.List;
import java.util.concurrent.TimeUnit;

@RestController
@RequestMapping("/system")
public class SystemController {
    @Autowired
    private SystemService systemService;
    @Autowired
    private RedisTemplate redisTemplate;

    //访问 http://localhost:8081/system/querySystemInfo
    @GetMapping("/querySystemInfo")
    public List<SystemInfo> querySystemInfo() {
        //1.查询Redis缓存,存在数据直接返回
        List<SystemInfo> systemInfoList = (List<SystemInfo>) redisTemplate.opsForValue().get("system:info");
        if (systemInfoList != null) {
            System.out.println("从Redis中取数据");
            return systemInfoList;
        }
        //2.Redis没有数据,查询数据库,往Redis缓存写一份,再返回
        List<SystemInfo> dBsystemInfoList = systemService.querySystemInfo();
        redisTemplate.opsForValue().set("system:info", dBsystemInfoList, 2, TimeUnit.HOURS);
        System.out.println("从数据库中查询到数据~");
        return dBsystemInfoList;
    }
}

测试效果:

思考:为什么以上的代码可以解决分布式缓存?

        因为上面的代码,即使同时在多台服务器部署,也都是先去Redis中查数据,实际查询数据库次数只有一次。

2.4 优雅实现分布式缓存(Redis+AOP+自定义注解)

在上面 2.3 中功能已经实现了,但是有个问题,那就是每个需要做缓存的接口都需要redisTemplate去取和存一下,会产生大量重复代码,这样太不优雅了,下面我们就是
用AOP+自定义注解来消除这些重复代码。

为了避免每次都用redisTemplate操作,创建RedisTool工具类。

第0步:准备RedisTool工具类

创建utils包,将它下面创建RedisTool类:

package org.wuya.utils;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import java.util.concurrent.TimeUnit;

@Component
public class RedisTool {

    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 根据key删除对应的value
     * @param key
     * @return
     */
    public boolean remove(final String key) {
        if (exists(key)) {
            Boolean delete = redisTemplate.delete(key);
            return delete;
        }
        return false;
    }

    /**
     * 根据key删除缓存中是否有对应的value
     */
    public boolean exists(final String key) {
        return redisTemplate.hasKey(key);
    }

    /**
     * 获取锁
     *
     * @param lockKey 锁
     * @param value   身份标识(保证锁不会被其他人释放)
     * @return 获取锁成功返回true,获取锁失败返回false
     */
    public boolean lock(String lockKey, String value) {
        //如有多个线程同时操作的话,只会保证有一个线程把key设置到Redis中成功
        return redisTemplate.opsForValue().setIfAbsent(lockKey, value);
    }

    /**
     * 释放锁
     *
     * @param key
     * @param value
     * @return 释放成功返回true,失败返回false
     */
    public boolean unlock(String key, String value) {
        Object currentValue = redisTemplate.opsForValue().get(key);
        boolean result = false;
        if (StringUtils.hasLength(String.valueOf(currentValue)) && currentValue.equals(value)) {
            result = redisTemplate.opsForValue().getOperations().delete(key);
        }
        return result;
    }

    /**
     * 根据key获得缓存的基本对象
     *
     * @param key
     * @param <T>
     * @return
     */
    public <T> T getCacheObject(final String key) {
        ValueOperations<String, T> valueOperations = redisTemplate.opsForValue();
        return valueOperations.get(key);
    }

    /**
     * 写入缓存设置失效时间
     *
     * @param key
     * @param value
     * @param expireTime
     * @return
     */
    public boolean setEx(final String key, Object value, Long expireTime) {
        boolean result = false;
        try {
            ValueOperations valueOperations = redisTemplate.opsForValue();
            valueOperations.set(key, value);
            redisTemplate.expire(key, expireTime, TimeUnit.SECONDS);
            result = true;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return result;
    }

    /**
     * 缓存基本的对象,Integer、String、实体类等
     *
     * @param key
     * @param value
     * @param timeout
     * @param timeUnit
     * @param <T>
     */
    public <T> void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit) {
        if (timeout == -1) {
            //不设置过期时间,表示永久有效
            redisTemplate.opsForValue().set(key, value);
        } else {
            redisTemplate.opsForValue().set(key, value, timeout, timeUnit);
        }
    }
}

第一步:导入AOP依赖

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

第二步:自定义注解

创建annotation包,在包中定义注解MyCache

package org.wuya.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface MyCache {

    String cacheNames() default "";

    String key() default "";

    //缓存时间(单位秒,默认是无限期)
    int time() default -1;
}

第三步:业务类代码

//访问 http://localhost:8081/system/querySystemInfo2
@GetMapping("/querySystemInfo2")
@MyCache(cacheNames = "system",key = "systeminfo")
public List<SystemInfo> querySystemInfo2() {
    List<SystemInfo> dBsystemInfoList = systemService.querySystemInfo();
    System.out.println("querySystemInfo2从数据库中查询到数据~");
    return dBsystemInfoList;
}

第四步:编写切面类MyCacheAop

创建aop包,在包下编写切面类。

package org.wuya.aop;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.wuya.annotation.MyCache;
import org.wuya.utils.RedisTool;

import java.util.concurrent.TimeUnit;

@Component
@Aspect
public class MyCacheAop {

    @Autowired
    private RedisTool redisTool;

    /**
     * 定义切点(含义:拦截被 @MyCache 标记的方法)
     */
    @Pointcut("@annotation(myCache)")
    public void pointCut(MyCache myCache) {
    }

    /**
     * 环绕通知
     */
    @Around("pointCut(myCache)")
    public Object around(ProceedingJoinPoint joinPoint, MyCache myCache) {
        String cacheNames = myCache.cacheNames();
        String key = myCache.key();
        int time = myCache.time();

        String redisKey = new StringBuilder(cacheNames).append(":").append(key).toString();
        Object redisData = redisTool.getCacheObject(redisKey);
        if (redisData != null) {
            System.out.println("优雅地从Redis分布式缓存中查到数据");
            return redisData;
        }

        Object dbData = null;
        try {
            //Redis缓存中没有数据时,joinPoint执行目标方法
            dbData = joinPoint.proceed();
            //将数据库中查询到的数据存入Redis缓存
            redisTool.setCacheObject(redisKey, dbData, time, TimeUnit.SECONDS);
        } catch (Throwable e) {
            throw new RuntimeException(e);
        }
        return dbData;
    }
}

注意:切面类上除了@Component注解,切得要加上@Aspect注解。

AOP+自定义注解实现分布式缓存的优点: 

三、Redis实现分布式锁

解决高并发库存超卖等问题。

先介绍一下场景:我现在有3台最新款Phone拿出来做秒杀活动,回馈新老客户,只要9.9元,今晚8点开抢,那肯定有很多人来抢。这就是典型的高并发场景,8点会有很多请求进来,可能1秒钟就抢光了,就没有余量了,这种场景我们怎么保证商品不超卖呢?分布式锁!下面我就来模拟一下上面所说的场景,库存我就不用MySQL做了,我就放到Rdis中了,做个缓存预热。

3.1 原理

setnx实现分布式锁原理(见上图):它的特点是设置key到Redis成功,返回true,表示拿到了锁;设置key到Redis失败,返回false,表示没拿到了锁。(对应setIfAbsent这个API)

库存预热:因为秒杀(高并发)场景下,瞬间访问可能倍增,所以需在秒杀活动开始前设置库存到Redis,这样就不会查询数据库了,起到保护数据库的效果。

/**
 * 获取锁
 *
 * @param lockKey 锁
 * @param value   身份标识(保证锁不会被其他人释放)
 * @return 获取锁成功返回true,获取锁失败返回false
 */
public boolean lock(String lockKey, String value) {
    //如有多个线程同时操作的话,只会保证有一个线程把key设置到Redis中成功
    return redisTemplate.opsForValue().setIfAbsent(lockKey, value);
}

3.2 初始化库存

初始化库存,即库存预热,往Redis存数据(存三台手机),在FirstController类中添加如下代码:

@Resource
private RedisTool redisTool;

/**
 * 初始化phone库存为3台
 * @return
 */
// http://localhost:8081/mytest/lock/stockInit
@GetMapping("/stockInit")
public String stockInit() {
    redisTool.setCacheObject("phone", "3", -1, TimeUnit.SECONDS);
    return "初始化库存成功!";
}

3.3 Redis实现分布式锁

        编写秒杀类SeckillController,实现分布式锁。

package org.wuya.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.wuya.utils.RedisTool;

import java.util.concurrent.TimeUnit;

@RestController
@RequestMapping("/seckill")
public class SeckillController {

    @Autowired
    private RedisTool redisTool;

    /**
     * 用户下单接口
     */
    // http://localhost:8081/seckill/saveOrder
    @GetMapping("/saveOrder")
    public ResponseEntity<String> saveOeder() {
        //假如用户下单的商品ID是1001,就是秒杀这个商品(实际应该是用户从前端从过来的)
        String productId = "1001";
        String threadName = Thread.currentThread().getName();
        try {
            //既然是秒杀场景,肯定会有很多请求,即会有很多线程。为了不超卖,这里需要去尝试获取锁
            boolean locked = getLock(productId, threadName);
            //获取到了锁,就可以开始扣减库存了
            if (locked) {
                //这里应该从DB查询得到商品的库存,这里只是模拟,直接从Redis中获取到剩余库存
                Object phone = redisTool.getCacheObject("phone");
                if (phone == null) {
                    ResponseEntity.status(HttpStatus.NOT_FOUND).body("lock_error");
                }
                int phoneStockNum = Integer.parseInt(phone.toString());
                //拿到了锁,不一定就能下单成功,还必须有库存才行,故须加个判断,否则会超卖
                if (phoneStockNum > 0) {
                    System.out.println("线程:" + threadName + " 获取到了锁,还有库存量:" + phoneStockNum);
                    int currentPhoneStockNum = phoneStockNum - 1;
                    redisTool.setCacheObject("phone", currentPhoneStockNum, -1, TimeUnit.SECONDS);
                    System.out.println("线程:" + threadName + "下单成功,扣减之后的剩余量:" + currentPhoneStockNum);
                    return ResponseEntity.status(HttpStatus.OK).body("save phone stock success,current stock:" + currentPhoneStockNum);
                } else {
                    System.out.println("线程:" + threadName + " 获取到了锁,库存已经为0");
                    return ResponseEntity.status(HttpStatus.NOT_FOUND).body("stock is zero");
                }
            }
            //代码走到这里,表示没有抢到锁,那就直接返回友好提示
            return ResponseEntity.status(HttpStatus.NOT_FOUND).body("保存订单失败");
        } finally {
            System.out.println("线程:" + threadName + "释放了锁");
            //TODO 释放锁是productId !!!!!! 不是phone !!!(导致测试一直失败)
            //再次测试时,要把Redis中上次出错的key=1001的key删掉,否则上锁时不能成功!
            //因为上锁的原理是setIfAbsent(lockKey, value),如果存在productId="1001"的key,线程是拿不到锁的!
            redisTool.unlock(productId, threadName);
        }
    }

    //获取锁
    private boolean getLock(String key, String value) {
        boolean lock = redisTool.lock(key, value);
        if (lock) {
            return true;
        } else {
            //递归!!!没有拿到锁的线程继续递归,自旋
            return getLock(key, value);
        }
    }

}

延伸:

  • ResponseEntity是org.springframework.http.ResponseEntity包中的类,以后可以使用;
  • HttpStatus也是org.springframework.http.HttpStatus包中的类,以后可以使用;
  • Assert是org.springframework.util包中的类,以后可以使用;

org.springframework.util包中还有Base64Utils、CollectionUtils、StringUtils、JdkIdGenerator、FileCopyUtils等工具类,都可以直接使用哦。

3.4 JMeter工具测试

总结:锁的是商品ID(productId),抢到锁之后调用Redis的API扣减库存时可以是商品的名称如“phone”,这两个不能是同一个值。加锁时用的API是setIfAbsent,扣库存用的是普通的set方法。

3.5 优雅实现分布式锁(Redis+AOP+自定义注解)

分布式锁的功能上面已经实现了,但如果一个项目中很多地方都需要使用到分布式锁解决一些并发问题的话,那么这这些接口中就都需要写获取锁、释放锁等代码了,非常冗余,此时我们可以利用AOP的思想将重复代码抽取出来。

第一步:自定义注解

package org.wuya.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 用于标记加Redis分布式锁
 */
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface RedisLock {
}

第二步:抽取加锁释放锁的公共代码

抽取后,别忘记业务代码上加@RedisLock注解,切面类上加@Component和@Aspect注解。

业务代码:

/**
 * 用户下单接口(优雅实现Redis分布式锁)
 */
// http://localhost:8081/seckill/saveOrder2
@GetMapping("/saveOrder2")
@RedisLock
public ResponseEntity<String> saveOeder2() {
    //这里应该从DB查询得到商品的库存,这里只是模拟,直接从Redis中获取到剩余库存
    Object phone = redisTool.getCacheObject("phone");
    if (phone == null) {
        ResponseEntity.status(HttpStatus.NOT_FOUND).body("lock_error");
    }
    int phoneStockNum = Integer.parseInt(phone.toString());
    //拿到了锁,不一定就能下单成功,还必须有库存才行,故须加个判断,否则会超卖
    if (phoneStockNum > 0) {
        int currentPhoneStockNum = phoneStockNum - 1;
        redisTool.setCacheObject("phone", currentPhoneStockNum, -1, TimeUnit.SECONDS);
        return ResponseEntity.status(HttpStatus.OK).body("save phone stock success,current stock:" + currentPhoneStockNum);
    } else {
        return ResponseEntity.status(HttpStatus.NOT_FOUND).body("stock is zero");
    }
}

切面类代码:

package org.wuya.aop;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.wuya.annotation.RedisLock;
import org.wuya.utils.RedisTool;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;

/**
 * 被@RedisLock所注解的方法,会被RedisLockAspect进行切面管理
 */
@Slf4j //这个注解是lombok的
@Component
@Aspect
public class RedisLockAspect {

    @Resource
    private RedisTool redisTool;

    //@Around(value = "@annotation(redisLock)", argNames = "joinPoint,redisLock")
    @Around("@annotation(redisLock)")  //这两种注解的写法都行的。MyCacheAop.java中定义切点那两行代码可以删掉
    public Object around(ProceedingJoinPoint joinPoint, RedisLock redisLock) throws Throwable {
        //获取request对象
        ServletRequestAttributes sra = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = sra.getRequest();
        String requestURI = request.getRequestURI();
        //获取入参商品ID
        String productId = requestURI.substring(requestURI.lastIndexOf("/") + 1);

        //TODO 实际开发中是根据上面的方式获取商品ID,这里模拟商品名是1002
        productId = "1002";
        //获取线程名
        String threadName = Thread.currentThread().getName();
        Object result = null;
        try {
            boolean lock = getLock(productId, threadName);
            if (lock) {
                //执行业务逻辑
                log.info("线程:{},获取到了锁,开始处理业务", threadName);
                result = joinPoint.proceed();
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            redisTool.unlock(productId, threadName);
            log.info("线程:{},业务代码处理完毕,锁已释放", threadName);
        }
        return result;
    }

    //获取锁
    private boolean getLock(String key, String value) {
        boolean lock = redisTool.lock(key, value);
        if (lock) {
            return true;
        } else {
            //递归!!!没有拿到锁的线程继续递归,自旋
            return getLock(key, value);
        }
    }

}

  经测试,没问题的。

四、Redis+Token机制实现接口幂等性校验

常见的接口幂等性实现方案有多种方法:

  • 数据库唯一主键;
  • 数据库乐观锁-版本号机制;
  • 防重Token令牌;
  • 分布式锁等等;

Redis+Token机制实现接口幂等性的优点:它的实现方式最优雅,使用比较广泛,简单易于扩展。所以在此介绍防重Token令牌的实现——使用Redis+拦截器+自定义注解,进行实现接口幂等性。

4.1 接口幂等性校验使用场景

4.2 原理图

4.3 编写一般业务代码

下面是有问题的代码,用JMeter并发访问用户下单接口saveOrder(),模拟用户连续点击多次,看到控制台输出N次结果都成功了。这肯定是有问题的!

package org.wuya.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.util.JdkIdGenerator;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.wuya.utils.RedisTool;


@RestController
@RequestMapping("/order")
public class CheckIdempotentController {

    @Autowired
    private RedisTool redisTool;

    /**
     * 获取token
     */
    //访问路径:http://127.0.0.1:8081/order/token
    @GetMapping("/token")
    public ResponseEntity<String> getToken() {
        //得到token
        String token = new JdkIdGenerator().generateId().toString();
        //存入Redis(设置5分钟后过期)(token对应的值不重要)
        boolean result = redisTool.setEx(token, token, 300L);
        if (result) {
            return ResponseEntity.ok(token);
        }
        return ResponseEntity.ok("token error");
    }

    /**
     * 用户下单接口
     */
    //访问路径:http://127.0.0.1:8081/order/saveOrder
    @GetMapping("/saveOrder")
    public ResponseEntity<String> saveOrder() {
        System.out.println("******用户下单成功******");
        //将数据保存在数据库中
        //........
        return ResponseEntity.ok("saveOrder success");
    }

}

4.4 接口幂等性实现步骤

第一步:自定义注解

记得在业务方法上面添加此注解,用于标识该方法需要幂等性校验。

package org.wuya.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 接口幂等性校验的自定义注解
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CheckIdempotent {
}

第二步:定义拦截器

创建interceptor包,在包中创建幂等性校验的拦截器类CheckIdempotentInterceptor

package org.wuya.interceptor;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.wuya.annotation.CheckIdempotent;
import org.wuya.utils.RedisTool;

import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.lang.reflect.Method;

/**
 * 接口幂等性校验的拦截器
 */
@Component
public class CheckIdempotentInterceptor implements HandlerInterceptor {

    @Autowired
    private RedisTool redisTool;

    /**
     * 前置处理,该方法将在处理之前进行调用
     *
     * @param request
     * @param response
     * @param handler
     * @return
     * @throws Exception
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //判断:如果拦截到的请求的目标资源不是方法,那就直接返回true放行即可,我们这里只拦截方法的请求
        if (!(handler instanceof HandlerMethod)) {
            return true;
        }

        HandlerMethod handlerMethod = (HandlerMethod) handler;
        Method method = handlerMethod.getMethod();
        CheckIdempotent checkIdempotentAnnotation = method.getAnnotation(CheckIdempotent.class);
        //判断拦截的目标方法是否被@CheckIdempotent注解标记
        if (checkIdempotentAnnotation != null) {
            //被@CheckIdempotent注解标记时,说明需要幂等性校验,于是就要校验token
            try {
                return checkToken(request);
            } catch (Exception e) {
                writeReturnJson(response, e.getMessage());
                return false;
            }
        }
        //没有被@CheckIdempotent注解标记时,返回true
        return true;
    }

    //返回提示信息给前端
    private void writeReturnJson(HttpServletResponse response, String message) {
        response.reset();
        response.setCharacterEncoding("UTF-8");
        response.setContentType("text/html;charset=utf-8");
        response.setStatus(404);
        ServletOutputStream outputStream = null;
        try {
            outputStream = response.getOutputStream();
            outputStream.print(message);
            outputStream.flush();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (outputStream != null) {
                try {
                    outputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * token校验
     *
     * @param request
     * @return
     */
    private boolean checkToken(HttpServletRequest request) throws Exception {
        //从请求头中获取token的值
        String token = request.getHeader("token");
        if (StringUtils.isEmpty(token)) {
            //请求头中不存在token,那就是非法请求,直接抛异常
            throw new Exception("illegal request");
        }
        //删除Redis中的token
        boolean remove = redisTool.remove(token);
        if (!remove) {
            //删除失败了,说明有其他请求抢先一步删除过了,那么此次请求就不能放行了,属于重复请求
            throw new Exception("token delete error");
        }
        return true;
    }
}

第三步:注册拦截器

只有注册(配置)了拦截器,才能生效。

package org.wuya.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
import org.wuya.interceptor.CheckIdempotentInterceptor;

import javax.annotation.Resource;

/**
 * 统一拦截器配置类
 */
@Configuration
public class WebConfiguration extends WebMvcConfigurationSupport {

    @Resource
    private CheckIdempotentInterceptor checkIdempotentInterceptor;

    //条件拦截器
    @Override
    protected void addInterceptors(InterceptorRegistry registry) {
        //checkIdempotentInterceptor拦截器只对/order/saveOrder请求拦截
        registry.addInterceptor(checkIdempotentInterceptor).addPathPatterns("/order/saveOrder");
        //这里还可以配置(注册)其他类型的拦截器
        //registry.addInterceptor(xxxInterceptor).addPathPatterns("url");
        super.addInterceptors(registry);
    }
}

第四步:测试

  • 首先访问路径:http://127.0.0.1:8081/order/token 生成一个token,同时把这个生成的UUID的token作为key存在了Redis(key对应的value不重要);
  • 然后,选中JMeter“线程组“”下面的“HTTP请求”,右键→添加→配置原件→HTTP信息头管理器,在其中添加token参数,值为刚刚存在Redis中的那个uuid值;
  • 输入请求路径http://127.0.0.1:8081/order/saveOrder等参数,点击测试,效果如上图。

幂等性总结★★★

核心是token校验对token的删除操作(Redis删除key具有原子性),如果删除成功则放行进行执行业务代码,如果失败则进行拦截不会执行业务代码,所以在Redis中存的token(key)的有效期内,同一个用户只能操作一次。

实际开发中如何操作:

  • 在用户首次进入页面,还没有任何操作之前,前端vue就会回调后端的一个方法【这个方法用于生成UUID并将生成的uuid作为Redis的key保存在Redis数据库】,然后给到前端进行解析保存;
  • 当用户填完页面信息点击“提交”按钮时,前端会将token封装在请求参数中向后端发起请求;
  • 后端接收到请求后,先解析请求参数中是否有刚刚存的那个token(token在Redis中存的key为那个uuid),如果有的话,会执行 redisTemplate.delete(uuid);这个方法,如果执行成功才会放行执行业务方法,因为只有一次请求会删除成功,所以就保证了接口幂等性。

五、接口防刷功能

5.1 防刷概述

  • 顾名思义,就是要实现某个接口在某段时间内只能让某人访问指定次数,超出次数,就不让访问了
  • 原理:在请求的时候,服务器通过Rdis记录下你请求的次数,如果次数超过限制就不让访问

具体应用:如发短信验证码,如果无限制让发的话,会产生费用,所以进行限制次数比较好。

实现方法:Redis+拦截器/AOP+自定义注解,实现接口防刷功能。我们这里用拦截器。

5.2 自定义注解

package org.wuya.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimit {

    /**
     * 限流的key
     */
    String key() default "limit:";

    /**
     * 周期,单位是秒
     */
    int cycle() default 5;

    /**
     * 一个周期内允许的请求次数
     */
    int count() default 1;

    /**
     * 默认提示信息
     */
    String msg() default "operation is too fast";

}

5.3 拦截器

package org.wuya.interceptor;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.wuya.annotation.RateLimit;

import javax.annotation.Resource;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import java.io.IOException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.TimeUnit;


/**
 * 限流的拦截器
 */
@Component
public class RateLimitInterceptor implements HandlerInterceptor {

    //@Autowired //这里使用会报错,报错信息和改错见下面图片
    @Resource
    private RedisTemplate<String, Integer> redisTemplate;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //如果请求的是方法,则需要做校验
        if (handler instanceof HandlerMethod) {
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            RateLimit rateLimit = handlerMethod.getMethodAnnotation(RateLimit.class);
            if (rateLimit == null) {
                //拦截的请求的目标方法没有RateLimit注解
                return true;
            }
            //方法上有RateLimit注解,需校验是否在刷接口
            String ip = request.getRemoteAddr();
            String uri = request.getRequestURI();
            String key = "RateLimit:" + ip + ":" + uri;
            if (redisTemplate.hasKey(key)) {
                //如果缓存中存在key,则访问次数+1
                redisTemplate.opsForValue().increment(key, 1);
                if (redisTemplate.opsForValue().get(key) > rateLimit.count()) {
                    System.out.println("操作太频繁了,当前时间:" + getCurrentTime());
                    writeReturnJson(response, rateLimit.msg());
                    return false;
                }
                //未超出访问次数限制,不进行拦截操作,返回true
            } else {
                //第一次设置数据,过期时间为注解确定的访问周期
                redisTemplate.opsForValue().set(key, 1, rateLimit.cycle(), TimeUnit.SECONDS);
                System.out.println("设置过期时间,当前时间:" + getCurrentTime());
            }
            return true;
        }
        //如果请求的不是方法,直接放行
        return true;
    }

    private static String getCurrentTime() {
        LocalDateTime localDateTime = LocalDateTime.now();
        return localDateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss SSS"));
    }

    //返回提示信息给前端
    private void writeReturnJson(HttpServletResponse response, String message) {
        response.reset();
        response.setCharacterEncoding("UTF-8");
        response.setContentType("text/html;charset=utf-8");
        response.setStatus(404);
        ServletOutputStream outputStream = null;
        try {
            outputStream = response.getOutputStream();
            outputStream.print(message);
            outputStream.flush();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (outputStream != null) {
                try {
                    outputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

上面代码中,使用@Autowired注解自动注入RedisTemplate<String, Integer> redisTemplate;时会报错(见下图),而使用@Resource时不会报错。

如果非要使用@Autowired时,可以在任意一个配置类中注入一个redisTemplate的Bean,如下:

@Bean
public RedisTemplate<String, Integer> redisTemplate2() {
    RedisTemplate<String, Integer> redisTemplate = new RedisTemplate<>();
    redisTemplate.setConnectionFactory(factory);
    //序列化器
    GenericJackson2JsonRedisSerializer myRedisSerializer = new GenericJackson2JsonRedisSerializer();
    //String类型数据key、value的序列化
    redisTemplate.setKeySerializer(myRedisSerializer);
    redisTemplate.setValueSerializer(myRedisSerializer);
    //hash结构key、value的序列化
    redisTemplate.setHashKeySerializer(myRedisSerializer);
    redisTemplate.setHashValueSerializer(myRedisSerializer);
    return redisTemplate;
}

 5.4 配置拦截器

在WebConfiguration配置类中,添加上这个防刷功能的拦截器:

/**
 * 统一拦截器配置类
 */
@Configuration
public class WebConfiguration extends WebMvcConfigurationSupport {

    @Resource
    private CheckIdempotentInterceptor checkIdempotentInterceptor;

    @Resource
    private RateLimitInterceptor rateLimitInterceptor;

    //条件拦截器
    @Override
    protected void addInterceptors(InterceptorRegistry registry) {
        //checkIdempotentInterceptor拦截器只对/order/saveOrder请求拦截
        registry.addInterceptor(checkIdempotentInterceptor).addPathPatterns("/order/saveOrder");

        //注册防刷功能的拦截器
        registry.addInterceptor(rateLimitInterceptor).addPathPatterns("/sendSms");

        super.addInterceptors(registry);
    }
}

5.5 业务接口&测试

package org.wuya.controller;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.wuya.annotation.RateLimit;

@RestController
public class RateLimitController {

    /**
     * 发送短信接口:6秒内只能访问两次
     */
    // http://localhost:8081/sendSms
    @GetMapping("/sendSms")
    @RateLimit(cycle = 6,count = 2)
    public ResponseEntity<String> sendSms() {
        System.out.println("成功发送一条短信");
        return ResponseEntity.ok("sendSms success");
    }
}

这样做的好处:如果需要实现防刷功能,添加@RateLimit(cycle = 6,count = 2)自定义注解即可,若不需要,直接去掉该注解即可。与业务代码没有耦合,代码简单。

测试结果没问题(6秒内只能访问两次)。

完整的项目结构:

5.7 延伸:@Resource和@Autowired的区别

以下由GitCode AI回答的。

@Resource 和 @Autowired 都是Spring框架用于依赖注入(DI, Dependency Injection)的注解,但它们有一些关键区别:

  1. 注解来源:

    • @Autowired 是Spring框架自带的核心注解。
    • @Resource 来源于JSR 250规范,被Spring框架支持。
  2. 默认按类型注入:

    • @Autowired 默认按照类型进行自动装配。如果有多个相同类型的bean,需要指定@Qualifier来区分。
    • @Resource 默认按名称进行注入。如果没有指定名称,则会寻找字段名相同的bean。
  3. 属性:

    • @Autowired 提供了required属性,可以设置为false,表示该依赖不是必需的,如果没有找到匹配的bean,Spring不会抛出异常。
    • @Resource 的name属性用来指定需要注入的bean的名字。
  4. 使用位置:

    • @Autowired 更灵活,可以用于字段、构造函数、setter方法以及参数。
    • @Resource 主要用于字段和setter方法,较少用在构造函数参数上。

如何在没有 `@Qualifier` 的情况下解决多个相同类型 Bean 的注入冲突?

在没有 @Qualifier 注解的情况下,处理相同类型 Bean 的注入冲突可以通过以下几种方式:

  1. 使用 Bean 名称作为属性名
    如果 Bean 的名称(默认是方法名)与需要注入的字段名相同,Spring 将自动将该 Bean 注入到字段中,无需任何额外注解。例如:
    @Configuration
    public class AppConfig {
        @Bean
        public MyService myService() {
            return new MyServiceImpl();
        }
    }
    
    @Component
    public class SomeClass {
        private MyService myService;  // Spring 会自动注入 'myService' Bean
    
        // ...
    }
    
  2. 使用 @Primary 注解
    在可能的候选 Bean 中标记一个为 @Primary,Spring 将优先选择这个 Bean 进行自动注入。如果没有其他配置,这可以避免注入冲突。例如:
    @Configuration
    public class AppConfig {
        @Bean
        @Primary
        public MyService primaryMyService() {
            return new MyServiceImpl();
        }
    
        @Bean
        public MyService secondaryMyService() {
            return new MyServiceImpl();
        }
    }
    
  3. 使用构造函数注入
    创建一个带有所需 Bean 类型参数的构造函数,并让 Spring 自动装配。这样可以选择性地提供所需的 Bean。例如:
    @Component
    public class SomeClass {
    
        private final MyService myService;
    
        @Autowired
        public SomeClass(MyService myService) {
            this.myService = myService;
        }
    
        // ...
    }
    
  4. 使用 @Resource 注解
    @Resource 默认按名称进行注入,因此可以避免注入冲突。如果名称不匹配,它会尝试按类型注入。例如:
    @Component
    public class SomeClass {
        @Resource(name = "primaryMyService")
        private MyService myService;
    
        // ...
    }
    
  5. 使用 @Inject 或 JSR-330 提供的注解
    如果你有一个模块化项目并想遵循 Java EE 标准,你可以使用 javax.inject.Inject 注解。这种方式也会考虑 Bean 名称,但如果名称未指定则按类型注入。例如:
    @Component
    public class SomeClass {
        @Inject
        private MyService myService;
    
        // ...
    }
    

请注意,在这些方法中,你可能需要更改配置以确保每个 Bean 都有一个独特的名字或者设置一个首选的 Bean(如使用 @Primary)。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1551449.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

9.HelloWorld案例常见问题

文章目录 一、BUG二、BUG的解决三、HelloWorld常见问题 一、BUG BUG&#xff08;小甲虫&#xff09;。计算机刚开始出现的时候&#xff0c;因为体积比较大&#xff0c;一些小虫子很容易转进去。有一天有一只蟑螂钻到了计算机当中&#xff0c;从而导致计算机不能正常运行&#…

揭秘!抖音严打AI网红骗局,维护虚拟世界秩序!

近年来&#xff0c;AI网红在社交媒体平台上的兴起引发了不少争议。为了规范虚拟人物的内容创作&#xff0c;抖音平台决定对AI网红乱象进行严厉打击&#xff0c;并推出了一系列措施。 AI-321 | 专注于AI工具分享的网站 AI工具集 | 人工智能工具箱 | 全球顶尖AI工具软件推荐与分…

Java的字符串的基础知识(必看)

目录 Java的字符串的基础知识(必看) String API的使用 String概述 创建String对象的两种方式 号比的是什么? 难点 经典String案例 易错点 StringBuilder 疑难点: StringJoiner 字符串相关类的底层原理 中文的存储原理 String的常见的构造方法 Java的字符串的基础…

Avue-crud表格操作栏不显示修改、删除按钮

2024-03-28 奇了怪了&#xff0c;CSDN自动把我之前的文章设置为VIP了&#xff0c;怪不得有时候搜东西看着看着要收费&#xff0c;现在找东西都不好找&#xff0c;我已经反馈不同意了&#xff0c;看看能不能给我取消吧 今天用Avue的时候发现操作栏的按钮没了&#xff0c;按照文…

网络专有名词

网络专有名词 一、子网掩码 IP地址是以网络号和主机号来标示网络上的主机的&#xff0c;我们把网络号相同的主机称之为本地网络&#xff0c;网络号不相同的主机称之为远程网络主机&#xff0c;本地网络中的主机可以直接相互通信&#xff1b;远程网络中的主机要相互通信必须通过…

从零开始学起!全方位解析App压力测试的关键要点!

简介 Monkey 是 Google 提供的一个用于稳定性与压力测试的命令行工具 可以运行在模拟器或者实际设备中 它向系统发送伪随机的用户事件对软件进行稳定性与压力测试 为什么要用 Monkey Monkey 就是像猴子一样上蹿下跳地乱点 为了测试软件的稳定性&#xff0c;健壮性 随机点…

如何使用群晖WebDAV实现固定公网地址同步Zotero文献管理器

文章目录 前言1. Docker 部署 Trfɪk2. 本地访问traefik测试3. Linux 安装cpolar4. 配置Traefik公网访问地址5. 公网远程访问Traefik6. 固定Traefik公网地址 前言 Trfɪk 是一个云原生的新型的 HTTP 反向代理、负载均衡软件&#xff0c;能轻易的部署微服务。它支持多种后端 (D…

Automatic Prompt Engineering

让大模型自己生成prompt&#xff0c;生成提示&#xff08;prompt&#xff09;存在两种不同的操作方式。第一种方式是在文本空间中进行&#xff0c;这种提示以离散的文本形式存在。第二种方式是将提示抽象成一个向量&#xff0c;在特征空间中进行操作&#xff0c;这种提示是抽象…

android安卓看点新闻课设

一、系统需求分析 1.1 引言 1.1.1 开发目的 看点新闻App的开发是为了实时查看最新消息以了解社会动态&#xff0c;增长知识&#xff0c;增广见闻&#xff0c;顺便娱乐一下内心世界来放松自己。 1.1.2 开发背景 随着新媒体的崛起&#xff0c;纸媒遭受到重大打击&#xff0c…

vscode安装vue3+elment-plus

1.用vscode打开打算创建项目的目录 2.命令行中运行以下命令 npm create vuelatest3.设置好项目名称 4.执行以下命令 cd <your-project-name>5.执行以下命令 cnpm install6.执行以下命令安装elment-plus cnpm install element-plus --save7.执行以下命令 npm run dev…

Vuepress 2从0-1保姆级进阶教程——美化与模板

Vuepress 2 专栏目录 1. 入门阶段 Vuepress 2从0-1保姆级入门教程——环境配置篇Vuepress 2从0-1保姆级入门教程——安装流程篇Vuepress 2从0-1保姆级入门教程——文档配置篇Vuepress 2从0-1保姆级入门教程——范例与部署 2.进阶阶段 Vuepress 2从0-1保姆级进阶教程——全文搜索…

使用unplugin-auto-import页面不引入api飘红

解决方案&#xff1a;. tsconfig.json文件夹加上 {"compilerOptions": {"target": "ES2020","useDefineForClassFields": true,"module": "ESNext","lib": ["ES2020", "DOM", &q…

Python程序设计 循环结构(二)

1.斐波那契数列 编写一个能计算斐波那契数列中第x个数的小程序。斐波那契数列&#xff08;Fibonacci sequence&#xff09;&#xff0c;又称黄金分割数列、 因数学家莱昂纳多斐波那契&#xff08;Leonardoda Fibonacci&#xff09;以兔子繁殖为例子而引入&#xff0c;故又称为…

分类任务中的评估指标:Accuracy、Precision、Recall、F1

概念理解 T P TP TP、 T N TN TN、 F P FP FP、 F N FN FN精度/正确率&#xff08; A c c u r a c y Accuracy Accuracy&#xff09; 二分类查准率 P r e c i s i o n Precision Precision&#xff0c;查全率 R e c a l l Recall Recall 和 F 1 − s c o r e F1-score F1−s…

Collection与数据结构 数据结构预备知识(一) :集合框架与时间空间复杂度

1.集合框架 1.1 什么是集合框架 Java集合框架,又被称为容器,是定义在java.util包下的一组接口和接口实现的一些类.其主要的表现就是把一些数据放入这些容器中,对数据进行便捷的存储,检索,管理.集合框架底层实现原理其实就是各种数据结构的实现方法,所以在以后的学习中,我们会…

2024年2月吸尘器行业线上电商(京东天猫淘宝)综合排行榜

鲸参谋监测的线上电商平台&#xff08;淘宝天猫京东&#xff09;2月吸尘器行业销售数据公开。 根据鲸参谋电商数据平台显示&#xff0c;吸尘器行业2月销量累计约53万件&#xff0c;环比上个月下滑29%&#xff0c;同比去年下滑19%&#xff1b;销售额累计约4亿&#xff0c;环比上…

​ ​Redis(五)主从复制:主从模式介绍、配置、拓扑(一主一从结构、一主多从结构、树形主从结构)、原理(复制过程、​​​​​​​数据同步psync)、总结

接上次博客&#xff1a;Redis&#xff08;四&#xff09;&#xff1a;持久化和事务&#xff1a;RDB&#xff08;定期备份&#xff09;【触发机制、流程说明、文件的处理、优缺点】、AOF&#xff08;实时备份&#xff09;【使用AOF、命令写入、文件同步、重写机制、启动时数据恢…

Karmada 管理有状态应用 Xline 的早期探索与实践

背景与动机 目前随着云原生技术和云市场的不断成熟&#xff0c;越来越多的 IT 厂商开始投入到跨云多集群的怀抱当中。以下是 flexera 在 2023 年中关于云原生市场对多云多集群管理的接受程度的调查报告&#xff08;http://info.flexera.com&#xff09; 从 flexera 的报告中可…

学习或复习电路的game推荐:nandgame(NAND与非门游戏)、Turing_Complete(图灵完备)、logisim工具

https://www.nandgame.com/ 免费 https://store.steampowered.com/app/1444480/Turing_Complete/ 收费&#xff0c;70元。据说可以导出 Verilog &#xff01; logisim及其衍生版本 都需要安装java环境。 http://www.cburch.com/logisim/ 是原版&#xff0c; 下载页面&#…

Python实现猜数字游戏:一次编程旅程

博主猫头虎的技术世界 &#x1f31f; 欢迎来到猫头虎的博客 — 探索技术的无限可能&#xff01; 专栏链接&#xff1a; &#x1f517; 精选专栏&#xff1a; 《面试题大全》 — 面试准备的宝典&#xff01;《IDEA开发秘籍》 — 提升你的IDEA技能&#xff01;《100天精通鸿蒙》 …