Redis实现分布式锁来防止重复提交

前言:

在系统中,有些接口如果重复提交,可能会造成脏数据或者其他的严重的问题,所以我们一般会对与数据库有交互的接口进行重复处理。我们首先会想到在前端做一层控制。当前端触发操作时,或弹出确认界面,或disable入口并倒计时等等,但是这并不能彻底限制,因此我们这里使用Redis来对某些操作加锁

场景:

场景一:在网络延迟的情况下让用户有时间点击多次submit按钮导致表单重复提交
场景二:表单提交后用户点击【刷新】按钮导致表单重复提交
场景三:用户提交表单后,点击浏览器的【后退】按钮回退到表单页面后进行再次提交
应用:这里我们用到Redis的SETNX key value命令,对于该命令的解释是

将 key 的值设为 value ,当且仅当 key 不存在。
 
若给定的 key 已经存在,则 SETNX 不做任何动作。
 
SETNX 是『SET if Not eXists』(如果不存在,则 SET)的简写。
思路(如果不想每一个请求都单独处理,以下行为,可以在自定义拦截器里面统一处理,1,2步在preHandle中处理,第3步再afterCompletion中处理):

把参数组装好,进行MD5加密作为key,这样如果重复提交的话,这个请求生成的key就是一样的
在请求之前,改action先去拿锁,拿到锁再继续进行下去
请求结束之后,必须释放锁,虽然我们已经对锁做了过期处理,防止死锁,但是不建议只靠这样的操作解锁
 

代码实现:


@Component
public class RedisLock {
 
    public static final int LOCK_EXPIRE = 3000; // ms
 
    @Autowired
    private StringRedisTemplate redisTemplate;
 
 
    /**
     *  分布式锁
     *
     * @param key key值
     * @return 是否获取到
     */
    public boolean lock(String key) {
        String lock = key;
        try {
            return (Boolean) redisTemplate.execute((RedisCallback) connection -> {
                long expireAt = System.currentTimeMillis() + LOCK_EXPIRE;
                Boolean acquire = connection.setNX(lock.getBytes(), String.valueOf(expireAt).getBytes());
                if (acquire) {
                    return true;
                } else {
                    //判断该key上的值是否过期了
                    byte[] value = connection.get(lock.getBytes());
                    if (Objects.nonNull(value) && value.length > 0) {
                        long expireTime = Long.parseLong(new String(value));
                        if (expireTime < System.currentTimeMillis()) {
                            // 如果锁已经过期
                            byte[] oldValue = connection.getSet(lock.getBytes(), String.valueOf(System.currentTimeMillis() + LOCK_EXPIRE).getBytes());
                            // 防止死锁
                            return Long.parseLong(new String(oldValue)) < System.currentTimeMillis();
                        }
                    }
                }
                return false;
            });
        } finally {
            RedisConnectionUtils.unbindConnection(redisTemplate.getConnectionFactory());
        }
    }
 
 
    @Autowired
    private RedisService redisService;
 
    /**
     * 删除锁
     *
     * @param key
     */
    public void delete(String key) {
        try {
            redisTemplate.delete(key);
        } finally {
            RedisConnectionUtils.unbindConnection(redisTemplate.getConnectionFactory());
        }
    }
 
}
测试Controller,如果拿不到锁,则等待0.5秒后继续拿,重复5次

@RestController
public class RedisLockTestController {
 
    @Autowired
    private RedisLock redisLock;
 
    @PostMapping("createOrder")
    public String createOrder(HttpServletRequest request){
        String lockKey = MapUtil.getRedisKeyByParam(request.getParameterMap());
        if (redisLock.lock(lockKey)){
            //处理逻辑
            redisLock.delete(lockKey);
            return "success";
        }else {
            // 设置失败次数计数器, 当到达5次时, 返回失败
            int failCount = 1;
            while(failCount <= 5){
                // 等待100ms重试
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                if (redisLock.lock(lockKey)){
                    // 执行逻辑操作
                    //处理逻辑
                    redisLock.delete(lockKey);
                    return "success";
                }else{
                    failCount ++;
                }
            }
            return "请勿重复提交请求";
        }
 
    }
 
}
请求参数工具类

public class MapUtil {
 
    public static String getRedisKeyByParam(Map<String, String[]> requestParams) {
        //除去数组中的空值
        Map<String, String> sPara = paraFilter(toVerifyMap(requestParams,false));
        //把数组所有元素,按照“参数=参数值”的模式用“&”字符拼接成字符串
        String prestr = createLinkString(sPara);
        //生成签名结果
        String mysign = DigestUtils.md5Hex(getContentBytes(prestr, "UTF-8"));
        return mysign;
    }
 
    /**
     * 除去数组中的空值
     * @param sArray 参数组
     * @return 去掉空值后新的参数组
     */
    public static Map<String, String> paraFilter(Map<String, String> sArray) {
        Map<String, String> result = new HashMap<>();
        if (sArray == null || sArray.size() <= 0) {
            return result;
        }
        for (String key : sArray.keySet()) {
            String value = sArray.get(key);
            if (value == null || value.equals("")) {
                continue;
            }
            result.put(key, value);
        }
        return result;
    }
 
    /**
     * 把数组所有元素排序,并按照“参数=参数值”的模式用“&”字符拼接成字符串
     * @param params 需要排序并参与字符拼接的参数组
     * @return 拼接后字符串
     */
    public static String createLinkString(Map<String, String> params) {
        List<String> keys = new ArrayList<>(params.keySet());
        Collections.sort(keys);
        String prestr = "";
        for (int i = 0; i < keys.size(); i++) {
            String key = keys.get(i);
            String value = params.get(key);
            if (i == keys.size() - 1) {//拼接时,不包括最后一个&字符
                prestr = prestr + key + "=" + value;
            } else {
                prestr = prestr + key + "=" + value + "&";
            }
        }
        return prestr;
    }
 
    private static byte[] getContentBytes(String content, String charset) {
        if (charset == null || "".equals(charset)) {
            return content.getBytes();
        }
        try {
            return content.getBytes(charset);
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException("MD5签名过程中出现错误,指定的编码集不对,您目前指定的编码集是:" + charset);
        }
    }
 
 
    /**
     * 请求参数Map转换验证Map
     * @param requestParams 请求参数Map
     * @param charset 是否要转utf8编码
     * @return
     * @throws UnsupportedEncodingException
     */
    public static Map<String,String> toVerifyMap(Map<String, String[]> requestParams, boolean charset) {
        Map<String,String> params = new HashMap<>();
        for (Iterator iter = requestParams.keySet().iterator(); iter.hasNext();) {
            String name = (String) iter.next();
            String[] values = requestParams.get(name);
            String valueStr = "";
            for (int i = 0; i < values.length; i++) {
                valueStr = (i == values.length - 1) ? valueStr + values[i] : valueStr + values[i] + ",";
            }
            //乱码解决,这段代码在出现乱码时使用。如果mysign和sign不相等也可以使用这段代码转化
            if(charset)
                valueStr = getContentString(valueStr, "UTF-8");
            params.put(name, valueStr);
        }
        return params;
    }
 
    /**
     * 编码转换
     * @param content
     * @param charset
     * @return
     */
    private static String getContentString(String content, String charset) {
        if (charset == null || "".equals(charset)) {
            return new String(content.getBytes());
        }
        try {
            return new String(content.getBytes("ISO-8859-1"), charset);
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException("指定的编码集不对,您目前指定的编码集是:" + charset);
        }
    }
}


 
 

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Redis 可以通过使用 setnx(SET if Not eXists)命令来实现分布式防止某段时间重复提交的功能。 当用户进行操作时,可以先向 Redis 中添加一个标识符作为锁来标记该操作正在进行中。如果成功添加了该标识符,说明该操作尚未被其他用户提交,可以继续执行操作;如果添加失败,说明该操作已经被其他用户提交,则可以视为重复提交,进行相应的处理。 具体实现步骤如下: 1. 在 Redis 中设置一个键,作为标识符,用来表示操作的锁状态。键名可以根据业务需求进行设定,例如可以使用用户id和操作类型等唯一标识进行拼接。 2. 使用 setnx 命令尝试向 Redis 中添加键,设置锁。 3. 如果 setnx 命令执行成功(返回 1),说明锁被成功设置,可以继续进行操作。 4. 如果 setnx 命令执行失败(返回 0),说明锁已经存在,操作可能已经被其他用户提交,可以视为重复提交。 5. 在完成操作后,需要释放锁。可以使用 del 命令来删除键,释放锁。 需要注意的是,在设置锁时可以设定一个过期时间,避免锁一直存在导致其他用户无法进行相同操作。可以使用 expire 命令设置键的过期时间。 使用 Redis 实现分布式防止重复提交的好处是,不需要依赖外部存储或者数据库来进行判断,操作的判定和锁的设置都在内存中进行,速度快且效率高。但需要确保 Redis 的高可用性和数据一致性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值