浅析项目中的并发

前言

控制并发的方法很多,从最基础的synchronized,juc中的lock,到数据库的行级锁,乐观锁,悲观锁,再到中间件级别的redis,zookeeper分布式锁。特别是初级程序员,对于所谓的锁一直都是听的比用的多,第一篇文章不深入探讨并发,更多的是一个入门介绍,适合于初学者,主题是“根据并发出现的具体业务场景,使用合理的控制并发手段”。

什么是并发

由一个大家都了解的例子引入我们今天的主题:并发

类共享变量遭遇并发


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

public class Demo {

    public Integer count = 0;

    public static void main(String[] args) {

        final Demo demo = new Demo();

        Executor executor = Executors.newFixedThreadPool(10);

        for(int i=0;i<1000;i++){

            executor.execute(new Runnable() {

                @Override

                public void run() {

                    demo.count++;

                }

            });

        }

        try {

            Thread.sleep(5000);

        } catch (InterruptedException e) {

            e.printStackTrace();

        }

        System.out.println("final count value:"+demo1.count);

    }

}

final count value:973

本例中创建了一个初始化时具有10个线程的线程池,多线程对类变量count进行自增操作。这个过程中,自增操作并不是线程安全的,happens-before原则并不会保障多个线程执行的先后顺序,导致了最终结果并不是想要的1000

下面,我们把并发中的共享资源从类变量转移到数据库中。

充血模型遭遇并发


1

2

3

4

5

6

7

8

9

10

11

@Component

public class Demo2 {

    @Autowired

    TestNumDao testNumDao;

    @Transactional

    public void test(){

        TestNum testNum = testNumDao.findOne("1");

        testNum.setCount(testNum.getCount()+1);

        testNumDao.save(testNum);

    }

}

依旧使用多线程,对数据库中的记录进行+1操作


1

2

3

4

5

6

7

8

9

10

11

12

13

14

Demo2 demo2;

public String test(){

    Executor executor = Executors.newFixedThreadPool(10);

    for(int i=0;i<1000;i++){

        executor.execute(new Runnable() {

            @Override

            public void run() {

                demo2.test();

            }

        });

    }

    return "test";

}

数据库的记录


1

2

id  | count

1   | 344

初窥门径的程序员会认为事务最基本的ACID中便包含了原子性,但是事务的原子性和今天所讲的并发中的原子操作仅仅是名词上有点类似。而有点经验的程序员都能知道这中间发生了什么,这只是暴露了项目中并发问题的冰山一角,千万不要认为上面的代码没有必要列举出来,我在实际项目开发中,曾经见到有多年工作经验的程序员仍然写出了类似于上述会出现并发问题的代码。

贫血模型遭遇并发


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

@RequestMapping("testSql")

    @ResponseBody

    public String testSql() throws InterruptedException {

        final CountDownLatch countDownLatch = new CountDownLatch(1000);

        long start = System.currentTimeMillis();

        Executor executor = Executors.newFixedThreadPool(10);

        for(int i=0;i<1000;i++){

            executor.execute(new Runnable() {

                @Override

                public void run() {

                    jdbcTemplate.execute("update test_num set count = count + 1 where id = ‘1‘");

                    countDownLatch.countDown();

                }

            });

        }

        countDownLatch.await();

        long costTime =System.currentTimeMillis() - start;

        System.out.println("共花费:"+costTime+" s");

        return "testSql";

    }

数据库结果: count : 1000 达到了预期效果 这个例子我顺便记录了耗时,控制台打印:共花费:113 ms

简单对比一下二,三两个例子,都是想对数据库的count进行+1操作,唯一的区别就是,后者的+1计算发生在数据库,而前者的计算依赖于事先查出来的值,并且计算发生在程序的内存中。而现在大部分的ORM框架,导致了写充血模型的程序员变多,不注意并发的话,就会出现问题。下面我们来看看具体的业务场景。

业务场景

  • 修改个人信息
  • 修改商品信息
  • 扣除账户余额,扣减库存

业务场景分析

第一个场景,互联网如此众多的用户修改个人信息,这算不算并发?答案是:算也不算。

  • 算,从程序员角度来看,每一个用户请求进来,都是调用的同一个修改入口,具体一点,就是映射到controller层的同一个requestMapping,所以一定是并发的。
  • 不算,虽然程序是并发的,但是从用户角度来分析,每个人只可以修改自己的信息,所以,不同用户的操作其实是隔离的,所以不算“并发”。这也是为什么很多开发者,在日常开发中一直不注意并发控制,却也没有发生太大问题的原因,大多数初级程序员开发的还都是CRM,OA,CMS系统。

回到我们的并发,第一种业务场景,是可以使用如上模式的,对于一条用户数据的修改,我们允许程序员读取数据到内存中,内存计算修改(耗时操作),提交更改,提交事务。


1

2

3

4

5

6

7

//Transaction start

User user = userDao.findById("1");

user.setName("newName");

user.setAge(user.getAge()+1);

...//其他耗时操作

userDao.save(user);

//Transaction commit

这个场景变现为:几乎不存在并发,不需要控制,场景乐观。

为了严谨,也可以选择控制并发,但我觉得这需要交给写这段代码的同事,让他自由发挥。

第二个场景已经有所不同了,同样是修改一个记录,但是系统中可能有多个操作员来维护,此时,商品数据表现为一个共享数据,所以存在微弱的并发,通常表现为数据的脏读,例如操作员A,B同时对一个商品信息维护,我们希望只能有一个操作员修改成功,另外一个操作员得到错误提示(该商品信息已经发生变化),否则,两个人都以为自己修改成功了,但是其实只有一个人完成了操作,另一个人的操作被覆盖了。

这个场景表现为:存在并发,需要控制,允许失败,场景乐观。

通常我建议这种场景使用乐观锁,即在商品属性添加一个version字段标记修改的版本,这样两个操作员拿到同一个版本号,第一个操作员修改成功后版本号变化,另一个操作员的修改就会失败了。


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

class Goods{

    @Version

    int version;

}

//Transaction start

try{

    Goods goods = goodsDao.findById("1");

    goods.setName("newName");

    goods.setPrice(goods.getPrice()+100.00);

    ...//其他耗时操作

    goodsDao.save(goods);

}catch(org.hibernate.StaleObjectStateException e){

    //返回给前台

}

//Transaction commit

springdata配合jpa可以自动捕获version异常,也可以自动手动对比。

第三个场景 这个场景表现为:存在频繁的并发,需要控制,不允许失败,场景悲观。

强调一下,本例不应该使用在项目中,只是为了举例而设置的一个场景,因为这种贫血模型无法满足复杂的业务场景,而且依靠单机事务来保证一致性,并发性能和可扩展性能不好。

一个简易的秒杀场景,大量请求在短时间涌入,是不可能像第二种场景一样,100个并发请求,一个成功,其他99个全部异常的。

设计方案应该达到的效果是:有足够库存时,允许并发,库存到0时,之后的请求全部失败;有足够金额时,允许并发,金额不够支付时立刻告知余额不足。

可以利用数据库的行级锁, update set balance = balance – money where userId = ? and balance >= money; update stock = stock – number where goodsId = ? and stock >= number ; 然后在后台 查看返回值是否影响行数为1,判断请求是否成功,利用数据库保证并发。

需要补充一点,我这里所讲的秒杀,并不是指双11那种级别的秒杀,那需要多层架构去控制并发,前端拦截,负载均衡….不能仅仅依赖于数据库的,会导致严重的性能问题。为了留一下一个直观的感受,这里对比一下oracle,mysql的两个主流存储引擎:innodb,myisam的性能问题。


1

2

3

4

5

6

oracle:

10000个线程共计1000000次并发请求:共花费:101017 ms =>101s

innodb:

10000个线程共计1000000次并发请求:共花费:550330 ms =>550s

myisam:

10000个线程共计1000000次并发请求:共花费:75802 ms =>75s

可见,如果真正有大量请求到达数据库,光是依靠数据库解决并发是不现实的,所以仅仅只用数据库来做保障而不是完全依赖。需要根据业务场景选择合适的控制并发手段。

原文地址:https://www.cnblogs.com/javalyy/p/8875579.html

时间: 2024-10-27 07:41:24

浅析项目中的并发的相关文章

项目中遇到并发问题和解决办法

最近在做一朋友帮砍价的活动.建立在微信公众号端的一个电商平台,然后我负责砍价模块. 由于这个模块高并发的几率比较大,所有有些逻辑模块就要采取一些缓存技术和排它锁的用户, 比如:由于项目需求是可以多个人同时砍价,我们又有砍到最低价格的限制,所以不进行处理的话很有可能就会超出我们所限制的价格!所以当用户砍价砍到最低价的时候就需要用到排它锁了 ,直接上代码: //说明砍到最低价 $order_price = $res['bg_order_price']-$res['bg_floorprice']; $

面试常问问题:银行网上支付项目中怎么控制多线程高并发访问?

面试常问问题:银行网上支付项目中怎么控制多线程高并发访问? synchronized关键字主要解决多线程共享数据同步问题. ThreadLocal使用场合主要解决多线程中数据因并发产生不一致问题. ThreadLocal和Synchonized都用于解决多线程并发访问.但是ThreadLocal与synchronized有本质的区别: synchronized是利用锁的机制,使变量或代码块在某一时该只能被一个线程访问.而ThreadLocal为每一个线程都提供了变量的副本,使 得每个线程在某一时

项目开发中应用并发的一二事

在多线程环境下,使用BlockingCollection以及ConcurrentQueue来消费生产者生产的资源,这是我自己写的多生产者多消费者的作法,其实也是基于单个task下的阻塞队列的IsComplete来识别的. 使用阻塞队列更简单但是内部的消费者线程比较适合使用单独的线程不适合使用线程池,而且阻塞队列为空时会阻塞消费者线程,当然阻塞线程池内的线程也没什么影响只是不推荐这么做,而且阻塞的队列的性能也没有ConcurrentQueue的性能高. 我在项目中遇到多生产者多消费者问题,多生产者

项目中使用Redis的一些总结和体会

第一部分:为什么我的项目中要使用Redis 我知道有些地方没说到位,希望大神们提出来,我会吸取教训,大家共同进步! 注册时邮件激活的部分使用Redis 发送邮件时使用Redis的消息队列,减轻网站压力. 使用Lucene.Net在进行分词时使用Redis消息队列和多线程来避免界面卡死等性能问题. 请大家先思考一个问题:这个问题在大并发.高负载的网站中必须考虑!大家思考如何让速度更快. 三种方法:(1)数据库(2)页面静态化(3)Redis.Memcached 第二部分:Redis是什么 概述:r

项目中遇到的某些问题及解决办法(一)

简介 该博文记录了一些平时在工作中遇到的问题及解决办法,某些问题有解决办法,某些问题暂时没有解决办法,如果有大神知道的,请多多指点. 如果某些问题有更好的解决办法,也请指教. 正文 1.在一个方法中用泛型操作两个不同的类型(Type). 难点:需要实现一个方法,进入参数一个泛型,返回信息一个泛型.但是一个方法中泛型只支持一种类型. 解决办法:将进入和返回放在一个类型中,用特性将进入参数和返回参数区分开. 2.微信三方登录,需要在PC桌面应用端+API服务实现. 难点:微信官网只提供了网页三方登录

[翻译]在 .NET Core 中的并发编程

原文地址:http://www.dotnetcurry.com/dotnet/1360/concurrent-programming-dotnet-core 今天我们购买的每台电脑都有一个多核心的 CPU,允许它并行执行多个指令.操作系统通过将进程调度到不同的内核来发挥这个结构的优点.然而,还可以通过异步 I/O 操作和并行处理来帮助我们提高单个应用程序的性能.在.NET Core中,任务 (tasks) 是并发编程的主要抽象表述,但还有其他支撑类可以使我们的工作更容易. 并发编程 - 异步 v

CJCMS系列---说说项目中的缓存实现(1)

缓存者,临时文件交换区也.主要就是方便查找,提高查找效率(效率在于读内存速度比读硬盘快).  大多数的项目的缓存都是通过设定过期时间来做的,可是我对于这样的替换策略不以为然,而且会导致混乱. 有人说:最让人蛋疼的莫过于命名和缓存了. 那么缓存蛋疼在哪里呢,那就是容易导致脏数据,缓存不应该成为脏数据,而大多数时间脏数据不可避免. 举一个例子:淘宝商城,我店里有一个商品,当时缓存的库存100,但是赶上光棍节大酬宾,各种并发请求,也许你在买的时候缓存里面还有20,但是100个订单已经下过了,但是点击下

Java 容器在实际项目中的应用

前言:在java开发中我们离不开集合数组等,在java中有个专有名词:"容器" ,下面会结合Thinking in Java的知识和实际开发中业务场景讲述一下容器在Web项目中的用法.可结合图片代码了解Java中的容器 备注 :这个地方 ,参考于朝向远方的博客Java容器详解,既然前人总结的这么好,我就直接拿来用,在这里更注重在实际开发中的例子,感谢那些总结的前辈们,辛苦了. 简单的数组例子 Thinking in Java 中并没有把数组归为Java的容器,实际上数组的确不是Java

iOS HmacSHA1加密 和 MD5 Base64加密 --iOS开发系列---项目中成长的知识五

项目中开发中需要对一些数据进行加密后和服务器验证是否是我们客户端发出的请求! 方案是服务器定的,使用HmacSHA1加密和MD5 Base64加密 加密过程比较复杂 1.获取格林威治时间 2.用base64编码对请求数据内容进行MD5值计算 3.设置请求格式 4.设置鉴权信息,需要对上面的3个内容以及请求的链接 进行HMacSHA1再次进行加密 加密过后把上面4步获得的值加入到http的请求头中,一并发送给服务器,服务器经过验证后,才返回给我们我们想要的信息 下面贴代码说明我们使用的两个加密 首