最近同事做一个微信支付的功能,其中微信回调,用到了MQ 防止重复消费的问题,我们做了多重判断,第一重使用redis 锁防止数据被微信的回调多次修改数据库,保证一条数据只修改一次,并配合支付状态来保证消息不会被重复消费,我们的支付状态存在以下几个状态:
- 未支付
- 已支付
- 未消费
- 已入列
当微信进行回调的时候,我们先通过redis 进行锁住,来保证数据在同一时间只有一个请求在修改,如果当前支付状态为未消费(默认为未消费),修改为已入列并放入消息队列,消费成功后状态改为已支付,来保证MQ 不会被重复消费,其中在Redis 锁的部分我们遇到了一些问题:
- 保证锁的有效性,一旦当前的锁被持有,当其他锁再次获取时不能被获取
- 锁超时问题:假如锁超时了,程序还没有处理完成,如何保证程序的安全性,我们这里并没有直接做处理,通过超时机制释放锁,而是只打印了一行日志,并且返回,等待任务执行完成,后,进行锁的删除,之后通过日志来排查导致超时的原因,因为数据超时是存在一定的原因的。
- 锁获取问题:这里我们使用自旋的方式来获取锁
- 锁的并发性:为了保证锁的并发性这里锁定的是当前支付订单的订单号,每个订单号可以获取不同的锁
Redis 锁的设计代码如下:
@Component
@Slf4j
public class RedisLockUtil {
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 等待redis锁
*
* @param redisKey 锁
* @param maxWaitTime 时间 毫秒
* @return true or false
*/
public boolean waitRedisLock(String redisKey, Long maxWaitTime) {
try {
int haveWaitTime = 0;
String value = getLock(redisKey, maxWaitTime);
int unit = 5;
while (StrUtil.isNotBlank(value)) {
ThreadUtil.sleep(unit);
haveWaitTime += unit;
if (haveWaitTime > maxWaitTime) {
log.error("系统获取redis锁超出最大时间:{}", redisKey);
return false;
}
value = getLock(redisKey, maxWaitTime);
if (StrUtil.isBlank(value)) {
return true;
}
}
return true;
} catch (Exception e) {
log.error("系统获取redis锁超出最大时间:{}\n{}", redisKey, e.getMessage());
return false;
}
}
private synchronized String getLock(String redisKey, Long maxWaitTime) {
String key = GlobalRedisKeyEnum.REDIS_LOCK.name() + ":" + redisKey;
String lockStr = redisTemplate.opsForValue().get(key);
if (StrUtil.isBlank(lockStr)) {
redisTemplate.opsForValue().set(key, "1", maxWaitTime, TimeUnit.MILLISECONDS);
}
return lockStr;
}
/**
* 删除redis锁
*
* @param redisKey
*/
public void delRedisLock(String redisKey) {
try {
redisTemplate.delete(GlobalRedisKeyEnum.REDIS_LOCK.name() + ":" + redisKey);
} catch (Exception e) {
log.error("删除redis锁失败:{}", e.getMessage());
}
}
}