✅如何解决消息重复消费、重复下单等问题?

典型回答

重复消费、重复下单的问题,主要的解决办法就是做好幂等,因为在分布式系统中,我们是没办法保证消息不会重新投递的,也没办法保证用户一定不会快速的点击两次进行下单。

所以,对于服务的提供者来说,我们需要在接口中做好幂等控制,来避免因为重复而导致的脏数据。

对于消息的重复消费问题,比较常见的解决方式,就是通过消息中定义的一个幂等号,来做防重判断。这个幂等号一般是约定好的一个业务字段,如果没有这样一个字段的话,也可以用消息中间件的msgid来做幂等控制,但是可能存在一个情况,那就是发送者重复发送了多次消息,这就会导致几次消息的msgid不一样,但是消息内容一致。所以,一般都需要在消息中约定一个唯一的幂等字段或者业务字段。

而对于重复下单的场景,这个幂等号应该怎么产生呢?有一个好的办法就是生成token,当用户每一次访问页面的时候,都向后端接口请求获取一个token,然后在之后本页面的操作中,都需要把这个token带过来。如果页面没有刷新,这个token应该是不变的。

有了这个token就可以用它来做防重控制,并且还能避免有人恶意的刷我们的接口。

在消息或者下单场景中,有了唯一的幂等字段之后,就可以基于一锁、二判、三更新来进行幂等控制了,详见:

✅如何解决接口幂等的问题?

扩展知识

token验证

使用Redis可以很方便地实现token的验证,并且可以让一个token只能用一次,具体的实现方式如下:

import redis.clients.jedis.Jedis;
import java.util.UUID;

public class RedisToken {
    public static void main(String[] args) {
        Jedis jedis = new Jedis("localhost");
        String token = UUID.randomUUID().toString();
        jedis.setex(token, 60 * 60, "1");
        System.out.println("Token: " + token);
        jedis.close();
    }
}

这里只展示了用UUID的方式,而在分布式场景中,如果要生成一个全局唯一的ID,有很多其他方案,这里就不展开介绍了,详见:

✅分布式ID生成方案都有哪些?

在生成了token之后,后续请求就可以校验这个token是否有效,并且确保只能用一次:

import redis.clients.jedis.Jedis;

public class redisTokenVerify {
    public void verify(String token) {
        Jedis jedis = new Jedis("localhost");
        String value = jedis.get(token);
        if (value != null) {
            //删除token
            jedis.del(token);
            //校验成功
        } else {
            //校验失败
        }
        jedis.close();
    }
}

这是一个token校验的简单实现,但是这个逻辑存在一个问题,那就是如果高并发场景,可能会导致多个线程同时,导致token同时校验通过。要解决这个问题,有两个办法,第一个是把get和del放到一个事务中,或者用lua脚本,或者用分布式锁也可以。

如使用Redis的事务:

import redis.clients.jedis.Jedis;
import redis.clients.jedis.Transaction;

public class RedisTokenVerifyWithTransaction {
   public void verify(String token) {
        Jedis jedis = new Jedis("localhost");
        Transaction tx = jedis.multi();
        tx.get(token);
        tx.del(token);
        Object result = tx.exec().get(0);
        if (result != null) {
             //校验成功
        } else {
           //校验失败
        }
        jedis.close();
    }
}

或者直接用 redis 的 del,他会返回被删除的 key 的数量,当返回1的时候表示存在并删除了1条。

1718449178683-830cb17c-7743-4d09-bd30-e6333f744757.png

import redis.clients.jedis.Jedis;
import redis.clients.jedis.Transaction;

public class RedisTokenVerifyWithTransaction {
   public void verify(String token) {
        Jedis jedis = new Jedis("localhost");
        Transaction tx = jedis.multi();
        if (tx.del(token) == 1) {
             //校验成功
        } else {
           //校验失败
        }
        jedis.close();
    }
}

原文: https://www.yuque.com/hollis666/xkm7k3/paqecpn87o0v6np5