web请求重放|幂等性问题

本文介绍了防止数据库中出现重复记录的三种方法:前端和H5事件处理、服务端防重放处理以及数据库唯一约束。重点讨论了服务端解决方案,包括使用token机制、Redis存储业务编号与用户ID以及无感阻塞处理。同时提供了一个基于Java和Lua的限流原子操作示例,以及拦截器实现的重放处理。这些策略旨在确保服务的幂等性和系统稳定性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

数据库保存相同的多条记录,一般从三个方面进行加固

1、端和h5做事件重复触发

缺点:各种机型和浏览器等兼容性问题会导致部分漏网之鱼,还有接口工具可能直接并发绕过常规

2、服务端做防重放处理,本次记录是服务端处理

3、数据库做唯一约束

缺点:相当于重新走了一圈服务的业务逻辑,数据库层面报错体验也不友好,服务端和数据层都增加压力

下面说下服务端的解决方案

1、token

前端+后端调整

前端请求验证后服务端保存token到session或redis里并返回给前端,前端请求时把token带回来由服务端验证,业务流程走完时删除token

2、

后端

用redis,做业务编号和userId存放在redis中,redis中过期时间设置1到2秒释放,同一个业务操作一般重放逼近时间相同,业务流程走完删除key

3、

后端无感方案,有重放时阻塞,等第一个执行完了,第二个去验证第一个执行结果再再返回成功和失败,或者也可以再次执行下,一般情况都是没有新增有就更新的业务逻辑

3.1在拦截器preHandle给redis加锁

3.2在拦截器preHandle判断有锁阻塞,直到获取解锁验证业务是否正常,正常返回成功结果

3.3在拦截器postHandle给redis解锁

java+Lua 原子性,限流

import com.google.common.collect.Lists;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import redis.clients.jedis.Jedis;
@Slf4j
@Component
public class RedisLua {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    /**
     *INCRBY  请求数加1
     */
    String script = "local key = KEYS[1]\n" +
            "local val = tonumber(ARGV[1])\n" +
            "local limit = tonumber(ARGV[2])\n" +
            "local ss = tonumber(ARGV[3])\n" +
            "local current = tonumber(redis.call(\"get\", key) or \"0\")\n" +
            "if current + 1 > limit then\n" +
            "    return 0\n" +
            "else\n" +
            "    redis.call(\"INCRBY\", key, val)\n" +
            "    redis.call(\"expire\", key, ss)\n" +
            "end\n" +
            "return 1";

    /**
     *
     * @param script  lua脚本
     * @param key     key
     * @param value     value
     * @param count     限流次数
     * @param expire    超时时间:单位:秒
     * @throws Exception
     */
    public Object eavl(String script,String key,String value,int count,int expire){
        try{
            Object ret = stringRedisTemplate.execute(new RedisCallback() {
                public Object doInRedis(RedisConnection connection) {
                    Jedis jedis = (Jedis) connection.getNativeConnection();
                    return jedis.eval(script, Lists.newArrayList(key),Lists.newArrayList(value,String.valueOf(count),String.valueOf(expire)));
                }
            }, true);
            return ret;
        }catch (Exception ex){
            log.error("幂等性异常,不做拦截",ex);
        }
        return null;
    }

    /**
     *
     * @param key     key
     * @param value     value
     * @param count     限流次数
     * @param expire    超时时间:单位:秒
     * @throws Exception
     */
    public Object eavl(String key,String value,int count,int expire){
        return this.eavl(script,key,value,count,expire);
    }
}

拦截器做重放处理

@Slf4j
public class AuthInterceptor implements HandlerInterceptor {
@Autowired
RedisLua redisLua;
String idempotenPrefix = "idempotent";
@Override
public boolean preHandle(HttpServletRequest req, HttpServletResponse resp, Object o) throws Exception {
 
String mehtod = req.getMethod();
String lastPath = req.getServletPath().substring(req.getServletPath().lastIndexOf("/")+1);//最path中最后一个地址,如果这个没办法排除连续唯一性可以把全路径都用'/'替换成":"做为redis的key
if("post".equals(mehtod.toLowerCase())){//只做post请求
    if (!checkIdempotent(user.getUid(),lastPath)){//flowStep一个步骤可能有多个post请求
        log.warn("idempoten user {},path {}",userId,req.getServletPath());
        log.warn("idempoten expire second:"+redisLua.getExpire(idempotenPrefix+":"+lastPath+":"+userId));
        //resp.getWriter().write();这里可以做前端提示或无感重复提交返回成功后的跳转
        return false;
    }
}
return true;

/**
 * 重放,幂等性问题拦截
 * @param userId
 * @return
 */
private boolean checkIdempotent(Long userId,String step) {
    String key = idempotenPrefix+":"+step+":"+userId;
    Object  ret = redisLua.eavl(key,"1",1,1);
    if (null == ret){//redis异常不拦截
        return true;
    }
    if (1 == Integer.valueOf(ret.toString())){//正常
        return true;
    }
    return false;
}

}

Apace实时历史数据库ApaceRDB是长沙软动信息科技有限公司自主研发的一套基于分布式事务型的通用实时数据库系统,它可以应用于现代工业企业,包括电力、石油、矿山、化工、钢铁、电信、航空等领域,为这些行业的SIS监控系统、仿真系统等提供数据保障。 Apace实时历史数据库Apace提供对实时时序数据的压缩、计算、存储、告警、分发、查询、统计功能,同时,为上层业务系统开发提供了丰富的应用接口,包括组态设计器以及Excel扩展报表插件。 Apace实时历史数据库系统引入了多种创新的技术和理念,各方面的指标在同类软件中都名列前茅。在实时数据采集方面,Apace可以在一台普通服务器上稳定的承载百万点的数据同步更新;在历史数据处理方面,在对多种压缩算法进行研究改进后,创造了Apace独有的魔方无损压缩算法,1万点1年的历史数据仅需5.8GB的空间。同时,Apace独创的索引技术,可以实现检索的时间无关性,即可以从几十、上百年的历史数据中高效的检索任一时间点的数据;在告警服务里,Apace首度提出了趋势拟合和波动拟合告警,这项技术让Apace的告警能力得到了质的飞跃,可实现更为复杂的告警规则;在计算服务方面,计算规则可以采用C#、VB.NET或JScript语言进行编写,支持程序集动态引用技术,算法设计者可以使用自定义的第三方程序集(如VC动态链接库),强化了计算服务的计算能力。 Apace实时历史数据库Apace组态图设计器,可以轻松的设计出仿真式组态图、趋势图、报表等各种所需的组态图,Apace已经为用户提供了20多种基础元件,除了这些基础元件以外,用户还可以根据行业的需要来自定义新的元件;组态图支持客户端脚本编码,可对图中各个元件进行编码控制。在设计过程中,可以随时对组态图进行预览,以查看实际效果,设计完成后通过内置的发布功能,可随时发布到指定的服务器上以供使用。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值