Введение
Что касается повторных запросов, это означает, что наш сервер получает несколько повторных запросов на один и тот же контент в течение короткого периода времени. Если такие повторные запросы идемпотентны (результат каждого запроса один и тот же, например, запрос запроса), то он фактически не оказывает на нас никакого влияния, но если он неидемпотент (каждый запрос будет влиять на ключевые данные, такие как Удалить отношение , установить взаимосвязь и т. д.), то в легком случае будут выдаваться грязные данные, а в тяжелом — системная ошибка.
Таким образом, в текущей ситуации широко распространенных распределенных сервисов, как избежать и устранить аномалию данных, вызванную повторными запросами, стало неотложной проблемой, требующей решения. Чтобы избежать повторных запросов, лучше всего делать это вместе.
1. Фронтенд или клиент напрямую запрещает отправку повторных запросов на неидемпотентные кнопки.
2. Серверная часть блокируется при получении запроса и разблокируется после завершения.
В этом блоге в основном рассказывается об общем решении для решения повторяющихся запросов, основанном на концепции распределенных блокировок в бэкенде.
2. Текст
Зачем использовать распределенные блокировки для ее решения? Поскольку наша текущая общая архитектура представляет собой распределенный сервер, внешний запрос перенаправляется на внутренний через уровень шлюза, как показано на рисунке ниже, поэтому, если ограничен только один сервер, он не может быть выполнен в распределенном режиме. сервис Отвечать на часто повторяющиеся запросы.
Основная мысль
Идея в основном состоит в том, чтобы добавить к интерфейсу распределенную блокировку, которая должна предотвратить повторные запросы.Шаги следующие:
- После получения запроса берем значение md5 по имени метода + параметру и получаем уникальный идентификатор метода и параметра;
- После получения удостоверения установите распределенную блокировку и установите срок действия;
- После завершения запроса снимите распределенную блокировку.
Запрет повторного запроса для текущего запроса может быть завершен. Если вы хотите сделать общее решение, вам нужно сделать небольшую функцию из вышеперечисленных шагов.Поскольку я знаком с фреймворками java и spring, я возьму это в качестве примера.
Реализация на основе весеннего аспекта и Redis
Предположительно, некоторые студенты, знакомые с Spring, уже знают, какой метод я хочу использовать.Для общих целей мы должны использовать функции aop Spring, аннотацию + аспект + md5key + отражение + реализацию Redis, как показано ниже:
- Определите аннотацию распределенной блокировки, аннотация содержит настройку времени истечения срока действия и параметры игнорирования;
- Определите аспект, точкой пересечения является аннотация распределенной блокировки, получите имя метода, параметры и время истечения срока действия, которые необходимо использовать для распределенной блокировки в аспекте, и используйте имя метода и параметры, которые не игнорируются как md5 для получения уникальный идентификатор;
- Затем установите распределенную блокировку redsis в соответствии с указанным выше уникальным идентификатором;
- Разблокировать после окончания метода.
код показывает, как показано ниже:
аннотация
Определите аннотацию с именем RepeatOperationLock.Параметры включают время истечения блокировки и атрибуты игнорирования (то есть атрибуты, которые не участвуют в вычислении идентификации распределенной блокировки MD5).
@Documented
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Component
public @interface RepeatOperationLock {
/**
* 锁时长,默认500ms
* @return
*/
long timeOut() default 500;
/**
* 忽略上锁参数位置,从0开始
* @return
*/
int[] ignoreIndex();
}
раздел
Pointcut - это вышеуказанная аннотация.В аспекте выполняются следующие действия, включая получение имени метода, получение атрибутов аннотации (время истечения срока действия, атрибуты игнорирования), вычисление метода + значение атрибута md5 и вызов метода внешний распределенный замок.
@Aspect
@Slf4j
@Component
public class LockAspect {
@Autowired
RepeatLockService repeatLockService;
@Pointcut("@annotation(com.ls.javabase.aspect.annotation.RepeatOperationLock)")
public void serviceAspect() {
}
@Before("serviceAspect()")
public void setLock(JoinPoint point) {
log.info("防止方法重复调用接口锁,上锁,point:{}", point);
Method method = ((MethodSignature) point.getSignature()).getMethod();
RepeatOperationLock repeatOperationLock = method.getAnnotation(RepeatOperationLock.class);
if (Objects.isNull(repeatOperationLock)) {
log.warn("---repeatOperationLock is null---");
return;
}
long timeOut = repeatOperationLock.timeOut();
int [] ignoreIndex = repeatOperationLock.ignoreIndex();
log.info("lockTime——{}", timeOut);
if (Objects.isNull(timeOut)) {
log.warn("---timeOut is null");
return;
}
String methodName = method.getName();
Object[] args = point.getArgs();
repeatLockService.setRepeatLock(methodName, args, timeOut);
}
@After("serviceAspect()")
public void removeLock(JoinPoint point) {
log.info("防止方法重复调用接口锁,解锁,point:{}",point);
Method method = ((MethodSignature) point.getSignature()).getMethod();
RepeatOperationLock repeatOperationLock = method.getAnnotation(RepeatOperationLock.class);
if (Objects.isNull(repeatOperationLock)) {
log.warn("---repeatOperationLock is null---");
return;
}
long timeOut = repeatOperationLock.timeOut();
log.info("lockTime——{}", timeOut);
if (Objects.isNull(timeOut)) {
log.warn("---timeOut is null");
return;
}
String methodName = method.getName();
Object[] args = point.getArgs();
repeatLockService.removeRepeatLock(methodName, args);
}
/**
*
* @param args
* @param ignoreIndex
* @return
*/
private Object [] getEffectiveArgs(Object[] args,int [] ignoreIndex) {
for (int i:ignoreIndex){
args[i] = null;
}
for (Object obj:args){
if (obj==null){
}
}
return args;
}
}
метод md5
public class Md5Encode {
/**
* constructors
*/
private Md5Encode() {
}
/**
* @param s 需要hash的字符串
* @return hash之后的字符串
*/
public static final String md5(final String s) {
char[] hexDigits = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
try {
byte[] btInput = s.getBytes(Charset.defaultCharset());
// 获得MD5摘要算法的 MessageDigest 对象
MessageDigest mdInst = MessageDigest.getInstance("MD5");
// 使用指定的字节更新摘要
mdInst.update(btInput);
// 获得密文
byte[] md = mdInst.digest();
// 把密文转换成十六进制的字符串形式
int j = md.length;
char[] str = new char[j * 2];
int k = 0;
for (int i = 0; i < j; i++) {
byte byte0 = md[i];
str[k++] = hexDigits[byte0 >>> 4 & 0xf];
str[k++] = hexDigits[byte0 & 0xf];
}
return new String(str);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}
Распределенная блокировка
Распределенная блокировка здесь использует Redis, например, непонимание книг с картинками блокировки, и в будущем будут внесены улучшения, чтобы реализовать полную схему распределенной блокировки и написать об этом в блоге.
@Slf4j
@Service
public class RepeatLockService {
@Autowired
RepeatRedisUtil repeatRedisUtil;
public void setRepeatLock(String methodName, Object[] args, Long expireTime) {
for (Object obj : args) {
log.info("方法名:{},对象:{},对象hashcode:{}", methodName, obj, obj.hashCode());
}
Boolean lock = repeatRedisUtil.setRepeatLock(methodName, args, expireTime);
if (!lock) {
log.info("已有相同请求");
}
}
public void removeRepeatLock(String methodName, Object[] args) {
repeatRedisUtil.removeRepeatLock(methodName, args);
}
}
@Component
public class RepeatRedisUtil {
@Autowired
RedisTemplate redisTemplate;
private static final String repeatLockPrefix = "repeat_lock_";
/**
* 设置重复请求锁,这一块的分布式锁的加与释放有问题,后续会专门出个文章总结redis分布式锁
* @param methodName
* @param args
* @param expireTime 过期时间ms
* @return
*/
public boolean setRepeatLock(String methodName, Object[] args,long expireTime) {
String key = getRepeatLockKey(methodName, args);
try {
boolean b = (boolean) redisTemplate.execute(new RedisCallback<Boolean>() {
@Override
public Boolean doInRedis(RedisConnection connection) throws DataAccessException {
Jedis jedis = null;
try {
jedis = (Jedis) connection.getNativeConnection();
String status = jedis.set(key, "1", NX, EX, expireTime);
if (setNXStatus.equals(status)) {
return Boolean.TRUE;
}
return Boolean.FALSE;
}finally {
connection.close();
}
}
});
return b;
} catch (Exception e) {
log.error("redis操作异常:{}",e);
return Boolean.FALSE;
}
}
/**
* 删除重复请求锁
* @param methodName
* @param args
*/
public void removeRepeatLock(String methodName, Object[] args){
String key = getRepeatLockKey(methodName, args);
redisTemplate.delete(key);
}
/**
* 获取重复请求锁Key
*
* @param methodName
* @param args
* @return
*/
public String getRepeatLockKey(String methodName, Object[] args) {
String repeatLockKey = repeatLockPrefix + methodName;
for (Object obj : args) {
repeatLockKey = repeatLockKey+"_"+ obj.hashCode();
}
return repeatLockKey;
}
}
Протестируйте интерфейс сервиса
То есть можно использовать аннотации к методу, а значит время истечения 200000мс, а второй параметр игнорируется.
@Slf4j
@Service
public class TestLockService {
@RepeatOperationLock(timeOut = 200000, ignoreIndex = 1)
public void testLock(UserDto userDto,int i){
log.info("service中属性:{},{}",userDto,i);
log.info("service中hashcode,userDto:{},i:{}",userDto.hashCode(),i);
}
}
Эпилог
Такое решение общей распределенной блокировки на основе пружины было общим, и действительно есть некоторые недостатки, такие как невозможность судить о том, будет ли оно неправильно понято при разблокировке и т. д. В будущем будет специально сделана сводка и улучшение распределенных блокировок. Выше только предложена идея решения повторяющихся запросов на основе распределенных блокировок, и я надеюсь пообщаться еще.