生活中避免因为忘记,导致重复做某件事.方法就是没做完一件事就拿笔记录一次,放到程序也一样.接口被调用了,就使用Redis记录一次.要想短时间内不能调用接口,就记好开始时间.下次再调用就检查时间间隔.为了知道是不是同一个人调用,所以还需要存这个人唯一标志.
幂等性解决方案:
数据库字段添加唯一约束
锁机制(数据库乐观锁/悲观锁,业务层的分布式锁)
redis的set防重复
全局请求唯一id (验证码)
防重表(原理跟3是类似)
redis 接口计时防刷:
/**
* User: ldj
* Date: 2022/9/22
* Time: 20:15
* Description: 使用restTemplate发送http请求获取短信验证码
*/
@Slf4j
@Component
public class SmsHandler {
@Autowired
private RestTemplate restTemplate;
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private SmsProperties smsProperties;
public void sendSmsCode(String phoneNum) {
//TODO 接口计时防暴刷
//第一种方案(推荐): Redis设置过期时间60s
String redisValue = redisTemplate.opsForValue().get(SmsConstant.SMS_CODE_CACHE_PREFIX + phoneNum);
if (StringUtils.isNotBlank(redisValue)) {
return;
}
//第二种方案(不推荐): 如果缓存时间设置长于60s
//if (StringUtils.isNotBlank(redisValue)) {
// long lastRequestTime = Long.parseLong(redisValue.split("_")[1]);
// //距离上次请求时间间隔不超过60s,不能再发送
// if (System.currentTimeMillis() - lastRequestTime < 60 * 1000){
// return;
// }
//}
Long requestTime = System.currentTimeMillis();
UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromHttpUrl(smsProperties.getUrl());
URI uri = uriComponentsBuilder.build().encode(StandardCharsets.UTF_8).toUri();
//请求头
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.add(SmsConstant.AUTHORIZATION, smsProperties.getAppCode());
httpHeaders.setContentType(MediaType.valueOf(MediaType.APPLICATION_FORM_URLENCODED_VALUE));
String smsCode = SerialNumUtils.getRandomNumber(6);
MultiValueMap<String, String> requestMap = new LinkedMultiValueMap<>();
requestMap.add(SmsConstant.PHONE_NUMBER, phoneNum);
requestMap.add(SmsConstant.TEMPLATE_KEY, smsProperties.getTemplateId());
requestMap.add(SmsConstant.CONTENT, SmsConstant.CODE + smsCode);
RequestEntity<MultiValueMap<String, String>> requestEntity = RequestEntity
.post(uri)
.headers(httpHeaders)
.body(requestMap);
ResponseEntity<ResponseDTO> responseEntity = null;
try {
responseEntity = restTemplate.exchange(requestEntity, ResponseDTO.class);
} catch (Exception e) {
log.error("[RestTemplate] (SmsComponent) http request error", e);
}
SmsHandler.dataHandler(responseEntity, phoneNum, smsCode, requestTime, redisTemplate);
}
private static void dataHandler(ResponseEntity<ResponseDTO> responseEntity,
String phoneNum,
String smsCode,
Long requestTime,
StringRedisTemplate redisTemplate) {
if (Objects.isNull(responseEntity)) {
log.warn("[RestTemplate] responseEntity is null");
return;
}
HttpStatus statusCode = responseEntity.getStatusCode();
log.info("[RestTemplate] statusCode:[{}]", statusCode.value());
//2.验证码发送成功后,在Redis记录请求的开始时间
if (Objects.equals(200, statusCode.value())) {
String value = smsCode + "_" + requestTime;
redisTemplate.opsForValue().set(SmsConstant.SMS_CODE_CACHE_PREFIX + phoneNum, value, 1, TimeUnit.MINUTES);
log.info(JacksonUtils.writeValueAsString(responseEntity.getBody()));
}
}
}
后续补充: 计时防刷计数器也可以实现(推荐)
![](https://i-blog.csdnimg.cn/blog_migrate/4317bab80a40d2cf733c24f5767f3cc1.png)
redis防重复提交实现:
@Autowired
private StringRedisTemplate redisTemplate;
@Test
void redisTest() {
//用户id+商品id
String order = "user123456:goods65769579476444";
//MD5加密处理
String encryptOrder = encrypt(order);
//模拟用户多次点击提交订单
BoundSetOperations<String, String> operations = redisTemplate.boundSetOps("name");
for (int i = 0; i < 3; i++) {
operations.add(order);
operations.expire(30, TimeUnit.MINUTES);
}
Boolean member = operations.isMember(encryptOrder);
if (member != null && member) {
//本次订单已经提交过了....返回空数据,提示重复提交
}else {
//提交订单
}
}
//java中实现MD5加密方式
private static String encrypt(String pwd) {
String encryptedPwd = null;
try {
MessageDigest md = MessageDigest.getInstance("MD5");
md.update(pwd.getBytes());
encryptedPwd = new BigInteger(1, md.digest()).toString(16);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
return encryptedPwd;
}