项目中的并发问题(1)

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

什么是并发

由一个你们都了解的例子引入咱们今天的主题:并发java

类共享变量遭遇并发

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:973mysql

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

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

充血模型遭遇并发

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操做spring

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" ;
}

数据库的记录sql

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

可见,若是真正有大量请求到达数据库,光是依靠数据库解决并发是不现实的,因此仅仅只用数据库来作保障而不是彻底依赖。须要根据业务场景选择合适的控制并发手段。

相关文章
相关标签/搜索