Analysis of concurrency in projects

foreword

There are many ways to control concurrency, from the most basic synchronized, lock in juc, to database row-level locks, optimistic locks, pessimistic locks, to middleware-level redis, zookeeper distributed locks. Especially junior programmers, they have always listened to the so-called lock more than they used it. The first article does not discuss concurrency in depth, but is more of an introductory introduction, suitable for beginners. The theme is "according to the specific occurrence of concurrency In business scenarios, use reasonable means of controlling concurrency.”

what is concurrency

Introducing our topic today with an example that everyone knows: concurrency

Class shared variables encounter concurrency

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

In this example, a thread pool with 10 threads is created at the time of initialization, and multiple threads perform an auto-increment operation on the class variable count. In this process, the auto-increment operation is not thread-safe, and the happens-before principle does not guarantee the order of execution of multiple threads, resulting in the final result not being the desired 1000

Next, we move the shared resources in concurrency from class variables to the database.

Hyperemia model encounters complications

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

Still using multithreading, +1 operations on records in the database

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

database records

1
2
id  | count
1   | 344

Programmers who have a first look at the door will think that the most basic ACID of transactions contains atomicity, but the atomicity of transactions and the atomic operations in concurrency discussed today are just a bit similar in terms of nouns. And programmers with a little experience can know what happened in the middle. This is just the tip of the iceberg that exposes the concurrency problems in the project. Don't think that the above code is unnecessary to list. In the actual project development, I have seen some Programmers with years of experience still write code similar to the one above with concurrency problems.

Anemia model encounters concurrency

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

Database results: count: 1000 The expected effect is achieved. I recorded the time-consuming by the way. The console prints: Total cost: 113 ms

Simply compare the second and third examples, both of which want to perform +1 operation on the count of the database. The only difference is that the +1 calculation of the latter occurs in the database, while the calculation of the former depends on the value found in advance, and The computation happens in the program's memory. However, most of the ORM frameworks now lead to more programmers writing congestion models. If they do not pay attention to concurrency, problems will occur. Let's take a look at specific business scenarios.

Business scene

  • edit personal information
  • Modify product information
  • Deduct account balance, deduct inventory

Business Scenario Analysis

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

  • 算,从程序员角度来看,每一个用户请求进来,都是调用的同一个修改入口,具体一点,就是映射到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

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

 

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=324522711&siteId=291194637