【请求去重】java基于分布式锁解决重复请求问题

问题:开发app时,app快速连续点击会向服务器连续发起请求,导致数据库出现重复数据。

解决思路:

对用户唯一标示+请求uri+请求参数进行去重。

1、利用jvm BlockingQueue堵塞队列,来一条请求判断是否存在队列,不存在添加,存在去除。

优点:消耗资源较小。

缺点:在分布式下,请求会分发在不同服务器上。

2、利用分布式锁,redis、zokeeper等,进行加锁。

优点:解决分布式下请求分发问题。

缺点:每次请求,需要操作一次分布式锁的消耗。

 

考虑第一种在分布式下,将要改变nginx分发策略为根据ip进行分发请求,这样将导致负载均衡作用降低。

采用第二种方式,写起来比较简单,并且redis这种高并发框架,这点消耗不算啥,缺点完全可以接受。

 

redis锁工具类,需要spring的redisTemplate支持。

import org.springframework.data.redis.core.RedisTemplate;

import java.util.concurrent.TimeUnit;

public class RedisLock {
    /**      使用说明
     *
     *      //使用方法,创建RedisLock对象
     *      RedisLock lock = new RedisLock(redisTemplate, "lock_" + product.getId());
     *      try {
     *          if (lock.lock()) {}
     *      } finally {
     *          lock.unlock();
     */


    private RedisTemplate<String, String> redisTemplate;
    /**
     * 重试时间
     */
    private static final int DEFAULT_ACQUIRY_RETRY_MILLIS = 100;
    /**
     * 锁的后缀
     */
    private static final String LOCK_SUFFIX = "_redis_lock";
    /**
     * 锁的前缀
     */
    private static final String LOCK_PREFIX = "REDIS_LOCK:";
    /**
     * 锁的key
     */
    private String lockKey;
    /**
     * 锁超时时间,防止线程在入锁以后,防止阻塞后面的线程无法获取锁
     */
    private int expireMsecs = 60 * 1000;
    /**
     * 线程获取锁的等待时间
     */
    private int timeoutMsecs = 10 * 1000;
    /**
     * 是否锁定标志
     */
    private volatile boolean locked = false;
 
    /**
     * 构造器
     * @param redisTemplate
     * @param lockKey 锁的key
     */
    public RedisLock(RedisTemplate<String, String> redisTemplate, String lockKey) {
        this.redisTemplate = redisTemplate;
        this.lockKey = LOCK_PREFIX + lockKey + LOCK_SUFFIX;
    }
 
    /**
     * 构造器
     * @param redisTemplate
     * @param lockKey 锁的key
     * @param expireMsecs 锁的有效期
     */
    public RedisLock(RedisTemplate<String, String> redisTemplate, String lockKey, int expireMsecs) {
        this(redisTemplate, lockKey);
        this.expireMsecs = expireMsecs;
    }

    /**
     * 构造器
     * @param redisTemplate
     * @param lockKey 锁的key
     * @param timeoutMsecs 获取锁的超时时间
     * @param expireMsecs 锁的有效期
     */
    public RedisLock(RedisTemplate<String, String> redisTemplate, String lockKey,  int expireMsecs , int timeoutMsecs) {
        this(redisTemplate, lockKey, expireMsecs);
        this.timeoutMsecs = timeoutMsecs;
    }

    public String getLockKey() {
        return lockKey;
    }
 
    /**
     * 封装和jedis方法
     * @param key
     * @return
     */
    private String get(final String key) {
        Object obj = redisTemplate.opsForValue().get(key);
        return obj != null ? obj.toString() : null;
    }
 
    /**
     * 封装和jedis方法
     * @param key
     * @param value
     * @return
     */
    private boolean setNX(final String key, final String value) {
        Boolean boo = redisTemplate.opsForValue().setIfAbsent(key, value);
        if(boo) redisTemplate.expire(key,expireMsecs + 1, TimeUnit.MILLISECONDS);
        return boo;
    }
 
    /**
     * 封装和jedis方法
     * @param key
     * @param value
     * @return
     */
    private String getSet(final String key, final String value) {
        Object obj = redisTemplate.opsForValue().getAndSet(key,value);
        if(obj != null){
            redisTemplate.expire(key,expireMsecs + 1,TimeUnit.MILLISECONDS);
            return (String) obj;
        }
        return null;
    }
 
    /**
     * 获取锁
     * @return 获取锁成功返回ture,超时返回false
     * @throws InterruptedException
     */
    public synchronized boolean lock() throws InterruptedException {
        int timeout = timeoutMsecs;
        while (timeout >= 0) {
            long expires = System.currentTimeMillis() + expireMsecs + 1;
            String expiresStr = String.valueOf(expires); //锁到期时间
            if (this.setNX(lockKey, expiresStr)) {
                locked = true;
                return true;
            }
            //redis里key的时间
            String currentValue = this.get(lockKey);
            //判断锁是否已经过期,过期则重新设置并获取
            if (currentValue != null && Long.parseLong(currentValue) < System.currentTimeMillis()) {
            	//设置锁并返回旧值
                String oldValue = this.getSet(lockKey, expiresStr);
                //比较锁的时间,如果不一致则可能是其他锁已经修改了值并获取
                if (oldValue != null && oldValue.equals(currentValue)) {
                    locked = true;
                    return true;
                }
            }
            timeout -= DEFAULT_ACQUIRY_RETRY_MILLIS;
            //延时
            Thread.sleep(DEFAULT_ACQUIRY_RETRY_MILLIS);
        }
        return false;
    }
    /**
     * 释放获取到的锁
     */
    public synchronized void unlock() {
        if (locked) {
            redisTemplate.delete(lockKey);
            locked = false;
        }
    }

}

起初我担心用redis进行加锁不是原子操作。

后lock方法实际测试,为原子操作。

 

在拦截器中取出请求url、请求参数、用户唯一标示 当做锁 在 preHandle 方法中加锁 ,在afterCompletion方法中解锁

如果在perHandle中获取不到锁,return false,

@Component
public class TouristInterceptor implements HandlerInterceptor {.

@Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        Enumeration<String> paramNames = request.getParameterNames();
        while (paramNames.hasMoreElements()) {
            String paramName = (String) paramNames.nextElement();
            String[] paramValues = request.getParameterValues(paramName);
            if (paramValues.length == 1) {
                String paramValue = paramValues[0];
                if (paramValue.length() != 0) {
                    logger.info(paramName + ":" + paramValue);
                }
            }
        }

        // 获取访问URL
        String url = request.getRequestURL().toString();
        logger.info("访问URL:{}", url);

        // 请求去重锁
        requestLock = restapiBaseInterceptor.getRequestLock(request);
        boolean lock = restapiBaseInterceptor.requestLock(requestLock);
        if ( lock) {
            logger.info("【请求锁】获取到锁:{}",requestLock.getLockKey());
            return true;
        } else {
            response.setCharacterEncoding("UTF-8");
            response.setContentType("application/json; charset=utf-8");
             if(!lock){
                try {
                    PrintWriter out = response.getWriter();
                    logger.error("【请求去重】重复请求:{}",requestLock.getLockKey());
                    out.append(Jsons.toJson(new MapleReturnBody(ReturnConstants.PLEASE_NOT_REPEAT)));
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            return false;

        }
    }
@Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response,     Object handler, Exception ex) throws Exception {
        restapiBaseInterceptor.requestUnlock(requestLock);
        logger.info("【请求锁】释放锁:{}",requestLock.getLockKey());
    }
}

我们认为在一个请求在3秒钟没有处理完,就当这个请求死了并释放掉这个请求,我们把锁的超时时间设置3秒,获取所的等待时间设置为1秒。

这样,当两条同样的请求,在第一条请求没有返回信息之前,第二条请求就会被锁挡住。

当用户登录时,我们可以取用户唯一标示当做锁,但是当用户未登录时,怎么防止游客的重复请求呢?

思路:每个设备在进入app时生成uuid,一直保存到他离开app。uuid作为设备唯一标示来限制游客重复请求

问题:怎样判断请求是否是app发出的?这条请求是否已失效呢?

        每个服务器,再用户登录的情况下,肯定有一套来判断用户身份的机制,但是如果未登录,怎么防掉不是由app发出的请求呢?

 

app再发起请求时,带上一个参数,内容为:固定字符串-uuid-时间戳,举例 :

csdn-51254720adc749fab81930c232c1f29f-1539410361628

然后通过AES算法加密后传给服务器,服务器拿到加密的aes后进行解密,分割 – 判断第一段是不是csdn,最后一段与服务器时间比较,是否超过时效时间范围。

这里有一个问题,app的时间戳与服务器时间不一样,解决方案是服务器提供一个查询时间戳接口,app初始化时查询服务器时间戳,用app时间减去服务器时间得到时间差,把计算出的时间差存在本地,需要计算服务器时间戳时,用app时间 减去 时间戳 得到服务器时间

,给出公式:

app时间 – 服务器时间 = 时间差

app时间 – 时间戳 = 服务器时间

 

 

    原文作者:java锁
    原文地址: https://blog.csdn.net/qq_41793222/article/details/83063370
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞