場景描述
大型互聯(lián)網(wǎng)項目中,由于業(yè)務(wù)特點(例如秒殺)同一時間很多的人在使用,用戶連續(xù)快速點擊,而且前端沒有針對性處理,導(dǎo)致連續(xù)發(fā)送兩次請求,此時如果不做好控制,那么系統(tǒng)將會產(chǎn)生很多的數(shù)據(jù)重復(fù)的問題。
解決方案
解決思路:相同的請求在同一時間只能被處理一次。
分布式鎖
1.服務(wù)器A接收到請求之后,獲取鎖,獲取成功
2.服務(wù)器A進行業(yè)務(wù)處理,訂單提交成功
3.服務(wù)器B接收到相同的請求,獲取鎖,失敗,因為鎖被服務(wù)器A獲取了,并且未釋放
4.服務(wù)器A處理完成,釋放鎖 實現(xiàn)采用redis
流程圖

20190128151646582.png
代碼設(shè)計
防重復(fù)提交注解類
/**
* 避免重復(fù)提交注解
* @Author huanglian
* @Date 2019-01-24-下午4:56
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AvoidRepeatableCommit {
long acquireTimeout() default 1000;
long lockTimeout() default 2000;
}
防重復(fù)提交切面類
/**
* 避免重復(fù)提交切面
*
* @Author huanglian
* @Date 2019-01-24-下午5:09
*/
@Component
@Aspect
public class AvoidRepeatableCommitAspect {
private static final Logger logger = LoggerFactory.getLogger(AvoidRepeatableCommitAspect.class);
@Autowired
private StringRedisTemplate stringRedisTemplate;
private AvoidRepeatableCommitLock avoidRepeatableCommitLock;
@PostConstruct
private void init() {
avoidRepeatableCommitLock = new AvoidRepeatableCommitLock(stringRedisTemplate);
}
@Autowired
private HttpServletRequest request;
@Around("execution(public * com.app.sns.controller..*.*(..))")
public Object process(ProceedingJoinPoint joinPoint) throws Throwable {
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
AvoidRepeatableCommit avoidRepeatableCommitAnnotation = method.getAnnotation(AvoidRepeatableCommit.class);
//如果注解為空,則不需要校驗重復(fù)提交
if (avoidRepeatableCommitAnnotation == null) {
return joinPoint.proceed();
}
//按key從小到大,對參數(shù)進行排序,目的是確保統(tǒng)一請求的內(nèi)容加密結(jié)果一致
Map<String, String[]> requestParams = request.getParameterMap();
String body = requestParams.get("body")[0];
//按key從小到大,對參數(shù)body進行排序
TreeMap treeMap = JSONObject.parseObject(body, TreeMap.class);
body = JSONObject.toJSONString(treeMap);
requestParams.put("body", new String[]{body});
String lockName = MapUtil.getRedisKeyByParam(requestParams);
logger.info("lockName ->" + lockName);
long start = System.currentTimeMillis();
//獲取鎖
String identifier = avoidRepeatableCommitLock.acquireLock(lockName);
//鎖標識為空,則表示鎖已存在,當(dāng)前請求是重復(fù)提交
if (identifier == null) {
IBaseResp resp = new IBaseResp();
return BaseRespUtils.buildFailed(resp, ErrorType.FAILED.getErrorCode(), "您操作太頻繁啦");
}
logger.info("acquireLock time -> " + (System.currentTimeMillis() - start));
try {
return joinPoint.proceed();
} finally {
//釋放鎖
boolean flag = avoidRepeatableCommitLock.releaseLock(lockName, identifier);
logger.info("releaseLock flag ->" + flag);
}
}
}
防重復(fù)提交鎖類
/**
* Redis防重復(fù)提交鎖
*
* @Author huanglian
* @Date 2019-01-24-上午9:50
*/
public class AvoidRepeatableCommitLock {
private static Logger logger = LoggerFactory.getLogger(AvoidRepeatableCommitLock.class);
private final long lockTimeout = 3000;
private StringRedisTemplate redisTemplate;
public AvoidRepeatableCommitLock(StringRedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
public String acquireLock(final String lockName) {
return acquireLock(lockName, lockTimeout);
}
public String acquireLock(final String lockName, final long lockTimeout) {
//保證釋放鎖的時候是同一個持有鎖的人
final String identifier = UUID.randomUUID().toString();
final int lockExpire = (int) (lockTimeout / 1000);
final String lockKey = "lock:" + lockName;
final String lua = "if redis.call(\"setnx\",KEYS[1], ARGV[1])==1 then " +
"redis.call(\"expire\",KEYS[1], ARGV[2]) " +
"return 1 " +
"else return 0 end";
Long rs = redisTemplate.execute(new RedisCallback<Long>() {
@Override
public Long doInRedis(RedisConnection redisConnection) throws DataAccessException {
Object nativeConnection = redisConnection.getNativeConnection();
Long result = 0L;
List<String> keys = new ArrayList<>();
keys.add(lockKey);
List<String> values = new ArrayList<>();
values.add(identifier);
values.add(lockExpire + "");
// 集群模式
if (nativeConnection instanceof JedisCluster) {
result = (Long) ((JedisCluster) nativeConnection).eval(lua, keys, values);
}
// 單機模式
if (nativeConnection instanceof Jedis) {
result = (Long) ((Jedis) nativeConnection).eval(lua, keys, values);
}
return result;
}
});
return rs == 1 ? identifier : null;
}
public boolean releaseLock(final String lockName, final String identifier) {
logger.info(lockName + "開始釋放鎖:" + identifier);
final String lockKey = "lock:" + lockName;
final String lua = "if redis.call(\"get\",KEYS[1])==ARGV[1] then " +
"return redis.call(\"del\",KEYS[1]) " +
"else return 0 end";
Long rs = redisTemplate.execute(new RedisCallback<Long>() {
@Override
public Long doInRedis(RedisConnection redisConnection) throws DataAccessException {
Object nativeConnection = redisConnection.getNativeConnection();
Long result = 0L;
List<String> keys = new ArrayList<>();
keys.add(lockKey);
List<String> values = new ArrayList<>();
values.add(identifier);
// 集群模式
if (nativeConnection instanceof JedisCluster) {
result = (Long) ((JedisCluster) nativeConnection).eval(lua, keys, values);
}
// 單機模式
if (nativeConnection instanceof Jedis) {
result = (Long) ((Jedis) nativeConnection).eval(lua, keys, values);
}
return result;
}
});
return rs.intValue() > 0;
}
}
Controller類
/**
* @Author huanglian
* @Date 2019-01-24-上午11:38
*/
@Controller
@RequestMapping(value = "/lock/")
public class LockController {
@AvoidRepeatableCommit
@RequestMapping(value = "/test", method = RequestMethod.POST)
@ResponseBody
public IBaseResp lock() throws InterruptedException {
Thread.sleep(1000);
return BaseRespUtils.buildSuccess(new IBaseResp());
}
}