使用分布式锁解决多次回调问题

2018-08-04  本文已影响127人  StephenRo

问题

系统有一个支付回调接口,偶尔出现了秒级内回调2次的情况。我们在回调接口中写了推送任务。结果就是2次重复推送。伪代码如下:

1. 获得订单
2. 更改信息和状态(已支付)
3. 通知,推送,定时任务

在接口加入状态判断仍然有2次推送的情况出现。也就是说2个线程出现了并发的情况。这个时候就要使用到分布式锁来限制程序的并发执行。

1. 获得订单
x. 如果订单状态为已支付,则抛异常或直接return
2. 更改信息和状态(已支付)
3. 通知,推送,定时任务

分布式锁

分布式锁是控制分布式系统之间同步访问共享资源的一种方式。在分布式系统中,常常需要协调他们的动作。如果不同的系统或是同一个系统的不同主机之间共享了一个或一组资源,那么访问这些资源的时候,往往需要互斥来防止彼此干扰来保证一致性,在这种情况下,便需要使用到分布式锁。

分布式系统中,常常需要协调他们的动作。如果不同的系统或是同一个系统的不同主机之间共享了一个或一组资源,那么访问这些资源的时候,往往需要互斥来防止彼此干扰来保证一致性,这个时候,便需要使用到分布式锁。

首先,为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件:

Redis实现分布式锁

@Slf4j
public class RedisLock {

    private static final String LOCK_SUCCESS = "OK";
    private static final String SET_IF_NOT_EXIST = "NX";
    private static final String SET_WITH_EXPIRE_TIME = "PX";
    private static final Long RELEASE_SUCCESS = 1L;


    /**
     * 尝试获取分布式锁
     * @param jedis Redis客户端
     * @param lockKey 锁
     * @param requestId 请求标识
     * @param expireTime 超期时间
     * @return 是否获取成功
     */
    public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {

        String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
//        log.info("lock -> {} lockKey -> {} request -> {}" ,result, lockKey, requestId);
        return LOCK_SUCCESS.equals(result);

    }


    /**
     * 释放分布式锁
     * @param jedis Redis客户端
     * @param lockKey 锁
     * @param requestId 请求标识
     * @return 是否释放成功
     */
    public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {

        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
        return RELEASE_SUCCESS.equals(result);

    }
}

简单的测试,controller省略

public void updateStatus(String id) throws Exception {
        Jedis jedis = jedisPool.getResource();
        String requestId = SerialGenerator.randomUUID();
        boolean isGetLock = RedisLock.tryGetDistributedLock(jedis, "onPaySuccess" + id, requestId , 5);
        if (isGetLock) {
            Optional<MerchantOrder> byId = merchantOrderRepository.findById(id);
            MerchantOrder order = byId.get();
            if (order.getStatus() == OrderStatus.PAID) {
                throw new  BaseException("重复消费");
            }
            order.setStatus(OrderStatus.PAID);
            merchantOrderRepository.save(order);
        }
        RedisLock.releaseDistributedLock(jedis, "onPaySuccess" + id,requestId);
    }

JMeter测试

分别 启用10个和100个线程对接口进行测试。从log可以看出只有一个线程的log打印出了sql语句,后面的都显示重复消费。

额外的单机非集群思路

碰到问题,解决问题,仅做记录。如有问题,欢迎指教。

上一篇下一篇

猜你喜欢

热点阅读