167Java利用可重入锁避免并发下出现错误数据,并且避免死锁以及等待锁的时间过长
Posted zhangchao19890805
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了167Java利用可重入锁避免并发下出现错误数据,并且避免死锁以及等待锁的时间过长相关的知识,希望对你有一定的参考价值。
注意
本文只讲解使用可重入锁解决问题的方法,其他方案放在文末,也不考虑 select for update 的方案。
1.场景
我以医院的病房管理系统为例来说明可重入锁。
先放数据库的表结构:
-- 房间表
CREATE TABLE IF NOT EXISTS public.room
(
id bigint NOT NULL, -- 主键
room_no character varying(10) COLLATE pg_catalog."default", -- 房间号
remark character varying(50) COLLATE pg_catalog."default", -- 备注
sort_no integer, -- 排序
CONSTRAINT yqgl_room_pkey PRIMARY KEY (id)
)
-- 病床表
CREATE TABLE IF NOT EXISTS public.t_bed
(
id bigint NOT NULL, -- 主键
room_id bigint NOT NULL, -- 房间ID
user_id bigint NOT NULL, -- 病人ID
bed_no character varying(50) COLLATE pg_catalog."default", -- 床的编号
update_time timestamp without time zone NOT NULL, -- 更新时间
CONSTRAINT yqgl_bed_pkey PRIMARY KEY (id)
)
病床表里的数据是管理员提前按照房间ID和病床编号录入的。对于无人占用的病床记录,字段user_id设置成0。
在医院里每个病房的床位是有限的,护士需要在系统中选择有空床位的病房,来分配给新入院的病人入住。当系统分配完房间后还需要一系列后续操作,比如打印单据交给病人,或者给病人发送提示短信。医院有多名护士多台电脑来同时操作系统分配房间。这个时候并发条件下如果没有锁就有可能出现以下问题:
- 护士A发现101病房空着,点击按钮分配给病人A。
- 于此同时护士B也发现101病房空着,点击按钮分配给病人B。
- 服务器同时接到了两个请求并分配了两个线程A、B来同时处理两个请求。
- 线程A和B同时开启数据库事务。
- 线程A查询数据库发现101房间还有一个空床位,病人A当前没有占用床位。
- 同时线程B查询数据库也发现101房间还有一个空床位,病人B当前没有占用床位。
- 线程A更新数据库,把最后一个床位ID和病人A的ID关联。并执行后续操作。
- 线程B更新数据库,把最后一个床位ID和病人B的ID关联。并执行后续操作。
- 线程A提交数据库事务,返回给护士A成功提示。
- 线程B提交数据库事务,返回给护士B成功提示。
显然护士A的操作被护士B覆盖了。但是系统给出了有误导性的提示。更为严重的是,系统的后续操作会带来更多麻烦,比如打印出了错误的住院单,给病人发送了错误的提示短信。
2.必须要搞清楚锁的范围
首先说明,锁的范围要覆盖数据库事务的范围,即先加锁、再开启事务、然后提交或者回滚事务、最后解锁。如果把锁放在事务中(即先开启事务、再加锁、解锁、最后提交或者回滚事务),是没有效果的。为了弄清楚原因,我们先看数据库的四种隔离机制:
- read uncommited(读未提交)
- read commited (读已提交)
- repeatable read (可重复读)
- Serializable (串行化)
由于性能原因,几乎没有人使用 Serializable。为了数据正确性也几乎没有人使用 read uncommited(读未提交)。常用的是 read commited 和 repeatale read 。下面我们分开讨论:
2.1 read commited
如果我们把加锁的代码放到事务中,因为对数据库的更改未提交,很可能导致两个线程一前一后读取了旧版本的数据并做了修改,然后同时提交事务后,导致其中一个的数据被覆盖。
2.1 repeatable read
虽然可重复读能够让事务中的数据不受其他事务的影响,但是在本例中并不能解决问题。下面我们来演示以下:
- 线程A开启事务、获取锁、查询数据库检查确定可以更新、更新数据库、解锁,在提交前操作系统调度线程。CPU切换到线程B。
- 线程B开启事务、获取锁、查询数据库检查确定可以更新、更新数据库、解锁。因为线程A没有提交数据库,所以这里数据库读取的是旧快照。
- 线程AB提交事务,数据被其中一方覆盖。
3.解决方案
存放锁对象的类 LockUtils
import java.util.concurrent.locks.ReentrantLock;
public class LockUtils
public static final ReentrantLock BED_LOCK = new ReentrantLock();
为了保证锁覆盖事务,我把锁放到 Controller 层:
@RestController
@RequestMapping("/isolate/userInfo")
public class RoomController
/**
* 分配房间
*/
@PutMapping("/distributeRoom")
public R<Void> distributeRoom(@RequestBody Map<String, Object> param)
// 检查参数的代码,已忽略
R r = R.error();
// 使用重入锁,避免同一个房间被分配给多个人。
try
// 这里是最多等待十秒钟就放弃,提示系统用户稍后重试。
boolean flag = LockUtils.BED_LOCK.tryLock(10L * 1000L, TimeUnit.MILLISECONDS);
if (flag)
r = roomService.distributeRoom(patientId, roomId);
else
r = R.error("系统繁忙,请稍后重试。");
catch (InterruptedException e)
e.printStackTrace();
finally
// 确保锁被释放
LockUtils.YQGL_USER_INFO_LOCK.unlock();
return r;
业务类 RoomService,注意这里的方法声明了事务,确保锁的范围覆盖事务的范围。里面的业务使用伪代码编写。
@Service
public class RoomServiceImpl implements RoomService
@Override
@Transactional(rollbackFor = Exception.class)
public R<Void> distributeRoom(Long patientId, Long roomId)
1. 查询数据库,判断房间有没有床位,判断病人当前有没有床位。
2. 如果房间没有床位或者病人已经占用了其他床位,终止执行,给出用户提示。
3. 如果房间有床位并且病人没有床位,就更新数据库,把床位分配给病人。
4. 执行其他相关操作。
以上是关于167Java利用可重入锁避免并发下出现错误数据,并且避免死锁以及等待锁的时间过长的主要内容,如果未能解决你的问题,请参考以下文章
Java并发程序设计(12)并发锁之可重入锁ReentrantLock