问题:开发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时间 – 时间戳 = 服务器时间