java 实际生产中多线程、高并发顺序性的处理

多线程、并发顺序性的问题其实很常见。拿个实际项目中的消息推送举个例子,在12点整的时候,有3个事件同时触发了:1、到点领外卖券2、商家折扣3、附件推荐的新品,这几个都是不同商家发起的http请求,并要推送到用户手机上。

如果不考虑并发正常写个controller处理,用户手机上收到消息大概率就是乱序的,有的用户可能收到是3、2、1,有的可能是1、3、2。

这是因为每次一个http请求的时候,tomcat就会从线程池里取出一个空线程来处理,同一时间3个http请求,可能就有3个线程同时处理,这个时候就会有多线程的问题。

因为项目要求推送到手机弹窗的消息要有序,必须是1、2、3推送到用户手机,比较常见的就是用锁、消息队列。这里以单点部署的服务举例

1、如果项目只是单点的部署,只有一台服务器,不建议上锁,对于多线程乱序的问题,无锁化才是最快的。新建一个只有一个线程的线程池ThreadPoolExecutor,把所有的请求放进单个线程池里,让线程通过队列依次消费,这样可以避免线程的上下文切换和上锁等待时间

    /**
     * 确保高并发下大部分场景都是一个线程顺序执行
     */
    private final ExecutorService executorService = new ThreadPoolExecutor(
            1, 20, 60L, TimeUnit.SECONDS, new LinkedBlockingDeque<>());

每次来一个http请求,通过线程池的executorService.submit把处理事件统一放进单个线程里

        try {
            executorService.submit(() -> {
                //处理http请求的事件 商家A的1、2、3事件处理;
            });
        } catch (Exception e) {
            Thread.currentThread().interrupt();
        }

2、如果是多个商家同时推送消息,项目只要保证单个商家的消息是顺序性的,可以选择直接锁住商家的id,这样其他商家发的时候也不会阻塞,只阻塞单个商家的消息。

private void sendMessage(String id, String message) {
    synchronized(id.intern()){
        // 商家A的消息1、2、3
    }
}

3、如果是分布式的时候,可以用redission上分布式的锁,确保每次商家发消息的时候不管多少台服务器都只能乖乖的等待

@Resource
private RedissonClient redissonClient;
///.......

RLock lock = redissonClient.getLock(id);
//根据尝试获取锁的值来判断具体执行的代码
if(lock.tryLock(WAIT_TIME, LEASE_TIME, TimeUnit.SECONDS)) {
     try{
         //处理商家消息1、2、3
     }catch(Exception ex){
         
     }finally{
     	//当获取锁成功时最后一定要记住finally去关闭锁
         lock.unlock();   //释放锁
     } 
}else {
	//else时为未获取锁,则无需去关闭锁
    //如果不能获取锁,则直接做其他事情
}

4、分布式的时候,也可以用消息队列,这里以常用的kafka举例

4.1、分区分配策略:生产者在发送消息时可以选择消息的键(key),Kafka会使用这个键的哈希值来决定消息应该放入哪个分区。如果所有相关消息使用相同的键,它们会被发送到相同的分区,并在那里保持顺序。

public class KeyBasedPartitioner implements Partitioner {

    private AtomicInteger counter = new AtomicInteger(0); // 示例中使用一个原子整数作为轮询计数器

    @Override
    public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
        List<PartitionInfo> partitions = cluster.partitionsForTopic(topic);
        int numPartitions = partitions.size();

        // 假设key是String类型,可以根据业务需求转换key类型并计算分区索引
        if (key instanceof String) {
            int partition = Math.abs(key.hashCode() % numPartitions); // 简单的哈希取模分区策略
            // 或者实现更复杂的逻辑,比如根据key的某些特性路由到固定分区
            return partition;
        } else {
            // 如果没有key,或者key不是预期类型,可以采用默认的轮询方式
            return counter.getAndIncrement() % numPartitions;
        }
    }

    @Override
    public void close() {}

    @Override
    public void configure(Map<String, ?> configs) {}
}

4.2、定制分区策略

自定义的分区器(Partitioner)类来更精确地控制消息的分区分配,适用于那些需要根据业务标识id,保持消息顺序的场景。

自定义分区器,可以确保具有相同业务标识的消息被发送到同一分区,从而在单个分区内部保持消息顺序。

public class OrderIdPartitioner implements Partitioner {

    @Override
    public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
        // 假设key是我们需要排序的订单ID
        if (key instanceof String) {
            int numPartitions = cluster.partitionCountForTopic(topic);
            String orderId = (String) key;
            // 这里只是简单示例,实际项目中应根据业务逻辑制定合适哈希算法
            int partition = Math.abs(orderId.hashCode()) % numPartitions;
            return partition;
        } else {
            // 若key非字符串类型,可以采用默认分区策略
			return DEFAULT_PARTITION;
        }
    }
}

然后注册并使用自定义分区器,确保消息按照业务标识路由到正确的分区。

@Configuration
public class KafkaProducerConfig {

    @Bean
    public KafkaTemplate<String, OrderMsg> kafkaTemplate() {
        Map<String, Object> configProps = new HashMap<>();
        // 其他配置...
        configProps.put(ProducerConfig.PARTITIONER_CLASS_CONFIG, OrderIdPartitioner.class);

        DefaultKafkaProducerFactory<String, OrderMsg> producerFactory = new DefaultKafkaProducerFactory<>(configProps);
        return new KafkaTemplate<>(producerFactory);
    }
}

  • 4
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值