控制并发的方法不少,从最基础的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框架,致使了写充血模型的程序员变多,不注意并发的话,就会出现问题。下面咱们来看看具体的业务场景。(须要知道数据库的行锁)多线程
第一个场景,互联网如此众多的用户修改我的信息,这算不算并发?答案是:算也不算。
回到咱们的并发,第一种业务场景,是可使用如上模式的,对于一条用户数据的修改,咱们容许程序员读取数据到内存中,内存计算修改(耗时操做),提交更改,提交事务。
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
|
可见,若是真正有大量请求到达数据库,光是依靠数据库解决并发是不现实的,因此仅仅只用数据库来作保障而不是彻底依赖。须要根据业务场景选择合适的控制并发手段。