Redis项目实战

news/2024/4/27 18:22:30/文章来源:https://blog.csdn.net/2301_81298401/article/details/136990516

本文用用代码演示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.luyixian.cn/news_show_1026689.aspx

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

相关文章

软件杯 深度学习 机器视觉 人脸识别系统 - opencv python

文章目录 0 前言1 机器学习-人脸识别过程人脸检测人脸对其人脸特征向量化人脸识别 2 深度学习-人脸识别过程人脸检测人脸识别Metric Larning 3 最后 0 前言 &#x1f525; 优质竞赛项目系列&#xff0c;今天要分享的是 &#x1f6a9; 深度学习 机器视觉 人脸识别系统 该项目…

stm32之GPIO寄存器

文章目录 1 背景2 GPIO寄存器的类型2.1 端口配置寄存器2.2 设置/清除寄存器和位清除寄存器 3 总结 1 背景 C51单片机在进行数据的输入输出时&#xff0c;是直接操作与外部引脚关联的内部寄存器&#xff0c;例如&#xff0c;当设置P2_1为0时&#xff0c;就是将外部引脚的P21引脚…

Spark DAG

Spark DAG 什么是DAG DAG 是一组顶点和边的组合。顶点代表了 RDD&#xff0c; 边代表了对 RDD 的一系列操作。 DAG Scheduler 会根据 RDD 的 transformation 动作&#xff0c;将 DAG 分为不同的 stage&#xff0c;每个 stage 中分为多个 task&#xff0c;这些 task 可以并行运…

后端前行Vue之路(一):初识Vue

1.Vue是什么 Vue (读音 /vjuː/&#xff0c;类似于 view) 是一套用于构建用户界面的渐进式框架。与其它大型框架不同的是&#xff0c;Vue 被设计为可以自底向上逐层应用。Vue 的核心库只关注视图层&#xff0c;不仅易于上手&#xff0c;还便于与第三方库或既有项目整合。另一方…

2.6、媒体查询(mediaquery)

概述 媒体查询作为响应式设计的核心&#xff0c;在移动设备上应用十分广泛。媒体查询可根据不同设备类型或同设备不同状态修改应用的样式。媒体查询常用于下面两种场景&#xff1a; 针对设备和应用的属性信息&#xff08;比如显示区域、深浅色、分辨率&#xff09;&#xff0…

兼容 Presto、Trino、ClickHouse、Hive 近 10 种 SQL 方言,Doris SQL Convertor 解读及实操演示

随着版本迭代&#xff0c;Apache Doris 一直在拓展应用场景边界&#xff0c;从典型的实时报表、交互式 Ad-hoc 分析等 OLAP 场景到湖仓一体、高并发数据服务、日志检索分析及批量数据处理&#xff0c;越来越多用户与企业开始将 Apache Doris 作为统一的数据分析产品&#xff0c…

Vue3气泡卡片(Popover)

效果如下图&#xff1a;在线预览 APIs 参数说明类型默认值必传title卡片标题string | slot‘’falsecontent卡片内容string | slot‘’falsemaxWidth卡片内容最大宽度string | number‘auto’falsetrigger卡片触发方式‘hover’ | ‘click’‘hover’falseoverlayStyle卡片样式…

不可变集合及Stream流

若希望某个数据是不可修改的&#xff0c;就可以考虑使用不可变集合&#xff0c;以提高安全性&#xff1b;&#xff08;JKD9之后才有&#xff09; List不可变集合&#xff1a; public static void main(String[] args) {/*创建不可变的List集合"张三", "李四&q…

蓝桥杯练习06给网页化个妆

给页面化个妆 介绍 各个网站都拥有登录页面&#xff0c;设计一个界面美观的登录页面&#xff0c;会给用户带来视觉上的享受。本题中我们要完成一个登录页面的布局。 准备 开始答题前&#xff0c;需要先打开本题的项目代码文件夹&#xff0c;目录结构如下&#xff1a; 其中&…

蓝桥杯2019年第十届省赛真题-组队

一、题目 组队 题目描述 作为篮球队教练&#xff0c;你需要从以下名单中选出 1 号位至 5 号位各一名球员&#xff0c; 组成球队的首发阵容。每位球员担任 1 号位至 5 号位时的评分如下表所示。请你计算首发阵容 1 号位至 5 号位的评分之和最大可能是多少&#xff1f; &#xff…

ubuntu - 编译 linphone-sdk

业务需求需要定制sdk&#xff0c;首先声明我们需要的是在Android4.4上跑的sdk&#xff0c;因此本次编译的sdk最低支持为19&#xff08;不同版本需要的环境不一致&#xff09;&#xff0c;编译过程较容易&#xff0c;难点在于环境配置 环境准备 Ubuntu 18.04.6 android-sdk_r24.…

面试题:Java虚拟机JVM的组成

1. 基础概念 JVM是什么 Java Virtual Machine Java程序的运行环境&#xff08;java二进制字节码的运行环境&#xff09; 好处&#xff1a; 一次编写&#xff0c;到处运行 自动内存管理&#xff0c;垃圾回收机制 JVM由哪些部分组成&#xff0c;运行流程是什么&#xff1f; …

每日一题 --- 移除链表元素[力扣][Go]

移除链表元素 题目&#xff1a;203. 移除链表元素 给你一个链表的头节点 head 和一个整数 val &#xff0c;请你删除链表中所有满足 Node.val val 的节点&#xff0c;并返回 新的头节点 。 示例 1&#xff1a; 输入&#xff1a;head [1,2,6,3,4,5,6], val 6 输出&#xf…

ViTAR: Vision Transformer with Any Resolution

ViTAR: Vision Transformer with Any Resolution 相关链接&#xff1a;arxiv 关键字&#xff1a;Vision Transformer、Resolution Adaptability、Adaptive Token Merger、Fuzzy Positional Encoding、High-Resolution Image Processing 摘要 本文解决了视觉Transformer&#x…

【ORB-SLAM3】在 Ubuntu20.04 上编译 ORM-SLAM3 并使用 D435i、EuRoC 和 TUM-VI 运行测试

【ORB-SLAM3】在 Ubuntu20.04 上编译 ORM-SLAM3 并使用 D435i、EuRoC 和 TUM-VI 运行测试 1 Prerequisites1.1 C11 or C0x Compiler1.2 Pangolin1.3 OpenCV1.4 Eigen3 2 安装 Intel RealSense™ SDK 2.02.1 测试设备2.2 编译源码安装 (Recommend)2.3 预编译包安装 3 编译 ORB-S…

[密码学] 密码学基础

目录 一 为什么要加密? 二 常见的密码算法 三 密钥 四 密码学常识 五 密码信息威胁 六 凯撒密码 一 为什么要加密? 在互联网的通信中&#xff0c;数据是通过很多计算机或者通信设备相互转发&#xff0c;才能够到达目的地,所以在这个转发的过程中&#xff0c;如果通信包…

常见的三种办公室租赁方式各自优缺点

商业办公的租赁市场。找商业办公地点&#xff0c;跟找住宅租房有点像&#xff0c;但目的和要求不同。主要也是三种方式&#xff1a;直接找房东租、接手别人的转租&#xff0c;或者找中介帮忙。每种方式都有它的小窍门和注意事项。 直租 直租商业办公&#xff0c;就是直接和办公…

GPT提示词分享 —— 代码释义者

提示词&#x1f447; 我希望你能充当代码解释者&#xff0c;阐明代码的语法和语义。 3.5版本&#x1f447; free2gpt 4.0版本&#x1f447; gpt4

互联网医院APP开发攻略:搭建智能医疗平台

互联网医院APP为患者提供了便捷的就医途径&#xff0c;还为医生和医院提供了更加高效的服务和管理手段。接下来&#xff0c;小编将我们本文将就互联网医院APP的开发攻略&#xff0c;以及如何搭建智能医疗平台进行探讨。 1.确定需求和目标 这包括确定服务对象&#xff08;患者、…

鸿蒙HarmonyOS应用开发之C/C++标准库机制概述

OpenHarmony NDK提供业界标准库 libc标准库、 C标准库 &#xff0c;本文用于介绍C/C标准库在OpenHarmony中的机制&#xff0c;开发者了解这些机制有助于在NDK开发过程中避免相关问题。 1. C兼容性 在OpenHarmony系统中&#xff0c;系统库与应用Native库都在使用C标准库&#…