返回顶部
首页 > 资讯 > 后端开发 > Python >Java中对于并发问题的处理思路分享
  • 797
分享到

Java中对于并发问题的处理思路分享

Java处理并发问题方法Java处理并发问题Java并发问题 2023-02-23 11:02:25 797人浏览 独家记忆

Python 官方文档:入门教程 => 点击学习

摘要

首先我们一起回顾一些并发的场景 首先最基本的,我们要弄清楚什么的并发嘞?我简单粗暴的理解就是:一段代码,在同一时间段内,被多个线程同时处理的情况就是并发现象。下面简单画了个图: 那

首先我们一起回顾一些并发的场景

首先最基本的,我们要弄清楚什么的并发嘞?我简单粗暴的理解就是:一段代码,在同一时间段内,被多个线程同时处理的情况就是并发现象。下面简单画了个图:

那么只要是并发现象就需要我们进行并发处理吗?那肯定不是滴。我们就拿大家都能理解的订单业务来举例,比如说下面两种简单的场景:

  • 对于C端业务来讲,基本上是由一串随机的序列号组成,可以为UUID、数字串、年月日商户(加密)+随机唯一序列号等等方式。这样的目的也是为了保障商户订单量的安全,防止他人去进行恶意分析。
  • 对于B端业务来讲,基本上都是由商户+年月日+顺序递增序列号的方式组成。这样方便客户方进行订单的汇总以及后期的追溯业务。

以上两种场景的区别基本上就是随机唯一序列号和顺序递增序列号的区别。伪代码如下:

public void addOrder() {
    // 1.获取当前年月日以及商户标识
    String currentDate = "yyyyMMddHHmmss";
    String busineSSMan = "商户标识";
    // 2.获取获取序列号
    long index = getIndex();
    // 3.拼接订单号
    String orderNum = businessman + currentDate + index;
    // 4.生成订单
    save(订单对象);
}

那么对于C端的随机唯一序列号来讲,我认为肯定是没必要进行并发控制的,只要写一个生成随机唯一序列号的算法就好了,这样生成出来的订单号必然是唯一的。

public String getIndex() {
    // 根据算法生成唯一序列号
    return buildIndexUtils.build();
}

但对于B端的顺序递增序列号来讲,就需要进行并发控制了。因为既然要保证顺序递增,我在生成当前序列号的同时就必然需要之前上一个单子的序列号是什么,因此我就必然需要一个地方去存储这个序列号。伪代码如下:

public String getIndex() {
    // 1.获取当前商户、当前单据已生成的最大序列号
    Integer index = dao.getIndex(商户, 单据) + 1;
    // 2.序列号 + 1
    index = index++;
    // 3.修改当前商户、当前单据已生成的最大序列号
    dao.update(商户, 单据, index);
    // 4.返回序列号
    return index + "";
}

此时如果事务为可重复读,Thread1开启事务并获取并修改序列号,此时在Thread1未提交事务之前Thread2开启事务并获取序列号。此时两个线程获取到的序列号必然是一致的,这样就会出现订单号重复的问题。

如果更换隔离级别呢?是否能够解决这个问题?

  • 读已提交?同样如果在Thread1提交事务之前Thread2就执行完第一步获取最大序列号呢?一样有问题。
  • 读未提交?一样的呀,在两个Thread都执行完第一步,但没有执行update的情况。
  • 串行化?那就和加同步没啥区别的,而且是阻塞式的。一堆请求占用数据库连接阻塞在这里,如果出现资源耗尽的情况就比较严重了。
  • 不用事务?这个如果遇到2中的场景也一样的。

那么加锁呢?

  • 单机环境下我们可以选择Synchronized或Lock来进行处理。众所周知,jdk1.6之后就对Synchronized进行了改进,不再是单纯的阻塞,而是先进行自旋处理,在一定程度上也达到了自旋节省资源的效果。但是Synchronized或Lock还是要根据实际情况来进行处理的。如果我们为了省事而使用Synchronized对事务代码进行加锁的话,首先我们要保证避免长事务的出现,否则响应超时了,而事务还没有释放,那就比较严重了,异常情况堪比锁表。
  • 分布式环境下我们可以依赖RedisZooKeeper来实现分布式锁。这里需要注意的是,如果要依赖Redis实现的话,尽可能保证Redis采用单实例或分片集群的方式进行部署。主从的部署方式在某种极端情况下出现节点宕机时会导致误判的情况。毕竟Redis是AP性质的。
  • 还可以通过数据库来实现,比如通过select for update来实现行锁、通过version字段实现乐观锁、添加唯一约束的方式。首先select for update实现行锁和上面的串行化事务差别不大,都是数据库连接的阻塞,不建议使用。而乐观锁和唯一约束的方案更适用于作为一个保底方案,否则人家并发请求的时候只有一个请求能成功,其他的都失败。这样的用户体验也不好。

最后我们能得出一个结论。是否进行并发控制要依据该并发操作是否会造成数据安全问题来决定的。好了,下面向大家分享一些在学习工作中对于并发问题的处理思路

由于请求重试导致的并发安全问题

在与第三方系统交互或者微服务内部跨模块交互时,我们通常会采用Httprpc等方式,并设置最大请求时间以及重试次数。因为我们绝对不允许因为下游服务的异常问题而拖累当前服务的正常运行。而通常情况下,最大请求时间也是根据两个服务之间的实际业务以及下游接口进行多次测试而设定的,一般来说不会随便的出现请求超时的情况。但是一旦下游业务的接口因为某种原因(比如网络卡顿或者出现效率问题)导致请求超时的情况,就很有可能因为上游服务的重试而导致下游服务数据重复的问题。

这种情况从本质上来说也就是个重复消费的问题。我们只需要双方配合做好幂等就好了。

1.首先,如果涉及到前端,比如说点击前端的按钮触发业务并且调用下游服务的业务。这个时候既要考虑前端重复提交也要考虑后端的重复发送以及重复消费问题。前端最常用的方式就是做一个进度条或进行防抖处理,避免一个用户频繁点击按钮。

那么如果是多个用户同时提交同一条数据呢?这个情况主要是在B端业务中出现,比如说多个用户均具有这条数据的修改权限,此时也并发点击按钮提交了这条数据。一般来说,这种情况出现的概率还是极少数的,也不会有多少并发量。因此我们直接采用数据库的乐观锁进行保底控制就好了,只允许一个人操作成功,其他人操作失败并提示该数据已被修改。


public void update(Long id, Integer status) {
    // 1.根据ID查询数据
    PO po = dao.select(id);
    // 2.判断数据的状态是否符合修改要求(这一步主要是应对两个线程都进入Controller层,其中线程1刚好提交事务后,线程2开始事务的情况)
    if(!status.equals(po.getStatus())) {
        throw new TJCException("数据已被修改,请刷新后重试");
    }
    // 3.修改数据(启用乐观锁机制,主要应对线程1提交事务之前线程2开启事务的情况)
    int i = dao.update("update table set xxx = ?, version = version + 1 where id = ? and version > ?");
    if(i == 0) {
        throw new TJCException("数据已被修改,请刷新后重试");
    }
    // 继续执行下面业务
}

2.上游服务请求下游服务时,在请求头或消息中添加消息唯一ID。下游服务第一次接收到这个消息后首先将消息保存在缓存中并根据测试结果设置合理的有效期(有效期尽可能比正常请求时间长个一两分钟就好)。这样就可以拦截上述所说的重试导致的重复消费问题。

// 上游服务发送消息
public void request() {
    String messageId = "xxxx";
    rpc.request(messageId, message);
}

// 下游服务消费消息
public void consume(String messageId, String message) {
    // 将messageId存储在redis中, 单机环境也可以直接找个map去存或者存在Guava中
    Boolean flag = stringRedisTemplate.opsForValue()
                .setIfAbsent(messageId, "1", 60, TimeUnit.SECONDS);
    if(!flag) {
       log.error("重复消息拦截");
       return;
    }
    // 继续执行下面业务
     .....
    // 事务完成后(提交/回滚),删除标识
    TransactionSynchronizationManager.reGISterSynchronization(new TransactionSynchronizationAdapter() {
        @Override
        public void afterCompletion(int status) {
            stringRedisTemplate.delete(messageId);
        }
    });
}

在这里是否有小伙伴会有这样的一个疑问,如果重复发送的消息中messageId不一致或者上游服务接口本身就被调用了多次怎么办?

(1)首先,我觉得在上游服务接口本身就被调用了多次的情况下,第一点中的第2步骤(判断数据状态)这种方式就可以把它拦截掉。

(2)其次,如果出现重复发送的消息中messageId不一致的情况,我认为这就属于程序员问题了,可以不放在这里进行考虑。如果硬要考虑的话,貌似也没什么更好的办法,那就加锁吧。

顺序递增订单号问题

在开头我们通过引用这个生成订单号的例子分析了一些什么情况下需要进行并发处理问题,并且上面是采用加锁方式处理的。那么是否还有其他的方式比加锁更好一些呢?比较加锁影响吞吐量呀,哈哈。非必要情况下,我是不会进行加锁处理的,除非在定制开发的过程中,用户的要求是能用就行,那就可以偷懒了哈哈,节省时间去摸鱼!!!!

下面给大家分享一些我常用的一种方式:Redis+lua。我们都知道操作内存肯定是比操作数据库要更快一些的,那么我们可以干脆将各个单据的序列号添加到Redis中。并且订单号是根据年月日来进行重置的,所以我们可以将序列号的过期时间设置为24小时。

伪代码如下:

// 序列号的key可以设置为(模块名:orderIndex:订单类型:yyyyMMdd)
String dateFORMat = getCurrentDateFormat("yyyyMMdd");
// key
String key = 模块名 + ":" + orderIndex + ":" + 订单类型 + ":" + dateFormat;
String script = "if (redis.call('exists', KEYS[1]) == 0) then redis.call('setex', KEYS[1], ARGV[1], ARGV[2]) return 1 else return redis.call('incr', KEYS[1]) end";
DefaultRedisScript<Long> defaultRedisScript = new DefaultRedisScript<>();
defaultRedisScript.setResultType(Long.class);
defaultRedisScript.setScriptText(script);
long count = stringRedisTemplate.execute(defaultRedisScript, Arrays.asList(key), (3600 * 24) + "", "1");

我们都清楚,Redis多指令执行是没办法保证原子性的。所以我们要借助Lua脚本将多个Redis执行以脚本的方式执行来保证多指令执行的原子性,再配合Redis基于内存以及单线程执行指令的优势,可以代替锁来赋予功能更大的吞吐量。

计数统计问题

在工作中我还做过这样一个需求。首先通过消息队列接收、主动拉取数据源的方式获取用户在实际业务中产生的源数据并根据设置的规则比对校验生成符合条件的数据保存在数据库中。并且对通过各个维度对生成的数据进行计数统计并推送下游单据。

比如说其中有一个统计维度为“在各个班的工作时间内,根据次数统计符合条件的数据并汇总推送下游单据”。那么要做这项业务,首先我们要对各个班的数据进行分别计数,当前班开始工作时同步开启计数,结束工作时停止计数,当计数器达到设置的标准后,将这些数据进行统计处理后推送下游单据。

根据上面的业务,通常来说有两种方式解决:

  • 将班、计数量、数据ID等数据存储在数据库中,并对获取数据、处理数据、计数、推送下游单据等操作统一加锁进行处理,保证数据计数的准确性。
  • 依然是通过Redis+Lua的方式进行处理。

最后通过实际的业务分析决定采用Redis+Lua的方式进行处理。只不过这次的Lua要写相对复杂的业务了。

伪代码如下:


public List<Long> countMonitor(Long indexStdId, Long currentTeamClassId, Long 
dataid, Integer count) {
        StringBuilder countMonitorLua = new StringBuilder();
        countMonitorLua.append("if (redis.call('hget', KEYS[1], KEYS[2]) == ARGV[2]) ");
        countMonitorLua.append("then ");
        countMonitorLua.append("    if (redis.call('hget', KEYS[1], KEYS[3]) == ARGV[3]) ");
        countMonitorLua.append("    then ");
        countMonitorLua.append("        redis.call('hset', KEYS[1], KEYS[3], 0) ");
        countMonitorLua.append("        redis.call('lpush', KEYS[4], ARGV[1]) ");
        countMonitorLua.append("        local list = redis.call('lrange', KEYS[4], 0, -1) ");
        countMonitorLua.append("        redis.call('del', KEYS[4]) ");
        countMonitorLua.append("        return list ");
        countMonitorLua.append("    else ");
        countMonitorLua.append("        redis.call('lpush', KEYS[4], ARGV[1]) ");
        countMonitorLua.append("        redis.call('hincrby', KEYS[1], KEYS[3], 1) ");
        countMonitorLua.append("        return {} ");
        countMonitorLua.append("    end ");
        countMonitorLua.append("else ");
        countMonitorLua.append("    redis.call('del', KEYS[4]) ");
        countMonitorLua.append("    redis.call('lpush', KEYS[4], ARGV[1]) ");
        countMonitorLua.append("    redis.call('hset', KEYS[1], KEYS[3], 1) ");
        countMonitorLua.append("    redis.call('hset', KEYS[1], KEYS[2], ARGV[2]) ");
        countMonitorLua.append("    if (redis.call('hget', KEYS[1], KEYS[3]) == ARGV[4]) ");
        countMonitorLua.append("    then ");
        countMonitorLua.append("        redis.call('hset', KEYS[1], KEYS[3], 0) ");
        countMonitorLua.append("        local list2 = redis.call('lrange', KEYS[4], 0, -1) ");
        countMonitorLua.append("        redis.call('del', KEYS[4]) ");
        countMonitorLua.append("        return list2 ");
        countMonitorLua.append("    else ");
        countMonitorLua.append("        return {} ");
        countMonitorLua.append("    end ");
        countMonitorLua.append("end ");

        DefaultRedisScript<List> defaultRedisScript = new DefaultRedisScript<>();
        defaultRedisScript.setResultType(List.class);
        defaultRedisScript.setScriptText(countMonitorLua.toString());
        List<String> keys = new ArrayList<>();
        keys.add(COUNTMONITOR_HASH.replace("${indexStd}", indexStdId.toString()));
        keys.add(COUNTMONITOR_HASH_CURRENTTEAMCLASSID);
        keys.add(COUNTMONITOR_HASH_COUNT);
        keys.add(COUNTMONITOR_LIST.replace("${indexStd}", indexStdId.toString()));
        List dataIdList = stringRedisTemplate.execute(defaultRedisScript, keys, gapDataId.toString(), currentTeamClassId.toString(), (count - 1) + "", count + "");

        List<Long> collect = null;
        if(!gapDataIdList.isEmpty()) {
            collect = (List<Long>) gapDataIdList.stream().map(o -> Long.valueOf(o.toString())).collect(Collectors.toList());
        }
        return collect;
    }

以上代码是根据我实际的业务代码改编成的伪代码,这个段代码没必要看懂哈,首先是伪代码,其实这个业务比较复杂,我也没写注释。更多的还是分享一下优化的处理思路:

首先计数量是由客户定的,可以设置的很小也可以设置的很大。由于这一点考虑,我将计数分成的两部分,一个是String类型的key做计数器,一个是List类型的key用来记录正在被计数的数据ID。这个List有可能是一个大key。所以我们不会去频繁的读取它的数量进行判断,而是通过读取这个String类型的计数器来校验计数。当计数符合条件后就将List取出来。这样做的好处是节省了频繁读取大key的耗时(实际上Redis读取大Key是非常耗时的,我们在实际开发中要时刻注意这一点)。

总结

总体来说,优化并发问题本质上就是通过优化各种请求的耗时(例如事务的耗时、数据库连接的耗时、http/rpc的耗时)来提升功能的吞吐量,达到用最少的资源浪费处理更多的事情。

我处理并发问题的思路总体上也就是通过同步锁、数据库锁以及唯一约束、Redis单线程的天然优势这三点上进行综合考虑,选择中更适合业务场景的一种处理方式。实际上退一万步说,对于一些B端的业务,用户的需求只是能用就行,那我们做定制开发的小伙伴们就直接一个锁就解决问题了,这样何乐而不为呢?还能节省出更多的摸鱼时间!哈哈!!!

但对于做通用产品来说,还是要尽可能的考虑更大的吞吐量。有的小伙伴可能有有疑问,Redis通常的使用规范不是只允许存放那些查询频率非常高的热点数据吗?嗯,那是对于大多数C端互联网项目而言的。而B端项目普遍业务要更加的复杂,而在这个基础上我们要想追求更大的吞吐量,其实用一用Redis也未尝不可哈。毕竟B端的QPS相比于C端来说要根本不在一个数量级。就算是偶然出现几个大Key,能有什么关系呢,只要我们设计的严谨一点,能够把控整体的资源就好啦。

以上就是Java中对于并发问题的处理思路分享的详细内容,更多关于Java处理并发问题的资料请关注编程网其它相关文章!

--结束END--

本文标题: Java中对于并发问题的处理思路分享

本文链接: https://lsjlt.com/news/197299.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

猜你喜欢
  • Java中对于并发问题的处理思路分享
    首先我们一起回顾一些并发的场景 首先最基本的,我们要弄清楚什么的并发嘞?我简单粗暴的理解就是:一段代码,在同一时间段内,被多个线程同时处理的情况就是并发现象。下面简单画了个图: 那...
    99+
    2023-02-23
    Java处理并发问题方法 Java处理并发问题 Java并发问题
  • Java中对于并发问题的处理方法是什么
    本篇内容介绍了“Java中对于并发问题的处理方法是什么”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!首先我们一起回顾一些并发的场景最基本的,...
    99+
    2023-07-05
  • Java中如何处理对象重定向并发问题?
    在Java中,对象重定向并发问题是一种常见的问题。当多个线程同时访问同一个对象时,可能会出现对象重定向并发问题,导致数据不一致或程序崩溃。本文将介绍Java中如何处理对象重定向并发问题,并提供一些示例代码。 一、什么是对象重定向并发问题?...
    99+
    2023-10-15
    对象 重定向 并发
  • java并发问题如何处理
    如何解决并发带来的脏数据问题?使用synchronized来起到同步加锁的作用,首先可以在类方法加synchronized关键字,代表方法锁(相关视频教程分享:java视频教程)也可以用synchronized关键字来声明一个同步块可以使用...
    99+
    2017-08-03
    java教程 java 并发问题 处理
  • 关于JS中的作用域中的问题思考分享
    目录作用域全局作用域作用域中的错误局部作用域with弊端数据泄露性能下降letconst作用域链闭包闭包对作用域链的影响匿名函数的赋值使用let作用域 作用域,也就是我们常说的词法作...
    99+
    2024-04-02
  • 关于Java的HashMap多线程并发问题分析
    目录并发问题的症状多线程put后可能导致get死循环多线程put的时候可能导致元素丢失put非null元素后get出来的却是nullHashMap数据结构HashMap的rehash...
    99+
    2023-05-19
    Java HashMap HashMap多线程并发
  • Go Spring开发技术中,如何处理对象的并发访问问题?
    在Go Spring开发中,对象的并发访问问题是一个比较常见的问题。如果没有进行正确的处理,就有可能出现线程安全问题,导致应用程序出现异常或崩溃。本文将介绍在Go Spring中如何处理对象的并发访问问题。 一、Go Spring中的对象...
    99+
    2023-07-26
    spring 开发技术 对象
  • ASP 中如何处理并发问题?
    ASP 中如何处理并发问题? 在 ASP 开发过程中,处理并发问题是一个非常重要的问题。如果不好地处理并发问题,可能会导致系统出现异常或者数据不一致等问题。本文将介绍 ASP 中如何处理并发问题,并且会穿插一些演示代码。 一、什么是并发问题...
    99+
    2023-11-12
    并发 数据类型 编程算法
  • Java工作中的并发问题处理方法有哪些
    这篇文章主要介绍“Java工作中的并发问题处理方法有哪些”,在日常操作中,相信很多人在Java工作中的并发问题处理方法有哪些问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”Java工作中的并发问题处理方法有哪些...
    99+
    2023-06-15
  • C#开发中如何处理并发访问问题
    C#开发中如何处理并发访问问题在C#开发中,处理并发访问问题是非常重要的,尤其是在多线程环境下。如果不正确处理并发访问,可能会导致数据不一致或者程序崩溃等问题。本文将介绍一些在C#开发中处理并发访问问题的常用方法,并提供具体的代码示例。使用...
    99+
    2023-10-22
    并发处理 线程安全 锁定
  • Go语言中如何处理路径同步和并发问题?
    在Go语言中,路径同步和并发问题是我们经常会遇到的问题。在这篇文章中,我们将介绍如何处理这些问题,并提供一些示例代码来演示如何实现。 路径同步问题 路径同步问题是指在多个协程同时操作同一个数据结构时,可能会导致数据出现错误或不一致的情况。...
    99+
    2023-06-18
    同步 并发 path
  • MySQL主从不同步问题分析与处理思路
    之前部署了Mysql主从复制环境(MySQL主从复制环境部署【http://blog.itpub.net/31015730/viewspace-2153251/】)以及总结了mysql主从复制的原理和...
    99+
    2024-04-02
  • Java中关于文件路径读取问题的分析
    Java读取文件路径 记录一种通用获取文件绝对路径的方法,即使代码换了位置了,这样编写也是通用的: 注意: 使用以下方法的前提是文件必须在类路径下,类路径:凡是在src下的都是类路径...
    99+
    2024-04-02
  • Java项目中获取路径的绝对路径问题和相对路径问题
    目录1.目录结构2.class.getResource(Stringname)3.class.getClassLoader().getResource(Stringname)3.1区...
    99+
    2024-04-02
  • 单例模式在PHP中并发访问的线程安全性处理思路
    然而,在并发访问的情况下,单例模式可能存在线程安全性问题。当多个线程同时请求获取单例对象时,可能会出现竞争条件,导致获得的实例不一致或者创建多个实例。为了解决这个问题,我们需要考虑如何保证单例模式在并发访问时的线程安全性。一种常见的解决方案...
    99+
    2023-10-21
    单例模式 线程安全性 并发访问 处理思路
  • MySQL单表千万级数据处理的思路分享
    目录项目背景改进思路观察数据特征多进程处理思路总结数据处理技巧项目背景 在处理过程中,今天上午需要更新A字段,下午爬虫组完成了规格书或图片的爬取又需要更新图片和规格书字段,由于单表千万级深度翻页会导致处理速度越来越慢...
    99+
    2022-05-20
    MySQL 单表数据处理 MySQL 千万级数据处理
  • 关于JAVA SOCKET UDP的高并发丢包问题
    在使用Java Socket进行UDP通信时,可能会遇到高并发丢包的问题。这是因为UDP协议是一种无连接的协议,不保证数据包的可靠传...
    99+
    2023-08-18
    Java
  • Java编程删除链表中重复的节点问题解决思路及源码分享
    一. 题目在一个排序的链表中,存在重复的结点,请删除该链表中重复的结点,重复的结点不保留,返回链表头指针。 二. 例子输入链表:1->2->3->3->4->4->5 处理后为:1->2->5...
    99+
    2023-05-31
    java 链表 ava
  • Java中的HTTP并发处理,JavaScript该如何应对?
    随着互联网的不断发展,Web应用程序的性能和响应速度已经成为用户使用Web应用程序时最重要的考虑因素之一。HTTP并发处理是Web应用程序性能优化的一个重要方面,可以提高Web应用程序的性能和响应速度。本文将介绍Java中的HTTP并发处...
    99+
    2023-09-06
    http 并发 javascript
  • PHP并发编程中的数据类型:如何处理分布式系统中的数据共享问题?
    随着互联网的发展,分布式系统的应用越来越广泛。分布式系统中常常需要多个进程或者节点之间共享数据。数据共享是分布式系统中必不可少的一部分,而在PHP并发编程中,数据共享是一个复杂的问题。本文将介绍在PHP并发编程中如何处理数据共享问题。 ...
    99+
    2023-11-11
    并发 数据类型 分布式
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作