Redis项目实战

本文用用代码演示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 {@Resourceprivate 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: 8081spring:redis:#Redis服务器IP地址(centos105虚拟机)host: 192.168.6.105port: 6379#Redis服务器连接密码(默认为空)#password: 123456#Redis数据库索引(默认为0)database: 0#连接超时时间(毫秒)timeout: 2000000jedis: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 {@Autowiredprivate RedisConnectionFactory factory;/*** 向Spring容器注入一个RedisTemplate对象,采用GenericJackson2JsonRedisSerializer这个序列化器进行序列化*/@Beanpublic 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 {@Autowiredprivate 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 {@Autowiredprivate SystemService systemService;@Autowiredprivate 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 {@Autowiredprivate 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 {@Autowiredprivate 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 {@Autowiredprivate 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 {@Resourceprivate 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();//获取入参商品IDString productId = requestURI.substring(requestURI.lastIndexOf("/") + 1);//TODO 实际开发中是根据上面的方式获取商品ID,这里模拟商品名是1002productId = "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 {@Autowiredprivate RedisTool redisTool;/*** 获取token*///访问路径:http://127.0.0.1:8081/order/token@GetMapping("/token")public ResponseEntity<String> getToken() {//得到tokenString 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 {@Autowiredprivate RedisTool redisTool;/*** 前置处理,该方法将在处理之前进行调用** @param request* @param response* @param handler* @return* @throws Exception*/@Overridepublic 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注解标记时,说明需要幂等性校验,于是就要校验tokentry {return checkToken(request);} catch (Exception e) {writeReturnJson(response, e.getMessage());return false;}}//没有被@CheckIdempotent注解标记时,返回truereturn 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中的tokenboolean 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 {@Resourceprivate CheckIdempotentInterceptor checkIdempotentInterceptor;//条件拦截器@Overrideprotected 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 //这里使用会报错,报错信息和改错见下面图片@Resourceprivate RedisTemplate<String, Integer> redisTemplate;@Overridepublic 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,则访问次数+1redisTemplate.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 {@Resourceprivate CheckIdempotentInterceptor checkIdempotentInterceptor;@Resourceprivate RateLimitInterceptor rateLimitInterceptor;//条件拦截器@Overrideprotected 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 {@Beanpublic 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@Primarypublic MyService primaryMyService() {return new MyServiceImpl();}@Beanpublic MyService secondaryMyService() {return new MyServiceImpl();}
    }
    
  3. 使用构造函数注入
    创建一个带有所需 Bean 类型参数的构造函数,并让 Spring 自动装配。这样可以选择性地提供所需的 Bean。例如:
    @Component
    public class SomeClass {private final MyService myService;@Autowiredpublic 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 {@Injectprivate MyService myService;// ...
    }
    

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

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

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

相关文章

知行之桥EDI系统功能介绍——FlatFile 端口介绍

FlatFile 端口能够实现平面文件与XML文件的互相转换。 每个 Flat File 端口配置一个特定的平面文件格式&#xff0c;从而实现与 XML 格式的互相转换。Flat File 端口有两个主要的模式&#xff1a; Position DelimitedCharacter Delimited 对于 Position Delimited 平面文件&a…

【Git篇】复习git

文章目录 &#x1f354;什么是git⭐git和svn的区别 &#x1f354;搭建本地仓库&#x1f354;克隆远程仓库&#x1f6f8;git常用命令 &#x1f354;什么是git Git是一种分布式版本控制系统&#xff0c;它可以追踪文件的变化、协调多人在同一个项目上的工作、恢复文件的旧版本等…

在宝塔面板中,为自己的云服务器安装SSL证书,为所搭建的网站启用https(主要部分攻略)

前提条件 My HTTP website is running Nginx on Debian 10&#xff08;或者11&#xff09; 时间&#xff1a;2024-3-28 16:25:52 你的网站部署在Debain 10&#xff08;或者11&#xff09;的 Nginx上 安装单域名证书&#xff08;默认&#xff09;&#xff08;非泛域名&#xf…

现在做抖音小店都需要准备什么?需要什么条件?门槛很高吗?

大家好&#xff0c;我是电商花花。 自从抖音小店这个项目做的人越来越多&#xff0c;很多人都想赶上抖音小店这个红利项目&#xff0c;但是很多新手在刚开始接触这个项目时候因为不懂&#xff0c;开始频频踩雷&#xff0c;不得不关店重新再来。 我们今天汇总了一下抖音小店的…

OSCP靶场--image

OSCP靶场–image 考点(CVE-2023-34152 suid strace提权) 1.nmap扫描 ## ┌──(root㉿kali)-[~/Desktop] └─# nmap -Pn -sC -sV 192.168.178.178 --min-rate 2500 Starting Nmap 7.92 ( https://nmap.org ) at 2024-03-27 23:43 EDT Nmap scan report for 192.168.178.17…

如何在群晖NAS搭建bitwarden密码管理软件并实现无公网IP远程访问

前言 作者简介&#xff1a; 懒大王敲代码&#xff0c;计算机专业应届生 今天给大家聊聊如何在群晖NAS搭建bitwarden密码管理软件并实现无公网IP远程访问&#xff0c;希望大家能觉得实用&#xff01; 欢迎大家点赞 &#x1f44d; 收藏 ⭐ 加关注哦&#xff01;&#x1f496;&am…

降分违规?90%新手会遇到的抖音小店运营问题!解决方法快围观!

哈喽~我是电商月月 今天我们聊聊新手开抖音小店会遇到的问题以及解决方法 为了完整性我们从头到尾分析&#xff0c;根据情况不同可自行翻阅 一&#xff0c;入驻和运营时的操作问题 1.营业执照的办理&#xff0c;选择&#xff0c;填写 营业执照的办理可以去当地工商局办理&…

迭代器模式(统一对集合的访问方式)

目录 前言 UML plantuml 类图 实战代码 Iterator ArrayList Client 自定义迭代器 TreeNode TreeUtils Client 前言 在实际开发过程中&#xff0c;常用各种集合来存储业务数据并处理&#xff0c;比如使用 List&#xff0c;Map&#xff0c;Set 等等集合来存储业务数…

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

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

Linux:环境变量的特性及获取

目录 一、环境变量基本概念 1.1命令行参数 1.2常见环境变量 二、环境变量相关指令 创建本地变量 三、环境变量通常是具有全局属性的 一、环境变量基本概念 环境变量(environment variables)不是一个而是一堆&#xff0c;彼此之间其实没有关系。本质上是为了解决不同场景下…

如何用智能AI绘一幅世界地图?

今天我们分享一下&#xff0c;用智能AI绘一幅世界地图的方法&#xff01; 为了方便你极速体验&#xff0c;特意在文末为你准备了登录帐号&#xff0c;省去你注册的烦恼。 认准AI绘画官网 如果你在百度搜索“AI绘画”或“Midjourney”&#xff0c;找出来的基本全是广告&#…

MSTP环路避免实验

思科设备参考&#xff1a; 一&#xff0c;技术简介 MSTP&#xff08;多生成树协议&#xff09;&#xff0c;MSTP解决了STP和RSTP没有考虑vlan的问题&#xff0c;STP和RSTP将所有的vlan共享为一个生成树实例&#xff0c;无法实现负载分担&#xff0c;这样就导致了网络中一些设…

【深度学习】YOLOv8:别再pip install ultralytics了

&#x1f525;博客主页&#xff1a; A_SHOWY&#x1f3a5;系列专栏&#xff1a;力扣刷题总结录 数据结构 云计算 数字图像处理 力扣每日一题_ 随着YOLOv8的版本不断更新&#xff0c;最新的几个版本会发现没有requirements.txt和setup.py&#xff0c;在安装包的依赖的时候&…

PCL点云处理之M估计样本一致性(MSAC)平面拟合(二百三十六)

PCL点云处理之M估计样本一致性(MSAC)平面拟合(二百三十五六) 一、算法介绍二、使用步骤1.代码2.效果一、算法介绍 写论文当然用RANSAC的优化变种算法MSAC啊,RANSAC太土太LOW了哈哈 MSAC算法(M-estimator Sample Consensus)是RANSAC(Random Sample Consensus)的一种…

Linux系统使用Docker部署Portainer结合内网穿透实现远程管理容器和镜像

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…

Tickeys for Mac:让每一次敲击都充满乐趣,提升打字体验新高度!

Tickeys for Mac 是一款为 macOS 设计的虚拟键盘音效生成器。它通过模拟机械键盘的声音&#xff0c;为打字和输入操作增添了音效反馈&#xff0c;让用户在使用电脑时感受到更加真实的键盘反馈体验。用户可以根据个人喜好选择不同类型的键盘声音和音效设置&#xff0c;让键盘操作…

【Python基础篇】那些年错过的Python

随着OpenAI的发展&#xff0c;Python的重要性不言而喻。不知你是否和我一样&#xff0c;不知道曾经说过多少次我要学Python&#xff0c;都没有执行起来… 近期我在知识库中更新了一波Python教程&#xff0c;选取了这一篇分享给大家。 前言 很多时候我们需要让程序变成交互性的…

实现DevOps需要什么?

实现DevOps需要什么&#xff1f; 硬性要求&#xff1a;工具上的准备 上文提到了工具链的打通&#xff0c;那么工具自然就需要做好准备。现将工具类型及对应的不完全列举整理如下&#xff1a; 代码管理&#xff08;SCM&#xff09;&#xff1a;GitHub、GitLab、BitBucket、SubV…

Notepad++:格式化json字符串(带转义)

目录 一、效果呈现 二、去除json字符串转义 三、格式化json字符串 一、效果呈现 格式化前 带字符串转义&#xff0c;带unicode编码字符 格式化后 二、去除json字符串转义 方法&#xff1a;采用Notepad的普通替换 第一&#xff1a;\"替换为" 第二&#xff1a;\\…

函数模板详解

大家好&#xff1a; 衷心希望各位点赞。 您的问题请留在评论区&#xff0c;我会及时回答。 一、函数模板 C另一种编程思想为&#xff1a;泛型编程&#xff0c;主要利用的技术就是模板。 C提供两种模板机制&#xff1a;函数模板、类模板 函数模板语法 函数模板作用&#xff…