通常我们可以在前端通过防抖和节流来解决短时间内请求重复提交的问题,如果因网络问题、Nginx重试机制、微服务Feign重试机制或者用户故意绕过前端防抖和节流设置,直接频繁发起请求,都会导致系统防重请求失败,甚至导致后台产生多条重复记录,此时我们需要考虑在后台增加防重设置。

  考虑到微服务分布式的场景,这里通过使用Redisson分布式锁+自定义注解+AOP的方式来实现后台防止重复请求的功能,基本实现思路:通过在需要防重的接口添加自定义防重注解,设置防重参数,通过AOP拦截请求参数,根据注解配置,生成分布式锁的Key,并设置有效时间。每次请求访问时,都会尝试获取锁,如果获取到,则执行,如果获取不到,那么说明请求在设置的重复请求间隔内,返回请勿频繁请求提示信息。

1、自定义防止重复请求注解,根据业务场景设置了以下参数:
  • interval: 防止重复提交的时间间隔。
  • timeUnit: 防止重复提交的时间间隔的单位。
  • currentSession: 是否将sessionId作为防重参数(微服务及跨域前后端分离时,无法使用,Chrome等浏览器跨域时禁止携带cookie,每次sessionId都是新的)。
  • currentUser: 是否将用户id作为防重参数。
  • keys: 可以作为防重参数的字段(通过Spring Expression表达式,可以做到多参数时,具体取哪个参数的值)。
  • ignoreKeys: 需要忽略的防重参数字段,例如有些参数中的时间戳,此和keys互斥,当keys配置了之后,ignoreKeys失效。
  • conditions:当参数中的某个字段达到条件时,执行防重配置,默认不需要配置。
  • argsIndex: 当没有配置keys参数时,防重拦截后会对所有参数取值作为分布式锁的key,这里时,当多参数时,配置取哪一个参数作为key,可以多个。此和keys互斥,当keys配置了之后,argsIndex配置失效。
package com.gitegg.platform.base.annotation.resubmit;

import java.lang.annotation.*;
import java.util.concurrent.TimeUnit; /**
* 防止重复提交注解
* 1、当设置了keys时,通过表达式确定取哪几个参数作为防重key
* 2、当未设置keys时,可以设置argsIndex设置取哪几个参数作为防重key
* 3、argsIndex和ignoreKeys是未设置keys时生效,排除不需要防重的参数
* 4、因部分浏览器在跨域请求时,不允许request请求携带cookie,导致每次sessionId都是新的,所以这里默认使用用户id作为key的一部分,不使用sessionId
* @author GitEgg
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ResubmitLock { /**
* 防重复提交校验的时间间隔
*/
long interval() default 5; /**
* 防重复提交校验的时间间隔的单位
*/
TimeUnit timeUnit() default TimeUnit.SECONDS; /**
* 是否仅在当前session内进行防重复提交校验
*/
boolean currentSession() default false; /**
* 是否选用当前操作用户的信息作为防重复提交校验key的一部分
*/
boolean currentUser() default true; /**
* keys和ignoreKeys不能同时使用
* 参数Spring EL表达式例如 #{param.name},表达式的值作为防重复校验key的一部分
*/
String[] keys() default {}; /**
* keys和ignoreKeys不能同时使用
* ignoreKeys不区分入参,所有入参拥有相同的字段时,都将过滤掉
*/
String[] ignoreKeys() default {}; /**
* Spring EL表达式,决定是否进行重复提交校验,多个条件之间为且的关系,默认是进行校验
*/
String[] conditions() default {"true"}; /**
* 当未配置key时,设置哪几个参数作为防重对象,默认取所有参数
*
* @return
*/
int[] argsIndex() default {}; }
2、自定义AOP拦截防重请求的业务逻辑处理,详细逻辑处理请看代码注释。可以在Nacos中增加配置resubmit-lock: enable: false 使防重配置失效,默认不配置为生效状态。因为是ResubmitLockAspect是否初始化的ConditionalOnProperty配置,此配置修改需要重启服务生效。
package com.gitegg.platform.boot.aspect;

import com.gitegg.platform.base.annotation.resubmit.ResubmitLock;
import com.gitegg.platform.base.enums.ResultCodeEnum;
import com.gitegg.platform.base.exception.SystemException;
import com.gitegg.platform.base.util.JsonUtils;
import com.gitegg.platform.boot.util.ExpressionUtils;
import com.gitegg.platform.boot.util.GitEggAuthUtils;
import com.gitegg.platform.boot.util.GitEggWebUtils;
import com.gitegg.platform.redis.lock.IDistributedLockService;
import com.google.common.collect.Maps;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.apache.commons.lang3.ArrayUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;
import org.springframework.util.DigestUtils; import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Map;
import java.util.TreeMap; /**
* @author GitEgg
* @date 2022-4-10
*/
@Log4j2
@Component
@Aspect
@RequiredArgsConstructor(onConstructor_ = @Autowired)
@ConditionalOnProperty(name = "enabled", prefix = "resubmit-lock", havingValue = "true", matchIfMissing = true)
public class ResubmitLockAspect { private static final String REDIS_SEPARATOR = ":"; private static final String RESUBMIT_CHECK_KEY_PREFIX = "resubmit_lock" + REDIS_SEPARATOR; private final IDistributedLockService distributedLockService; /**
* Before切点
*/
@Pointcut("@annotation(com.gitegg.platform.base.annotation.resubmit.ResubmitLock)")
public void resubmitLock() {
} /**
* 前置通知 防止重复提交
*
* @param joinPoint 切点
* @param resubmitLock 注解配置
*/
@Before("@annotation(resubmitLock)")
public Object resubmitCheck(JoinPoint joinPoint, ResubmitLock resubmitLock) throws Throwable { final Object[] args = joinPoint.getArgs();
final String[] conditions = resubmitLock.conditions(); //根据条件判断是否需要进行防重复提交检查
if (!ExpressionUtils.getConditionValue(args, conditions) || ArrayUtils.isEmpty(args)) {
return ((ProceedingJoinPoint) joinPoint).proceed();
}
doCheck(resubmitLock, args);
return ((ProceedingJoinPoint) joinPoint).proceed();
} /**
* key的组成为: resubmit_lock:userId:sessionId:uri:method:(根据spring EL表达式对参数进行拼接)
*
* @param resubmitLock 注解
* @param args 方法入参
*/
private void doCheck(@NonNull ResubmitLock resubmitLock, Object[] args) { final String[] keys = resubmitLock.keys();
final boolean currentUser = resubmitLock.currentUser();
final boolean currentSession = resubmitLock.currentSession(); String method = GitEggWebUtils.getRequest().getMethod();
String uri = GitEggWebUtils.getRequest().getRequestURI(); StringBuffer lockKeyBuffer = new StringBuffer(RESUBMIT_CHECK_KEY_PREFIX); if (null != GitEggAuthUtils.getTenantId())
{
lockKeyBuffer.append( GitEggAuthUtils.getTenantId()).append(REDIS_SEPARATOR);
} // 此判断暂时预留,适配后续无用户登录场景,因部分浏览器在跨域请求时,不允许request请求携带cookie,导致每次sessionId都是新的,所以这里默认使用用户id作为key的一部分,不使用sessionId
if (currentSession)
{
lockKeyBuffer.append( GitEggWebUtils.getSessionId()).append(REDIS_SEPARATOR);
} // 默认没有将user数据作为防重key
if (currentUser && null != GitEggAuthUtils.getCurrentUser())
{
lockKeyBuffer.append( GitEggAuthUtils.getCurrentUser().getId() ).append(REDIS_SEPARATOR);
} lockKeyBuffer.append(uri).append(REDIS_SEPARATOR).append(method); StringBuffer parametersBuffer = new StringBuffer();
// 优先判断是否设置防重字段,因keys试数组,取值时是按照顺序排列的,这里不需要重新排序
if (ArrayUtils.isNotEmpty(keys))
{
Object[] argsForKey = ExpressionUtils.getExpressionValue(args, keys);
for (Object obj : argsForKey) {
parametersBuffer.append(REDIS_SEPARATOR).append(String.valueOf(obj));
}
}
// 如果没有设置防重的字段,那么需要把所有的字段和值作为key,因通过反射获取字段时,顺序时不确定的,这里取出来之后需要进行排序
else{
// 只有当keys为空时,ignoreKeys和argsIndex生效
final String[] ignoreKeys = resubmitLock.ignoreKeys();
final int[] argsIndex = resubmitLock.argsIndex();
if (ArrayUtils.isNotEmpty(argsIndex))
{
for(int index : argsIndex){
parametersBuffer.append(REDIS_SEPARATOR).append( getKeyAndValueJsonStr(args[index], ignoreKeys));
}
}
else
{
for(Object obj : args){
parametersBuffer.append(REDIS_SEPARATOR).append( getKeyAndValueJsonStr(obj, ignoreKeys) );
}
}
} // 将请求参数取md5值作为key的一部分,MD5理论上会重复,但是key中还包含session或者用户id,所以同用户在极端时间内请参数不同生成的相同md5值的概率极低
String parametersKey = DigestUtils.md5DigestAsHex(parametersBuffer.toString().getBytes());
lockKeyBuffer.append(parametersKey); try {
boolean isLock = distributedLockService.tryLock(lockKeyBuffer.toString(), 0, resubmitLock.interval(), resubmitLock.timeUnit());
if (!isLock)
{
throw new SystemException(ResultCodeEnum.RESUBMIT_LOCK.code, ResultCodeEnum.RESUBMIT_LOCK.msg);
}
} catch (InterruptedException e) {
throw new SystemException(ResultCodeEnum.RESUBMIT_LOCK.code, ResultCodeEnum.RESUBMIT_LOCK.msg);
}
} /**
* 将字段转换为json字符串
* @param obj
* @return
*/
public static String getKeyAndValueJsonStr(Object obj, String[] ignoreKeys) {
Map<String, Object> map = Maps.newHashMap();
// 得到类对象
Class objCla = (Class) obj.getClass();
/* 得到类中的所有属性集合 */
Field[] fs = objCla.getDeclaredFields();
for (int i = 0; i < fs.length; i++) {
Field f = fs[i];
// 设置些属性是可以访问的
f.setAccessible(true);
Object val = new Object();
try {
String filedName = f.getName();
// 如果字段在排除列表,那么不将字段放入map
if (null != ignoreKeys && Arrays.asList(ignoreKeys).contains(filedName))
{
continue;
}
val = f.get(obj);
// 得到此属性的值
// 设置键值
map.put(filedName, val);
} catch (IllegalArgumentException e) {
log.error("getKeyAndValue IllegalArgumentException", e);
throw new RuntimeException("您的操作太频繁,请稍后再试");
} catch (IllegalAccessException e) {
log.error("getKeyAndValue IllegalAccessException", e);
throw new RuntimeException("您的操作太频繁,请稍后再试");
}
}
Map<String, Object> sortMap = sortMapByKey(map);
String mapStr = JsonUtils.mapToJson(sortMap);
return mapStr;
} private static Map<String, Object> sortMapByKey(Map<String, Object> map) {
if (map == null || map.isEmpty()) {
return null;
}
Map<String, Object> sortMap = new TreeMap<String, Object>(new Comparator<String>() {
@Override
public int compare(String o1,String o2) {
return ((String)o1).compareTo((String) o2);
}
});
sortMap.putAll(map);
return sortMap;
} }
3、Redisson分布式锁自定义接口
package com.gitegg.platform.redis.lock;

import java.util.concurrent.TimeUnit;

/**
* 分布式锁接口
* @author GitEgg
* @date 2022-4-10
*/
public interface IDistributedLockService { /**
* 加锁
* @param lockKey key
*/
void lock(String lockKey); /**
* 释放锁
*
* @param lockKey key
*/
void unlock(String lockKey); /**
* 加锁并设置有效期
*
* @param lockKey key
* @param timeout 有效时间,默认时间单位在实现类传入
*/
void lock(String lockKey, int timeout); /**
* 加锁并设置有效期指定时间单位
* @param lockKey key
* @param timeout 有效时间
* @param unit 时间单位
*/
void lock(String lockKey, int timeout, TimeUnit unit); /**
* 尝试获取锁,获取到则持有该锁返回true,未获取到立即返回false
* @param lockKey
* @return true-获取锁成功 false-获取锁失败
*/
boolean tryLock(String lockKey); /**
* 尝试获取锁,获取到则持有该锁leaseTime时间.
* 若未获取到,在waitTime时间内一直尝试获取,超过watiTime还未获取到则返回false
* @param lockKey key
* @param waitTime 尝试获取时间
* @param leaseTime 锁持有时间
* @param unit 时间单位
* @return true-获取锁成功 false-获取锁失败
* @throws InterruptedException
*/
boolean tryLock(String lockKey, long waitTime, long leaseTime, TimeUnit unit)
throws InterruptedException; /**
* 锁是否被任意一个线程锁持有
* @param lockKey
* @return true-被锁 false-未被锁
*/
boolean isLocked(String lockKey);
}
4、Redisson分布式锁自定义接口实现类
package com.gitegg.platform.redis.lock.impl;

import com.gitegg.platform.redis.lock.IDistributedLockService;
import lombok.RequiredArgsConstructor;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import java.util.concurrent.TimeUnit; /**
* 分布式锁的 Redisson 接口实现
* @author GitEgg
* @date 2022-4-10
*/
@Service
@RequiredArgsConstructor(onConstructor_ = @Autowired)
public class DistributedLockServiceImpl implements IDistributedLockService { private final RedissonClient redissonClient; @Override
public void lock(String lockKey) {
RLock lock = redissonClient.getLock(lockKey);
lock.lock();
} @Override
public void unlock(String lockKey) {
RLock lock = redissonClient.getLock(lockKey);
lock.unlock();
} @Override
public void lock(String lockKey, int timeout) {
RLock lock = redissonClient.getLock(lockKey);
lock.lock(timeout, TimeUnit.MILLISECONDS);
} @Override
public void lock(String lockKey, int timeout, TimeUnit unit) {
RLock lock = redissonClient.getLock(lockKey);
lock.lock(timeout, unit);
} @Override
public boolean tryLock(String lockKey) {
RLock lock = redissonClient.getLock(lockKey);
return lock.tryLock();
} @Override
public boolean tryLock(String lockKey, long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {
RLock lock = redissonClient.getLock(lockKey);
return lock.tryLock(waitTime, leaseTime, unit);
} @Override
public boolean isLocked(String lockKey) {
RLock lock = redissonClient.getLock(lockKey);
return lock.isLocked();
}
}
5、Spring Expression自定义工具类,通过此工具类获取注解上的Expression表达式,以获取相应请求对象的值,如果请求对象有多个,可以通过Expression表达式精准获取。
package com.gitegg.platform.boot.util;

import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable; import java.util.Map;
import java.util.concurrent.ConcurrentHashMap; /**
* Spring Expression 工具类
* @author GitEgg
* @date 2022-4-11
*/
public class ExpressionUtils { private static final Map<String, Expression> EXPRESSION_CACHE = new ConcurrentHashMap<>(64); /**
* 获取Expression对象
*
* @param expressionString Spring EL 表达式字符串 例如 #{param.id}
* @return Expression
*/
@Nullable
public static Expression getExpression(@Nullable String expressionString) { if (StringUtils.isBlank(expressionString)) {
return null;
} if (EXPRESSION_CACHE.containsKey(expressionString)) {
return EXPRESSION_CACHE.get(expressionString);
} Expression expression = new SpelExpressionParser().parseExpression(expressionString);
EXPRESSION_CACHE.put(expressionString, expression);
return expression;
} /**
* 根据Spring EL表达式字符串从根对象中求值
*
* @param root 根对象
* @param expressionString Spring EL表达式
* @param clazz 值得类型
* @param <T> 泛型
* @return 值
*/
@Nullable
public static <T> T getExpressionValue(@Nullable Object root, @Nullable String expressionString, @NonNull Class<? extends T> clazz) {
if (root == null) {
return null;
}
Expression expression = getExpression(expressionString);
if (expression == null) {
return null;
} return expression.getValue(root, clazz);
} @Nullable
public static <T> T getExpressionValue(@Nullable Object root, @Nullable String expressionString) {
if (root == null) {
return null;
}
Expression expression = getExpression(expressionString);
if (expression == null) {
return null;
}
//noinspection unchecked
return (T) expression.getValue(root);
} /**
* 求值
*
* @param root 根对象
* @param expressionStrings Spring EL表达式
* @param <T> 泛型 这里的泛型要慎用,大多数情况下要使用Object接收避免出现转换异常
* @return 结果集
*/
public static <T> T[] getExpressionValue(@Nullable Object root, @Nullable String... expressionStrings) {
if (root == null) {
return null;
}
if (ArrayUtils.isEmpty(expressionStrings)) {
return null;
}
//noinspection ConstantConditions
Object[] values = new Object[expressionStrings.length];
for (int i = 0; i < expressionStrings.length; i++) {
//noinspection unchecked
values[i] = (T) getExpressionValue(root, expressionStrings[i]);
}
//noinspection unchecked
return (T[]) values;
} /**
* 表达式条件求值
* 如果为值为null则返回false,
* 如果为布尔类型直接返回,
* 如果为数字类型则判断是否大于0
*
* @param root 根对象
* @param expressionString Spring EL表达式
* @return 值
*/
@Nullable
public static boolean getConditionValue(@Nullable Object root, @Nullable String expressionString) {
Object value = getExpressionValue(root, expressionString);
if (value == null) {
return false;
}
if (value instanceof Boolean) {
return (boolean) value;
}
if (value instanceof Number) {
return ((Number) value).longValue() > 0;
}
return true;
} /**
* 表达式条件求值
*
* @param root 根对象
* @param expressionStrings Spring EL表达式数组
* @return 值
*/
@Nullable
public static boolean getConditionValue(@Nullable Object root, @Nullable String... expressionStrings) {
if (root == null) {
return false;
}
if (ArrayUtils.isEmpty(expressionStrings)) {
return false;
}
//noinspection ConstantConditions
for (String expressionString : expressionStrings) {
if (!getConditionValue(root, expressionString)) {
return false;
}
}
return true;
}
}
5、防重测试,我们在系统的用户接口(GitEgg-Cloud工程的UserController类)上进行测试,通过多参数接口以及配置keys,不配置keys等各种场景进行测试,在测试时为了达到效果,可以将interval 时间设置为30秒。
  • 设置user参数的realName,mobile和page参数的size为key进行防重测试
    @ResubmitLock(interval = 30, keys = {"[0].realName","[0].mobile","[1].size"})
public PageResult<UserInfo> list(@ApiIgnore QueryUserDTO user, @ApiIgnore Page<UserInfo> page) {
Page<UserInfo> pageUser = userService.selectUserList(page, user);
PageResult<UserInfo> pageResult = new PageResult<>(pageUser.getTotal(), pageUser.getRecords());
return pageResult;
}
  • 不设置防重参数的key,只取第一个参数user,配置排除的参数,不参与放重key的生成
    @ResubmitLock(interval = 30, argsIndex = {0}, ignoreKeys = {"email","status"})
public PageResult<UserInfo> list(@ApiIgnore QueryUserDTO user, @ApiIgnore Page<UserInfo> page) {
Page<UserInfo> pageUser = userService.selectUserList(page, user);
PageResult<UserInfo> pageResult = new PageResult<>(pageUser.getTotal(), pageUser.getRecords());
return pageResult;
}
  • 测试结果

相关引用:

1、防重配置项及通过SpringExpression获取相应参数:https://www.jianshu.com/p/77895a822237

2、Redisson分布式锁及相关工具类:https://blog.csdn.net/wsh_ningjing/article/details/115326052

源码地址:

Gitee: https://gitee.com/wmz1930/GitEgg

GitHub: https://github.com/wmz1930/GitEgg

SpringCloud微服务实战——搭建企业级开发框架(三十九):使用Redis分布式锁(Redisson)+自定义注解+AOP实现微服务重复请求控制的更多相关文章

  1. SpringCloud微服务实战——搭建企业级开发框架(十九):Gateway使用knife4j聚合微服务文档

      本章介绍Spring Cloud Gateway网关如何集成knife4j,通过网关聚合所有的Swagger微服务文档 1.gitegg-gateway中引入knife4j依赖,如果没有后端代码编 ...

  2. SpringCloud微服务实战——搭建企业级开发框架(十二):OpenFeign+Ribbon实现负载均衡

      Ribbon是Netflix下的负载均衡项目,它主要实现中间层应用程序的负载均衡.为Ribbon配置服务提供者地址列表后,Ribbon就会基于某种负载均衡算法,自动帮助服务调用者去请求.Ribbo ...

  3. SpringCloud微服务实战——搭建企业级开发框架(十五):集成Sentinel高可用流量管理框架【熔断降级】

      Sentinel除了流量控制以外,对调用链路中不稳定的资源进行熔断降级也是保障高可用的重要措施之一.由于调用关系的复杂性,如果调用链路中的某个资源不稳定,最终会导致请求发生堆积.Sentinel ...

  4. SpringCloud微服务实战——搭建企业级开发框架(十):使用Nacos分布式配置中心

    随着业务的发展.微服务架构的升级,服务的数量.程序的配置日益增多(各种微服务.各种服务器地址.各种参数),传统的配置文件方式和数据库的方式已无法满足开发人员对配置管理的要求: 安全性:配置跟随源代码保 ...

  5. SpringCloud微服务实战——搭建企业级开发框架(十四):集成Sentinel高可用流量管理框架【限流】

      Sentinel 是面向分布式服务架构的高可用流量防护组件,主要以流量为切入点,从限流.流量整形.熔断降级.系统负载保护.热点防护等多个维度来帮助开发者保障微服务的稳定性. Sentinel 具有 ...

  6. SpringCloud微服务实战——搭建企业级开发框架(十六):集成Sentinel高可用流量管理框架【自定义返回消息】

    Sentinel限流之后,默认的响应消息为Blocked by Sentinel (flow limiting),对于系统整体功能提示来说并不统一,参考我们前面设置的统一响应及异常处理方式,返回相同的 ...

  7. SpringCloud微服务实战——搭建企业级开发框架(二十三):Gateway+OAuth2+JWT实现微服务统一认证授权

      OAuth2是一个关于授权的开放标准,核心思路是通过各类认证手段(具体什么手段OAuth2不关心)认证用户身份,并颁发token(令牌),使得第三方应用可以使用该token(令牌)在限定时间.限定 ...

  8. SpringCloud微服务实战——搭建企业级开发框架(三十四):SpringCloud + Docker + k8s实现微服务集群打包部署-Maven打包配置

      SpringCloud微服务包含多个SpringBoot可运行的应用程序,在单应用程序下,版本发布时的打包部署还相对简单,当有多个应用程序的微服务发布部署时,原先的单应用程序部署方式就会显得复杂且 ...

  9. SpringCloud微服务实战——搭建企业级开发框架(三十六):使用Spring Cloud Stream实现可灵活配置消息中间件的功能

      在以往消息队列的使用中,我们通常使用集成消息中间件开源包来实现对应功能,而消息中间件的实现又有多种,比如目前比较主流的ActiveMQ.RocketMQ.RabbitMQ.Kafka,Stream ...

随机推荐

  1. 萌新看过来,你还学不懂VScode插件吗?

    一.前言 VSCode是微软家一个非常轻量化的编辑器,体量虽轻,但是却有异常强大的功能.原因在于VSCode许多强大功能都是基于插件实现的,IDE只提供一个最基本的框架和基本功能,我们需要使用插件来丰 ...

  2. DC-1

    靶机准备 导入虚拟机设置网络为NAT kali:192.168.164.137 扫描获得靶机ip:192.168.164.182 netdiscover -r 192.168.164.0/24 渗透测 ...

  3. 利用多个sem信号量在线程通讯

    直接上代码,主要用到sem_trywait & sem_post #include<stdio.h> #include<pthread.h> #include<s ...

  4. 通过R Studio用Markdown写Beamer

    技术背景 在写一些学术演示文档时,经常有可能用到Beamer--一种Latex的学术风PPT模板,比如下图所示的这种: 这种风格的演示文档有几个明显的优点:简约.严肃.可以用Latex敲公式和推导.可 ...

  5. (转载)linux下Yum的$releasever和$basearch的取值

    https://blog.csdn.net/whatday/article/details/51097456

  6. python 函数基础知识

    1.函数返回的多个值会被组织成元组被返回,也可以用多个值来接收 2.调用函数时候,传入的参数叫实际参数,简称实参,定义函数的参数叫做形式参数,简称形参-- 位置参数 def mymax(x,y): a ...

  7. kafka端口和zookeeper端口

    一.问题描述 今天配合现场联调一个数据工具,工具使用到了kafka,程序启动之后包如下错误: [WARN ] [2020-08-17 19:17:27] [org.apache.kafka.clien ...

  8. 学习MFS(四)

    一.搭建Master Server 1.安装相关编译器.工具包 [root@master ~]# yum -y install gcc gcc-c++ zlib-devel 2.创建进程用户 [roo ...

  9. ESD@TVS选型

    一.工作原理 ESD ESD静电保护元件,又称静电抑制二极管.ESD是多个TVS晶粒或二极管采用不同的布局做成具有特定功能的多路或单路ESD保护器件,主要应用于各类通信接口静电保护,如USB.HDMI ...

  10. 9_状态观测器设计_Linear Observer Design_Matlab_Simulink建模(上)