目次
1.べき等とは何ですか
インターフェイスの冪等性は、ユーザーが開始した結果が複数のクリックによる副作用を引き起こさないことを意味します。たとえば、従来の支払いシナリオでは、ユーザーは製品を購入し、支払いは正常に引き落とされましたが、次のときにネットワークが異常になりまし同一操作
た一次请求或者多次请求
。一致的
結果が返されます。お金が差し引かれており、ユーザーがもう一度ボタンをクリックすると、この時点で 2 回目の差し引きが実行され、返された結果は成功です。ユーザーは残高を確認し、さらにお金が差し引かれていることがわかります。 . セックス。
2. REST スタイルとべき等性
ということで、あとはリクエストするだけですPOST
!
3. 解決策
おそらく主流の解決策:
- トークン メカニズム (フロントエンドはリクエスト ヘッダーにロゴを載せ、バックエンドは検証します)
- ロック機構
- データベース悲観的ロック (ロックテーブル)
- データベースのオプティミスティック ロック (管理するバージョン番号)
- ビジネス層の分散ロック (および分散ロックの再分割)
- グローバルユニークインデックスメカニズム
- Redisの設定の仕組み
- フロントボタンプラスリミット
私たちの解決策は、redis の set メカニズムです。
同じユーザーの場合、POST ストレージに関連するインターフェイスは 1 秒以内に 1 回だけ送信できます。
バックエンドを制御に完全に使用し、フロントエンドを制限することはできますが、エクスペリエンスは良くありません。
バックエンドはカスタム アノテーションを使用して反冪等性を必要とするインターフェイスにアノテーションを追加し、AOP スライシングを使用してビジネスとの結合を軽減します。スライス内のtoken、user_id、url
redisを構成するユーザー固有のキーを取得します。最初のリクエストでは、まずキーが存在するかどうかを判断し、存在しない場合は主キーのキーを Redis に追加し、有効期限を設定します。
例外が発生した場合、キーはアクティブに削除されます。削除に失敗しなかった場合、redis は 1 秒待った後に自動的に削除します。時間の誤差は許容されます。2回目のリクエストが来ると、まずキーが存在するかどうかを判断し、存在する場合は繰り返し送信され、保存されている情報が返されます。!
四、実戦
4.1. 依存関係のインポート
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Druid -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.16</version>
</dependency>
<!--jdbc-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!-- mysql -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- mybatis-plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
4.2、application.yml ファイルを作成する
server:
port: 8081
spring:
redis:
host: localhost
port: 6379
password: 123456
datasource:
#使用阿里的Druid
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/test?serverTimezone=UTC
username: root
password:
4.3、redis シリアル化
/**
* @author yunyan
* @date 2023/6/11 15:20
*/
@Configuration
public class RedisConfig {
@Bean
@SuppressWarnings(value = { "unchecked", "rawtypes" })
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory)
{
RedisTemplate<Object, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
Jackson2JsonRedisSerializer serializer = new Jackson2JsonRedisSerializer(Object.class);
// 使用StringRedisSerializer来序列化和反序列化redis的key值
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(serializer);
// Hash的key也采用StringRedisSerializer的序列化方式
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(serializer);
template.afterPropertiesSet();
return template;
}
}
4.4. カスタム注釈
/**
* 自定义注解防止表单重复提交
* @author yunyan
* @date 2023/6/11 15:25
*/
@Target(ElementType.METHOD) // 注解只能用于方法
@Retention(RetentionPolicy.RUNTIME) // 修饰注解的生命周期
@Documented
public @interface RepeatSubmit {
/**
* 防重复操作过期时间,默认1s
*/
long expireTime() default 1;
}
4.5. スライスの書き込み
/**
* @author 云烟
* @date 2023/6/11 16:00
*/
@Slf4j
@Component
@Aspect
public class RepeatSubmitAspect {
@Autowired
private RedisTemplate redisTemplate;
/**
* 定义切点
*/
@Pointcut("@annotation(com.example.demo.annotation.RepeatSubmit)")
public void repeatSubmit() {}
@Around("repeatSubmit()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder
.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
// 获取防重复提交注解
RepeatSubmit annotation = method.getAnnotation(RepeatSubmit.class);
// 获取token当做key,这里是新后端项目获取不到,先写死
// String token = request.getHeader("Authorization");
String tokenKey = "hhhhhhh,nihao";
if (StringUtils.isBlank(token)) {
throw new RuntimeException("token不存在,请登录!");
}
String url = request.getRequestURI();
/**
* 通过前缀 + url + token 来生成redis上的 key
*
*/
String redisKey = "repeat_submit_key:"
.concat(url)
.concat(tokenKey);
log.info("==========redisKey ====== {}",redisKey);
if (!redisTemplate.hasKey(redisKey)) {
redisTemplate.opsForValue().set(redisKey, redisKey, annotation.expireTime(), TimeUnit.SECONDS);
try {
//正常执行方法并返回
return joinPoint.proceed();
} catch (Throwable throwable) {
redisTemplate.delete(redisKey);
throw new Throwable(throwable);
}
} else {
// 抛出异常
throw new Throwable("请勿重复提交");
}
}
}
4.6. 戻り値の統一
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result<T> {
private Integer code;
private String msg;
private T data;
//成功码
public static final Integer SUCCESS_CODE = 200;
//成功消息
public static final String SUCCESS_MSG = "SUCCESS";
//失败
public static final Integer ERROR_CODE = 201;
public static final String ERROR_MSG = "系统异常,请联系管理员";
//没有权限的响应码
public static final Integer NO_AUTH_COOD = 999;
//执行成功
public static <T> Result<T> success(T data){
return new Result<>(SUCCESS_CODE,SUCCESS_MSG,data);
}
//执行失败
public static <T> Result failed(String msg){
msg = StringUtils.isEmpty(msg)? ERROR_MSG : msg;
return new Result(ERROR_CODE,msg,"");
}
//传入错误码的方法
public static <T> Result failed(int code,String msg){
msg = StringUtils.isEmpty(msg)? ERROR_MSG : msg;
return new Result(code,msg,"");
}
//传入错误码的数据
public static <T> Result failed(int code,String msg,T data){
msg = StringUtils.isEmpty(msg)? ERROR_MSG : msg;
return new Result(code,msg,data);
}
}
4.7. 単純な例外処理
/**
* @author yunyan
* @date 2023/6/11 16:05
*/
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(value = Throwable.class)
public Result handleException(Throwable throwable){
log.error("错误",throwable);
return Result.failed(500, throwable.getCause().getMessage());
}
}
4.8、コントローラーのテスト
/**
* @author yunyan
* @date 2023/6/11 16:20
*/
@RestController
@RequestMapping("/test")
public class TestController {
@Autowired
private SysLogService sysLogService;
// 默认1s,方便测试查看,写10s
@RepeatSubmit(expireTime = 10)
@PostMapping("/saveSysLog")
public Result saveSysLog(@RequestBody SysLog sysLog){
return Result.success(sysLogService.saveSyslog(sysLog));
}
}
4.9、サービス
/**
* @author yunyan
* @date 2023/6/11 16:40
*/
@Service
public class SysLogServiceImpl implements SysLogService {
@Autowired
private SysLogMapper sysLogMapper;
@Override
public int saveSyslog(SysLog sysLog) {
return sysLogMapper.insert(sysLog);
}
}
5. テスト
5.1、テスト用の郵便配達員
入力リクエスト: http://localhost:8081/test/saveSysLog
リクエストパラメータ:
{
"title":"你好",
"method":"post",
"operName":"我是测试幂等性的"
}
リクエストを 2 回送信します。
データベースを確認すると、正常に保存されたのは 1 つだけであることがわかります。
5.2、redisKey の表示
6. まとめ
これにより冪等の問題が解決され、間違ったデータがなくなり、バグの提出が 1 件減りました。これは誰もが注意を払わなければならない問題であり、解決しなければ問題が発生する可能性があります。