- 常见数据库的高并发中,需要用到两种锁,悲观锁与乐观锁,
那什么什么是悲观锁和乐观锁呢?
悲观锁,就是对数据的冲突采取一种悲观的态度,也就是说假设数据肯定会冲突,所以在数据开始读取的时候就把数据锁定住。【数据锁定:数据将暂时不会得到修改】
乐观锁,认为数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则让用户返回错误的信息。让用户决定如何去做。
高并发情况下,两种锁的性能差距多大呢?
看完下面的模拟用户下单扣减库存的例子你就会知道答案了,
一、数据库设计
订单表
CREATE TABLE t_order
(
id
int(11) NOT NULL AUTO_INCREMENT,
inventory
int(11) NOT NULL,
order_no
varchar(255) DEFAULT NULL,
user_name
varchar(255) DEFAULT NULL,
good_id
int(11) DEFAULT NULL,
PRIMARY KEY (id
)
) ENGINE=InnoDB AUTO_INCREMENT=39668 DEFAULT CHARSET=utf8mb4;
库存表
CREATE TABLE t_inventory
(
id
bigint(20) NOT NULL,
good_name
varchar(255) DEFAULT NULL,
inventory
int(11) NOT NULL,
good_id
int(11) DEFAULT NULL,
version
int(11) DEFAULT NULL,
PRIMARY KEY (id
)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
一、模拟用户下单部分代码
每个用户一单,下单成功后,减库存,下单部分代码如下
dao层
Controller层
二、并发测试(无锁)
1000人5秒内 持续10次,
2.1、jmeter测试结果如下
2.2、数据库结果
2.2.1、库存剩余
2.2.2、订单总记录
订单明细部分截图
2.3、分析:
从性能上讲,最长返回时间9秒左右,最短11毫秒,平均为1.7秒,大部集中在2秒左右
从订单总数上看,总共10000个人下单,都下单成功,符合预期,
从剩余库存上看,结果不对,正确结果应该是减少10000,而结果是9997685,少扣减了7685件
三、解决方案:
3.1 数据库加悲观锁
悲观锁顾名思义就是悲观的认为自己操作的数据都会被其他线程操作,所以就必须自己独占这个数据,可以理解为”独占锁“。在java中synchronized和ReentrantLock等锁就是悲观锁,数据库中表锁、行锁、读写锁等也是悲观锁
利用SQL行锁解决并发问题
行锁就是操作数据的时候把这一行数据锁住,其他线程想要读写必须等待,但同一个表的其他数据还是能被其他线程操作的。只要在需要查询的sql后面加上for update,就能锁住查询的行,特别要注意查询条件必须要是索引列,如果不是索引就会变成表锁,把整个表都锁住。
现在在原有的代码的基础上修改一下,先在InventoryDao增加一个手动写sql查询方法。代码如下
package com.test.dao;
import com.test.entity.Inventory;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
/**
- @ClassName InventoryMapper
- @Description TODO
- @Date 2019/3/21/02114:06
- @Version 1.0
/
@Repository
public interface InventoryDao extends JpaRepository<Inventory, Long> {
/- 自定义查询
- @param id
- @return
*/
@Query(value = “select * from t_inventory t1 where t1.id= :id for update”,nativeQuery = true)
public Inventory selectOneById (@Param(“id”) Long id);
}
利用JPA自带行锁解决并发问题
对于刚才提到的在sql后面增加for update,JPA有提供一个更优雅的方式,就是@Lock注解,这个注解的参数可以传入想要的锁级别。
现在在ArticleRepository中增加JPA的锁方法,其中LockModeType.PESSIMISTIC_WRITE参数就是行锁。
/**
- 自定义查询,利用JPA自带的注解加行锁
- @param id
- @return
*/
@Lock(value = LockModeType.PESSIMISTIC_WRITE)
@Query(value = "select t1 from Inventory t1 where t1.id= :id ")
public Inventory selectOneById (@Param(“id”) Long id);
然后把service层的服务改下
public Inventory find(Long id){
//List list = inventoryDao.selectOneById(1L);
/**
jpa自带查询,没有锁,
/
//Inventory inventory = inventoryDao.findById(id);
/
*自定义查询,带锁
*/
return inventoryDao.selectOneById(id);
}
3.1.1 压测结果(悲观锁)
3.1.1.1 jmeter 测试结果如下
3.1.1.2 数据库结果
3.1.1.2.1 库存剩余
3.1.1.2.2 订单总记录
部分截图
3.2 数据库加乐观锁
乐观锁顾名思义就是特别乐观,认为自己拿到的资源不会被其他线程操作所以不上锁,只是在插入数据库的时候再判断一下数据有没有被修改。所以悲观锁是限制其他线程,而乐观锁是限制自己,虽然他的名字有锁,但是实际上不算上锁,只是在最后操作的时候再判断具体怎么操作。
乐观锁通常为版本号机制或者CAS算法
利用SQL实现版本号解决并发问题
版本号机制就是在数据库中加一个字段当作版本号,比如我们加个字段version。那么这时候拿到Article的时候就会带一个版本号,比如拿到的版本是1,然后你对这个Article一通操作,操作完之后要插入到数据库了。发现哎呀,怎么数据库里的Article版本是2,和我手里的版本不一样啊,说明我手里的Article不是最新的了,那么就不能放到数据库了。这样就避免了并发时数据冲突的问题。
所以我们现在给t_inventory表加一个字段version
接着在InventoryDao增加更新的方法,注意这里是更新方法,和悲观锁时增加查询方法不同。
/**
- 自定义更新方法,对比版本号更新
- @param id
- @param inventory
- @return
*/
@Modifying
@Query(value = “update Inventory set inventory = :inventory, version = version + 1 where id = :id and version = :version”, nativeQuery = true)
public int updateById (Long id,int inventory);
Service 屋增加 方法
/**
- 更新库存
- @param id
- @param inventory
- @return
*/
public int updateById(Long id,int inventory){
return inventoryDao.updateById(id,inventory);
}
jpa的代码
@Version
private int version;
dao方法和悲观锁一样,不变,这种方式是非侵入式的,推荐使用
3.2.1 压测结果(乐观锁)
3.2.1.1 jmeter测试结果如下
数据库结果 符合预期,
3.3 综合结果分析
在秒内1万请求的压力测试,jpa中的悲观锁与乐观锁的详情对比下如下
悲观锁
乐观锁
结论,
从平均响应时间上看 乐观锁是悲观锁的2倍,
从95%line上看,乐观锁是悲观锁的2.3倍
从90%line上看,乐观锁是悲观锁的2.5倍
…
其它的对比,童鞋们可以仔细看,无论怎么看乐观锁性能都比百悲观锁大很多,这个测试跑的本地数据库,两个表的数据量都不大,如果两表的数据超过千万,在网络环境里,结果可能还有出入,有兴趣的同学可以试下,有问题童鞋欢迎留言