开场白:老铁们对于文章有错误、不准确,或需要补充的请留言讨论 ,大家共同学习。如果觉得还不错的请关注、留言、点赞 、收藏。 创作不易,且看且珍惜
对于重复提交的问题,主要由于重复点击或者网络重发请求, 我要先了解产生原因几种方式:
对于重复提交的问题 主要涉及到时 幂等 问题,那么先说一下什么是幂等。
幂等:F(F(X)) = F(X)多次运算结果一致;简单点说就是对于完全相同的操作,操作一次与操作多次的结果是一样的。
在开发中,我们都会涉及到对数据库操作。例如:
乐观锁和悲观锁 个人更喜欢乐观锁。version字段的方式;select…for update 的方式,* 要使用悲观锁,我们必须关闭mysql数据库的自动提交属性。重点 (数据库简单方案:可采取数据库唯一索引方式;这种在大数据量和高并发下效率依赖数据库硬件能力,可针对并发量不高的非核心业务;)| 序号 | 前端/后端 | 方案 | 优点 | 缺点 | 代码实现 |
|---|---|---|---|---|---|
| 1) | 前端 | 前端js提交后禁止按钮,返回结果后解禁等 | 简单 方便 | 只能控制页面,通过工具可绕过不安全 | 略 |
| 2) | 后端 | 提交后重定向到其他页面,防止用户F5和浏览器前进后退等重复提交问题 | 简单 方便 | 体验不好,适用部分场景,若是遇到网络问题 还会出现 | 略 |
| 3) | 后端 | 在表单、session、token 放入唯一标识符(如:UUID),每次操作时,保存标识一定时间后移除,保存期间有相同的标识就不处理或提示 | 相对简单 | 表单:有时需要前后端协商配合; session、token:加大服务性能开销 | 略 |
| 4) | 后端 | ConcurrentHashMap 、LRUMap 、google Cache 都是采用唯一标识(如:用户ID+请求路径+参数) | 相对简单 | 适用于单机部署的应用 | 见下 |
| 5) | 后端 | redis 是线程安全的,可以实现redis分布式锁。设置唯一标识(如:用户ID+请求路径+参数)当做key ,value值可以随意(推荐设置成过期的时间点),在设置key的过期时间 | 单机、分布式、高并发都可以决绝 | 相对复杂需要部署维护redis | 见下 |
4). google cache 代码实现 注解方式 Single lock
pom.xml 引入
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>28.2-jre</version>
</dependency>
配置文件 .yml
resubmit:
local:
timeOut: 30
实现代码
import java.lang.annotation.*;
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface LocalLock {
}
import com.alibaba.fastjson.JSONObject;
import com.example.mydemo.common.utils.IpUtils;
import com.example.mydemo.common.utils.Result;
import com.example.mydemo.common.utils.SecurityUtils;
import com.example.mydemo.common.utils.sign.MyMD5Util;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import lombok.Data;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* @author: xx
* @description: 单机放重复提交
*/
@Data
@Aspect
@Configuration
public class LocalLockMethodInterceptor {
@Value("${spring.profiles.active}")
private String springProfilesActive;
@Value("${spring.application.name}")
private String springApplicationName;
private static int expireTimeSecond =5;
@Value("${resubmit:local:timeOut}")
public void setExpireTimeSecond(int expireTimeSecond) {
LocalLockMethodInterceptor.expireTimeSecond = expireTimeSecond;
}
//定义缓存,设置最大缓存数及过期日期
private static final Cache<String,Object> CACHE =
CacheBuilder.newBuilder().maximumSize(1000).expireAfterWrite(expireTimeSecond, TimeUnit.SECONDS).build();
@Around("execution(public * *(..)) && @annotation(com.example.mydemo.common.interceptor.annotation.LocalLock)")
public Object interceptor(ProceedingJoinPoint joinPoint){
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
// LocalLock localLock = method.getAnnotation(LocalLock.class);
try{
String key = getLockUniqueKey(signature,joinPoint.getArgs());
if(CACHE.getIfPresent(key) != null){
return Result.fail("不允许重复提交,请稍后再试");
}
CACHE.put(key,key);
return joinPoint.proceed();
}catch (Throwable throwable){
throw new RuntimeException(throwable.getMessage());
}finally {
}
}
/**
* 获取唯一标识key
*
* @param methodSignature
* @param args
* @return
*/
private String getLockUniqueKey(MethodSignature methodSignature, Object[] args) throws NoSuchAlgorithmException {
//请求uri, 获取类名称,方法名称
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) requestAttributes;
HttpServletRequest request = servletRequestAttributes.getRequest();
// HttpServletResponse responese = servletRequestAttributes.getResponse();
//获取用户信息
String userMsg = SecurityUtils.getUsername(); //获取登录用户名称
//1.判断用户是否登录
if (StringUtils.isEmpty(userMsg)) { //未登录用户获取真实ip
userMsg = IpUtils.getIpAddr(request);
}
String hash = "";
List list = new ArrayList();
if (args.length > 0) {
String[] parameterNames = methodSignature.getParameterNames();
for (int i = 0; i < parameterNames.length; i++) {
Object obj = args[i];
list.add(obj);
}
hash = JSONObject.toJSONString(list);
}
//项目名称 + 环境编码 + 获取类名称 + 方法名称 + 唯一key
String key = "locallock:" + springApplicationName + ":" + springProfilesActive + ":" + userMsg + ":" + request.getRequestURI();
if (StringUtils.isNotEmpty(key)) {
key = key + ":" + hash;
}
key = MyMD5Util.getMD5(key);
return key;
}
使用:
@LocalLock
public void save(@RequestBody User user) {
}
5)redis
pom.xml 引入
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
.yml文件 redis 配置
spring:
redis:
host: localhost
port: :6379
password: 123456
import java.lang.annotation.*;
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface RedisLock {
int expire() default 5;
}
import com.alibaba.fastjson.JSONObject;
import com.google.common.collect.Lists;
import com.heshu.sz.blockchain.utonhsbs.common.utils.MyMD5Util;
import com.heshu.sz.blockchain.utonhsbs.common.utils.SecurityUtils;
import com.heshu.sz.blockchain.utonhsbs.common.utils.ip.IpUtils;
import com.heshu.sz.blockchain.utonhsbs.framework.interceptor.annotation.RedisLock;
import com.heshu.sz.blockchain.utonhsbs.framework.system.domain.BaseResult;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
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.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.List;
/**
* @author :xx
* @description:
* @date : 2022/7/1 9:41
*/
@Slf4j
@Aspect
@Configuration
public class RedisLockMethodInterceptor {
@Value("${spring.profiles.active}")
private String springProfilesActive;
@Value("${spring.application.name}")
private String springApplicationName;
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Pointcut("@annotation(com.heshu.sz.blockchain.utonhsbs.framework.interceptor.annotation.RedisLock)")
public void point() {
}
@Around("point()")
public Object doaround(ProceedingJoinPoint joinPoint) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
RedisLock localLock = method.getAnnotation(RedisLock.class);
try {
String lockUniqueKey = getLockUniqueKey(signature, joinPoint.getArgs());
Integer expire = localLock.expire();
if (expire < 0) {
expire = 5;
}
ArrayList<String> keys = Lists.newArrayList(lockUniqueKey);
String result = stringRedisTemplate.execute(setNxWithExpireTime, keys, expire.toString());
if (!"ok".equalsIgnoreCase(result)) {//不存在
return BaseResult.error("不允许重复提交,请稍后再试");
}
return joinPoint.proceed();
} catch (Throwable throwable) {
throw new RuntimeException(throwable.getMessage());
}
}
/**
* lua脚本
*/
private RedisScript<String> setNxWithExpireTime = new DefaultRedisScript<>(
"return redis.call('set', KEYS[1], 1, 'ex', ARGV[1], 'nx');",
String.class
);
/**
* 获取唯一标识key
*
* @param methodSignature
* @param args
* @return
*/
private String getLockUniqueKey(MethodSignature methodSignature, Object[] args) throws NoSuchAlgorithmException {
//请求uri, 获取类名称,方法名称
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) requestAttributes;
HttpServletRequest request = servletRequestAttributes.getRequest();
// HttpServletResponse responese = servletRequestAttributes.getResponse();
//获取用户信息
String userMsg = SecurityUtils.getUsername(); //获取登录用户名称
//1.判断用户是否登录
if (StringUtils.isEmpty(userMsg)) { //未登录用户获取真实ip
userMsg = IpUtils.getIpAddr(request);
}
String hash = "";
List list = new ArrayList();
if (args.length > 0) {
String[] parameterNames = methodSignature.getParameterNames();
for (int i = 0; i < parameterNames.length; i++) {
Object obj = args[i];
list.add(obj);
}
String param = JSONObject.toJSONString(list);
hash = MyMD5Util.getMD5(param);
}
//项目名称 + 环境编码 + 获取类名称 + 加密参数
String key = "lock:" + springApplicationName + ":" + springProfilesActive + ":" + userMsg + ":" + request.getRequestURI();
if (StringUtils.isNotEmpty(key)) {
key = key + ":" + hash;
}
return key;
}
使用
@RedisLock
public void save(@RequestBody User user) {
}
我正在编写一个包含C扩展的gem。通常当我写一个gem时,我会遵循TDD的过程,我会写一个失败的规范,然后处理代码直到它通过,等等......在“ext/mygem/mygem.c”中我的C扩展和在gemspec的“扩展”中配置的有效extconf.rb,如何运行我的规范并仍然加载我的C扩展?当我更改C代码时,我需要采取哪些步骤来重新编译代码?这可能是个愚蠢的问题,但是从我的gem的开发源代码树中输入“bundleinstall”不会构建任何native扩展。当我手动运行rubyext/mygem/extconf.rb时,我确实得到了一个Makefile(在整个项目的根目录中),然后当
在MRIRuby中我可以这样做:deftransferinternal_server=self.init_serverpid=forkdointernal_server.runend#Maketheserverprocessrunindependently.Process.detach(pid)internal_client=self.init_client#Dootherstuffwithconnectingtointernal_server...internal_client.post('somedata')ensure#KillserverProcess.kill('KILL',
我真的很习惯使用Ruby编写以下代码:my_hash={}my_hash['test']=1Java中对应的数据结构是什么? 最佳答案 HashMapmap=newHashMap();map.put("test",1);我假设? 关于java-等价于Java中的RubyHash,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.com/questions/22737685/
我已经在Sinatra上创建了应用程序,它代表了一个简单的API。我想在生产和开发上进行部署。我想在部署时选择,是开发还是生产,一些方法的逻辑应该改变,这取决于部署类型。是否有任何想法,如何完成以及解决此问题的一些示例。例子:我有代码get'/api/test'doreturn"Itisdev"end但是在部署到生产环境之后我想在运行/api/test之后看到ItisPROD如何实现? 最佳答案 根据SinatraDocumentation:EnvironmentscanbesetthroughtheRACK_ENVenvironm
我们的git存储库中目前有一个Gemfile。但是,有一个gem我只在我的环境中本地使用(我的团队不使用它)。为了使用它,我必须将它添加到我们的Gemfile中,但每次我checkout到我们的master/dev主分支时,由于与跟踪的gemfile冲突,我必须删除它。我想要的是类似Gemfile.local的东西,它将继承从Gemfile导入的gems,但也允许在那里导入新的gems以供使用只有我的机器。此文件将在.gitignore中被忽略。这可能吗? 最佳答案 设置BUNDLE_GEMFILE环境变量:BUNDLE_GEMFI
这似乎非常适得其反,因为太多的gem会在window上破裂。我一直在处理很多mysql和ruby-mysqlgem问题(gem本身发生段错误,一个名为UnixSocket的类显然在Windows机器上不能正常工作,等等)。我只是在浪费时间吗?我应该转向不同的脚本语言吗? 最佳答案 我在Windows上使用Ruby的经验很少,但是当我开始使用Ruby时,我是在Windows上,我的总体印象是它不是Windows原生系统。因此,在主要使用Windows多年之后,开始使用Ruby促使我切换回原来的系统Unix,这次是Linux。Rub
我正在玩HTML5视频并且在ERB中有以下片段:mp4视频从在我的开发环境中运行的服务器很好地流式传输到chrome。然而firefox显示带有海报图像的视频播放器,但带有一个大X。问题似乎是mongrel不确定ogv扩展的mime类型,并且只返回text/plain,如curl所示:$curl-Ihttp://0.0.0.0:3000/pr6.ogvHTTP/1.1200OKConnection:closeDate:Mon,19Apr201012:33:50GMTLast-Modified:Sun,18Apr201012:46:07GMTContent-Type:text/plain
我正在尝试使用boilerpipe来自JRuby。我看过guide从JRuby调用Java,并成功地将它与另一个Java包一起使用,但无法弄清楚为什么同样的东西不能用于boilerpipe。我正在尝试基本上从JRuby中执行与此Java等效的操作:URLurl=newURL("http://www.example.com/some-location/index.html");Stringtext=ArticleExtractor.INSTANCE.getText(url);在JRuby中试过这个:require'java'url=java.net.URL.new("http://www
我只想对我一直在思考的这个问题有其他意见,例如我有classuser_controller和classuserclassUserattr_accessor:name,:usernameendclassUserController//dosomethingaboutanythingaboutusersend问题是我的User类中是否应该有逻辑user=User.newuser.do_something(user1)oritshouldbeuser_controller=UserController.newuser_controller.do_something(user1,user2)我
什么是ruby的rack或python的Java的wsgi?还有一个路由库。 最佳答案 来自Python标准PEP333:Bycontrast,althoughJavahasjustasmanywebapplicationframeworksavailable,Java's"servlet"APImakesitpossibleforapplicationswrittenwithanyJavawebapplicationframeworktoruninanywebserverthatsupportstheservletAPI.ht