SSM(十五) 乐观锁与悲观锁的实际应用

SSM(十五) 乐观锁与悲观锁的实际应用

前言

随着互联网的兴起,现在三高(高可用、高性能、高并发)项目是越来越流行。

本次来谈谈高并发。首先假设一个业务场景:数据库中有一条数据,需要获取到当前的值,在当前值的基础上+10,然后再更新回去。
如果此时有两个线程同时并发处理,第一个线程拿到数据是10,+10=20更新回去。第二个线程原本是要在第一个线程的基础上再+20=40,结果由于并发访问取到更新前的数据为10,+20=30

这就是典型的存在中间状态,导致数据不正确。来看以下的例子:

并发所带来的问题

和上文提到的类似,这里有一张price表,表结构如下:


1

2

3

4

5

6

7


CREATE TABLE `price` (

`id` int(11) NOT NULL AUTO_INCREMENT COMMENT ‘主键‘,

`total` decimal(12,2) DEFAULT ‘0.00‘ COMMENT ‘总值‘,

`front` decimal(12,2) DEFAULT ‘0.00‘ COMMENT ‘消费前‘,

`end` decimal(12,2) DEFAULT ‘0.00‘ COMMENT ‘消费后‘,

PRIMARY KEY (`id`)

) ENGINE=InnoDB AUTO_INCREMENT=1268 DEFAULT CHARSET=utf8

我这里写了一个单测:就一个主线程,循环100次,每次把front的值减去10,再写入一次流水记录,正常情况是写入的每条记录都会每次减去10。


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19


/**

* 单线程消费

*/

@Test

public void singleCounsumerTest1(){

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

Price price = priceMapper.selectByPrimaryKey(1);

int ron = 10 ;

price.setFront(price.getFront().subtract(new BigDecimal(ron)));

price.setEnd(price.getEnd().add(new BigDecimal(ron)));

price.setTotal(price.getFront().add(price.getEnd()));

priceMapper.updateByPrimaryKey(price) ;

price.setId(null);

priceMapper.insertSelective(price) ;

}

}

执行结果如下:


可以看到确实是每次都递减10。

但是如果是多线程的情况下会是如何呢:

我这里新建了一个PriceController


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44


/**

* 线程池 无锁

* @param redisContentReq

* @return

*/

@RequestMapping(value = "/threadPrice",method = RequestMethod.POST)

@ResponseBody

public BaseResponse<NULLBody> threadPrice(@RequestBody RedisContentReq redisContentReq){

BaseResponse<NULLBody> response = new BaseResponse<NULLBody>() ;

try {

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

Thread t = new Thread(new Runnable() {

@Override

public void run() {

Price price = priceMapper.selectByPrimaryKey(1);

int ron = 10 ;

price.setFront(price.getFront().subtract(new BigDecimal(ron)));

price.setEnd(price.getEnd().add(new BigDecimal(ron)));

priceMapper.updateByPrimaryKey(price) ;

price.setId(null);

priceMapper.insertSelective(price) ;

}

});

config.submit(t);

}

response.setReqNo(redisContentReq.getReqNo());

response.setCode(StatusEnum.SUCCESS.getCode());

response.setMessage(StatusEnum.SUCCESS.getMessage());

}catch (Exception e){

logger.error("system error",e);

response.setReqNo(response.getReqNo());

response.setCode(StatusEnum.FAIL.getCode());

response.setMessage(StatusEnum.FAIL.getMessage());

}

return response ;

}

其中为了节省资源使用了一个线程池:


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17


@Component

public class ThreadPoolConfig {

private static final int MAX_SIZE = 10 ;

private static final int CORE_SIZE = 5;

private static final int SECOND = 1000;

private ThreadPoolExecutor executor ;

public ThreadPoolConfig(){

executor = new ThreadPoolExecutor(CORE_SIZE,MAX_SIZE,SECOND, TimeUnit.MICROSECONDS,new LinkedBlockingQueue<Runnable>()) ;

}

public void submit(Thread thread){

executor.submit(thread) ;

}

}

关于线程池的使用今后会仔细探讨。这里就简单理解为有10个线程并发去处理上面单线程的逻辑,来看看结果怎么样:

会看到明显的数据错误,导致错误的原因自然就是有线程读取到了中间状态进行了错误的更新。

进而有了以下两种解决方案:悲观锁和乐观锁。

悲观锁

简单理解下悲观锁:当一个事务锁定了一些数据之后,只有当当前锁提交了事务,释放了锁,其他事务才能获得锁并执行操作。

使用方式如下:
首先要关闭MySQL的自动提交:set autocommit = 0;


1

2

3

4

5

6


bigen --开启事务

select id, total, front, end from price where id=1 for update

insert into price values(?,?,?,?,?)

commit --提交事务

这里使用select for update的方式利用数据库开启了悲观锁,锁定了id=1的这条数据(注意:这里除非是使用了索引会启用行级锁,不然是会使用表锁,将整张表都锁住。)。之后使用commit提交事务并释放锁,这样下一个线程过来拿到的就是正确的数据。

悲观锁一般是用于并发不是很高,并且不允许脏读等情况。但是对数据库资源消耗较大。

乐观锁

那么有没有性能好,支持的并发也更多的方式呢?

那就是乐观锁。

乐观锁是首先假设数据冲突很少,只有在数据提交修改的时候才进行校验,如果冲突了则不会进行更新。

通常的实现方式增加一个version字段,为每一条数据加上版本。每次更新的时候version+1,并且更新时候带上版本号。实现方式如下:

新建了一张price_version表:


1

2

3

4

5

6

7

8


CREATE TABLE `price_version` (

`id` int(11) NOT NULL AUTO_INCREMENT COMMENT ‘主键‘,

`total` decimal(12,2) DEFAULT ‘0.00‘ COMMENT ‘总值‘,

`front` decimal(12,2) DEFAULT ‘0.00‘ COMMENT ‘消费前‘,

`end` decimal(12,2) DEFAULT ‘0.00‘ COMMENT ‘消费后‘,

`version` int(11) DEFAULT ‘0‘ COMMENT ‘并发版本控制‘,

PRIMARY KEY (`id`)

) ENGINE=InnoDB AUTO_INCREMENT=1268 DEFAULT CHARSET=utf8

更新数据的SQL:


1

2

3

4

5

6

7


<update id="updateByVersion" parameterType="com.crossoverJie.pojo.PriceVersion">

UPDATE price_version

SET front = #{front,jdbcType=DECIMAL},

version= version + 1

WHERE id = #{id,jdbcType=INTEGER}

AND version = #{version,jdbcType=INTEGER}

</update>

调用方式:


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47


/**

* 线程池,乐观锁

* @param redisContentReq

* @return

*/

@RequestMapping(value = "/threadPriceVersion",method = RequestMethod.POST)

@ResponseBody

public BaseResponse<NULLBody> threadPriceVersion(@RequestBody RedisContentReq redisContentReq){

BaseResponse<NULLBody> response = new BaseResponse<NULLBody>() ;

try {

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

Thread t = new Thread(new Runnable() {

@Override

public void run() {

PriceVersion priceVersion = priceVersionMapper.selectByPrimaryKey(1);

int ron = new Random().nextInt(20);

logger.info("本次消费="+ron);

priceVersion.setFront(new BigDecimal(ron));

int count = priceVersionMapper.updateByVersion(priceVersion);

if (count == 0){

logger.error("更新失败");

}else {

logger.info("更新成功");

}

}

});

config.submit(t);

}

response.setReqNo(redisContentReq.getReqNo());

response.setCode(StatusEnum.SUCCESS.getCode());

response.setMessage(StatusEnum.SUCCESS.getMessage());

}catch (Exception e){

logger.error("system error",e);

response.setReqNo(response.getReqNo());

response.setCode(StatusEnum.FAIL.getCode());

response.setMessage(StatusEnum.FAIL.getMessage());

}

return response ;

}

处理逻辑:开了三个线程生成了20以内的随机数更新到front字段。

当调用该接口时日志如下:

可以看到线程1、4、5分别生成了15,2,11三个随机数。最后线程4、5都更新失败了,只有线程1更新成功了。

查看数据库:

发现也确实是更新的15。

乐观锁在实际应用相对较多,它可以提供更好的并发访问,并且数据库开销较少,但是有可能存在脏读的情况。

总结

以上两种各有优劣,大家可以根据具体的业务场景来判断具体使用哪种方式来保证数据的一致性。

项目地址:https://github.com/crossoverJie/SSM.git

个人博客地址:http://crossoverjie.top

时间: 2024-12-07 10:02:50

SSM(十五) 乐观锁与悲观锁的实际应用的相关文章

oracle的乐观锁和悲观锁

一.问题引出 ① 假设当当网上用户下单买了本书,这时数据库中有条订单号为001的订单,其中有个status字段是'有效',表示该订单是有效的: ② 后台管理人员查询到这条001的订单,并且看到状态是有效的: ③ 用户发现下单的时候下错了,于是撤销订单,假设运行这样一条SQL: update order_table set status = '取消' where order_id = 001: ④ 后台管理人员由于在②这步看到状态有效的,这时,虽然用户在③这步已经撤销了订单,可是管理人员并未刷新界

面试必问系列:谈谈乐观锁与悲观锁!

前言 乐观锁和悲观锁问题,是出现频率比较高的面试题.本文将由浅入深,逐步介绍它们的基本概念.实现方式(含实例).适用场景,以及可能遇到的面试官追问,希望能够帮助你打动面试官. 目录 一.基本概念二.实现方式(含实例)三.优缺点和适用场景四.面试官追问:乐观锁加锁吗?五.面试官追问:CAS有哪些缺点?六.总结 一.基本概念 乐观锁和悲观锁是两种思想,用于解决并发场景下的数据竞争问题. 乐观锁:乐观锁在操作数据时非常乐观,认为别人不会同时修改数据.因此乐观锁不会上锁,只是在执行更新的时候判断一下在此

浅谈Mysql共享锁、排他锁、悲观锁、乐观锁及其使用场景

浅谈Mysql共享锁.排他锁.悲观锁.乐观锁及其使用场景 Mysql共享锁.排他锁.悲观锁.乐观锁及其使用场景 一.相关名词 |--表级锁(锁定整个表) |--页级锁(锁定一页) |--行级锁(锁定一行) |--共享锁(S锁,MyISAM 叫做读锁) |--排他锁(X锁,MyISAM 叫做写锁) |--悲观锁(抽象性,不真实存在这个锁) |--乐观锁(抽象性,不真实存在这个锁) 二.InnoDB与MyISAM Mysql 在5.5之前默认使用 MyISAM 存储引擎,之后使用 InnoDB .查

数据库的锁:行级锁、表锁、乐观锁、悲观锁的实现原理

一.相关名词 表级锁(锁定整个表) 页级锁(锁定一页) 行级锁(锁定一行) 共享锁(S锁,MyISAM 叫做读锁) 排他锁(X锁,MyISAM 叫做写锁) 悲观锁(抽象性,不真实存在这个锁) 乐观锁(抽象性,不真实存在这个锁) 二.InnoDB与MyISAM Mysql 在5.5之前默认使用 MyISAM 存储引擎,之后使用 InnoDB .查看当前存储引擎: show variables like '%storage_engine%'; MyISAM 操作数据都是使用的表锁,你更新一条记录就要

[数据库事务与锁]详解七: 深入理解乐观锁与悲观锁

注明: 本文转载自http://www.hollischuang.com/archives/934 在数据库的锁机制中介绍过,数据库管理系统(DBMS)中的并发控制的任务是确保在多个事务同时存取数据库中同一数据时不破坏事务的隔离性和统一性以及数据库的统一性. 乐观并发控制(乐观锁)和悲观并发控制(悲观锁)是并发控制主要采用的技术手段. 无论是悲观锁还是乐观锁,都是人们定义出来的概念,可以认为是一种思想.其实不仅仅是关系型数据库系统中有乐观锁和悲观锁的概念,像memcache.hibernate.

乐观锁和悲观锁的区别

一分钟教你知道乐观锁和悲观锁的区别 分类: 数据库(Database)2014-07-08 14:06 17588人阅读 评论(2) 收藏 举报 悲观锁(Pessimistic Lock), 顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁.传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁. 乐观锁(Optimistic Lock), 顾名思义,就是很乐观,

乐观锁,悲观锁

转:一分钟教你知道乐观锁和悲观锁的区别 悲观锁(Pessimistic Lock), 顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁.传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁. 乐观锁(Optimistic Lock), 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个

乐观锁与悲观锁及应用举例

最近因为在工作中需要,学习了乐观锁与悲观锁的相关知识,这里我通过这篇文章,把我自己对这两个"锁家"兄弟理解记录下来;       - 悲观锁:正如其名,它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)的修改持保守态度,因此,在整个数据处理过程中,将数据处于锁定状态.悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据).       以常用的mys

乐观锁和悲观锁

乐观锁和悲观锁 为什么需要锁(并发控制)? 在多用户环境中,在同一时间可能会有多个用户更新相同的记录,这会产生冲突.这就是著名的并发性问题. 典型的冲突有: l 丢失更新:一个事务的更新覆盖了其它事务的更新结果,就是所谓的更新丢失.例如:用户A把值从6改为2,用户B把值从2改为6,则用户A丢失了他的更新. l 脏读:当一个事务读取其它完成一半事务的记录时,就会发生脏读取.例如:用户A,B看到的值都是6,用户B把值改为2,用户A读到的值仍为6. 为了解决这些并发带来的问题. 我们需要引入并发控制机