目录
一、线程安全
synchronized和ReentrantLock 区别
二、悲观锁
优点与不足
InnoDB锁模式
InnoDB锁体验
三、乐观锁
含义
适用场景
乐观锁实现
优点与不足
四、Mysql事务与锁相关
1、事务
2、事务的特性:
3、并发事务带来的问题:
4、mysql 事务的隔离级别
5、事务中的加锁方式:
五、Spring的事务管理模式
一、线程安全
前提: 多线程中的并发控制,保证线程安全。
线程安全是多线程领域的问题,线程安全可以简单理解为一个方法或者一个实例可以在多线程环境中使用而不会出现问题。
在 Java 多线程编程当中,提供了多种实现 Java 线程安全的方式:
- 最简单的方式,使用
Synchronization
关键字 - 使用
java.util.concurrent.atomic
包中的原子类,例如AtomicInteger
- 使用
java.util.concurrent.locks
包中的锁 - 使用线程安全的集合
ConcurrentHashMap
- 使用
volatile
关键字,保证变量可见性(直接从内存读,而不是从线程cache
读。 其核心思想如下:当某个 CPU 在写数据时,如果发现操作的变量是共享变量,则会通知其他 CPU 告知该变量的缓存行是无效的,因此其他 CPU 在读取该变量时,发现其无效会重新从主存中加载数据
synchronized和ReentrantLock 区别
1、线程A和B都要获取对象O的锁定,假设A获取了对象O锁,B将等待A释放对O的锁定,
如果使用 synchronized ,如果A不释放,B将一直等下去,不能被中断
如果 使用ReentrantLock,如果A不释放,可以使B在等待了足够长的时间以后,中断等待,而干别的事情
ReentrantLock获取锁定与三种方式:
a) lock(), 如果获取了锁立即返回,如果别的线程持有锁,当前线程则一直处于休眠状态,直到获取锁
b) tryLock(), 如果获取了锁立即返回true,如果别的线程正持有锁,立即返回false;
c)tryLock(long timeout,TimeUnit unit), 如果获取了锁定立即返回true,如果别的线程正持有锁,会等待参数给定的时间,在等待的过程中,如果获取了锁定,就返回true,如果等待超时,返回false;
d) lockInterruptibly:如果获取了锁定立即返回,如果没有获取锁定,当前线程处于休眠状态,直到或者锁定,或者当前线程被别的线程中断
2、synchronized是是Java语言关键字,在JVM层面上实现的,不但可以通过一些监控工具监控synchronized的锁定,而且在代码执行时出现异常,JVM会自动释放锁定;
但ReentrantLock是java.util.concurrent包下面,是使用Lock则不行,lock是通过代码实现的,要保证锁定一定会被释放,就必须将unLock()放到finally{}中
3、在资源竞争不是很激烈的情况下,Synchronized的性能要优于ReetrantLock,但是在资源竞争很激烈的情况下,Synchronized的性能会下降几十倍,但是ReetrantLock的性能能维持常态;
ReenTrantLock 用法举例:
public class ReentrantLockDemo{
public static void main(String[] arg){
Runnable t1=new MyThread();
new Thread(t1,"t1").start();
new Thread(t1,"t2").start();
}
}
class MyThread implements Runnable {
private Lock lock=new ReentrantLock();
public void run() {
lock.lock();
try{
for(int i=0;i<5;i++)
System.out.println(Thread.currentThread().getName()+":"+i);
}finally{
lock.unlock();
}
}}
public class Test{
private ReentrantLock lock = new ReentrantLock(); @PostConstruct
public void init() {
checkAndRefresh();
}
protected void checkAndRefresh() {
lock.lock();
try {
doSomething...
} finally {
lock.unlock();
}
} }
二、悲观锁
总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java中synchronized
和ReentrantLock
等独占锁就是悲观锁思想的实现。
悲观锁假定会发生并发冲突,悲观锁的实现往往依靠数据库提供的锁机制。备注,在MySQL中使用悲观锁,必须关闭MySQL的自动提交,set autocommit=0。MySQL默认使用自动提交autocommit模式,也即你执行一个更新操作,MySQL会自动将结果提交。
在数据库中,悲观锁的流程如下:
- 在对任意记录修改之前,先尝试为该记录加上排它锁;
- 如果加锁失败,说明该记录正在被修改,那么当前查询可能要等待或者抛出异常。
- 如果成功加锁,那么就可以对记录做修改,事务完成后就可以解锁。其间如果有其它对该记录做修改或加排它锁的操作,都会等待解锁或直接抛出异常。
- 比如:SELECT * FROM CONTRACT where id = ** FOR UPDATE,这样就会对该行记录加上X锁。
需要注意的是,SELECT ... FOR UPDATE会对所有扫描过的行加锁,因此在MySql中使用悲观锁要走索引。另外使用SELECT *** FOR UPDATE加上X锁后,仍然可以使用SELECT * from contract where id =...查询出同一行记录。
优点与不足
悲观并发控制采用了“先取锁再访问”的保守策略,为数据处理的安全提供了保证。但是,数据库通过锁管理器实现加锁或者解锁会增加额外开销,降低数据库的并行性。
InnoDB锁模式
对于insert、update、delete,InnoDB会自动给涉及的数据加排他锁(X);对于一般的Select语句,InnoDB不会加任何锁,事务可以通过以下语句显示加共享锁或排他锁。
共享锁: SELECT ... LOCK IN SHARE MODE;
排他锁: SELECT ... FOR UPDATE;
InnoDB锁体验
在MySQL中行锁是通过给索引项加锁实现的,Oracle则是在数据块中对数据行加锁实现。因此,在MySQL中只有通过索引条件查询数据,InnoDB才能使用行锁,否则将使用表锁。
不加索引的查询,使用表锁
set autocommit=0;//设置手动提交事务
select * from xxx where no = 123 for update;//使用非索引字段查询,使用表锁
commit;//提交事务
使用索引的查询,使用行锁
set autocommit=0;//设置手动提交事务
select * from xxx where id = 123 for update;//使用非索引字段查询,使用行锁
commit;//提交事务
三、乐观锁
含义
总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据(如果数据被其他线程修改,则不进行数据更新,如果数据没有被其他线程修改,则进行数据更新)。
它假设多用户并发的事务在处理时不会彼此互相影响,各事务能够在不产生锁的情况下处理各自影响的那部分数据。在提交数据更新之前,每个事务会先检查在该事务读取数据后,有没有其它事务又修改了该数据。如果其它事务有更新的话,正在提交的事务会进行回滚。
适用场景
比较适合读取操作比较频繁的场景,如果出现大量的写入操作,数据发生冲突的可能性就会增大,为了保证数据的一致性,应用层需要不断的重新获取数据,这样会增加大量的查询操作,降低了系统的吞吐量。
实现方式: 相比于悲观锁,在对数据库进行更新时,乐观锁并不会使用数据库提供的锁机制,而是在业务层进行控制。
乐观锁实现
乐观锁,大多是基于数据版本( Version )记录机制实现。何谓数据版本?即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个 “version” 字段来实现。一般实现乐观锁的方式就是记录版本,在记录版本时通常有两种做法:
- 使用版本号。读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。 对比版本,比如对比version 列,当我们select出来的时候会有一个版本号,比如说version=1,而在我们每次更新时,总会把version加一,所以update的语句可以为 update user set name = 'a' ,version = version +1 where id = 1 and version = 1; 条件中version=1 可以确保没有任何事务更新过记录,否则会更新失败。
- 使用时间戳 。 下面的例子中使用了时间戳表示版本
1、查询出合同信息
select (contract_id,status,version....) from contract
2、生成一个时间戳
newVersion = new Date();
3、更新合同状态
update contract set status = 3,version=newVersion
where contract_id=#{id} and version=#{version}
除此之外,还可以使用表中有意义的字段,比如商品库存的数量。
update prouduct_stock set stockCount = stockCount - deductCount
where stockCount - deductCount > 0
除此之外,还可以使用表中有意义的字段,比如商品库存的数量。
优点与不足
在事务并发度小的场景中,有比较好的效果。乐观锁机制避免了长事务中的数据库加锁开销,大大提升了大并发量下的系统整体性能表现。 但是如果一个用户查询数据在做更新操作之前,第二个用户查询同一行记录,第一个用户更新并提交后,第二个用户不得不重新从数据库取数据。
可以使用版本号机制(数据库中)和CAS算法实现
版本号机制:乐观锁每次在执行数据的修改操作时,都会带上一个版本号,一旦版本号和数据的版本号一致就可以执行修改操作并对版本号执行 +1 操作,否则就执行失败
四、Mysql事务与锁相关
MySQL事务与锁的关系
事务:将对数据库进行的多个操作封装成单个,只有在所有操作都成功的情况下才真正实现变动,否则数据原封不动。
锁:一个让数据不能被修改的功能。
关系可以定义为:锁是实现事务其中一个特性的机制(数据库的锁只跟事务特性里的隔离性有关)。
spring事务与数据库事务与锁之间的关系
spring事务本质上使用数据库事务,而数据库事务本质上使用数据库锁,所以spring事务本质上使用数据库锁,开启spring事务意味着使用数据库锁;
1、事务
简单来说,事务就是一组操作,要么全部成功,要么全部失败。事务,是在数据库中用于保证数据正确性的一种机制
一个小栗子:
事务最常用的一个例子就是转账。A通过支付宝转给B 1000块前,那么随之而来的操作就是:A账号减少1000块,同时 B 账号增加1000块。事务就是保证这两个操作要么同时成功,要么同时失败,不然就会有问题了。
2、事务的特性:
ACID
- 原子性(Automic),事务是原子操作,要么全部成功,要么全部失败
- 一致性(Consistence),执行事务,数据保持一致;
- 隔离性(Isolation),不同事务之间操作互不影响
- 持久性(Durability),一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。
3、并发事务带来的问题:
- 脏读(修改前读了) A、B两个操作分别开启了事务,其中,A 操作修改了 id = 10的数据并未提交事务,B此时读到了A修改的数据,并且进行了操作,那么B此时读到的数据就是脏数据;
- 丢失修改(同时修改) A、B两个操作开启了事务,其中A修改了id=10的数据并未提交事务,B此时也对该数据进行了修改,并进行提交,那么此时就会丢失A的修改;
- 不可重复读(读后,修改了,再读) —— 针对修改而言 A、B两个操作开启了事务,A事务首先读取id=10,后面B事务修改了id=10的数据,然后A事务再次读取时,发现数据与之前不一致。
- 幻读(读后,新增了,再读) —— 针对新增而言 A、B两个操作开启了事务,其中A数据读取数据库,查询出10条数据,此时B事务新增数据库,然后A事务再次查询,发现检索出11条数据,造成幻读的情况。
4、mysql 事务的隔离级别
Mysql InnoDB默认的事务隔离级别为:REPEATABLE_READ
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 是否默认 |
| -------- | -------- | -------- | -------- | -------- |
| READ_UNCOMMITTED | 未解决 | 未解决 | 未解决 | 否 |
| READ_COMMITTED | 解决 | 未解决 | 未解决 | 否 |
| REPEATABLE_READ | 解决 | 解决 | 未解决 | 是 |
| SERIALIZABLE | 解决 | 解决 | 解决 | 否 |
5、事务中的加锁方式:
1、mysql中锁的种类
表级锁、行级锁
2、事务隔离级别与锁的关系
Read Committed(读取提交内容)
在RC级别下,读取是不会加锁的,但是数据的写入、修改、删除是需要加锁的
- 加锁的范围: 1、主键索引只会锁定那一行数据 2、二级索引会锁住当前数据,同时也会锁住与主键索引相关的数据; 3、没有索引的情况下,会将所有扫描的数据进行加锁
更多:https://www.atatech.org/articles/174519
五、Spring的事务管理模式
事务的控制目前最主要的手段已经变成了使用框架去操作了。下面就说说最流行的框架Spring的事务管理模式
但是Spring 的事务和DB的事务还是有区别的,Spring的事务是逻辑事务而非物理事务。最主要的区别在于Spring事务的传播性--
spring针对事务进行一系列的处理:
事务传播机制:
进行数据库操作,两个操作中事务如何管理
REQUIRED(默认值):如果当前没有事务开启一个事务,有的话自动加入到这个事务中
REQUIRES_NEW :针对被调用者,不管调用者是否存在事务,被调用者创建一个新事务。
MANDATORY:使用当前的事务,如果当前没有事务,就抛出异常。
NESTED:如果存在事务,嵌套事务中执行,如果没有则新建
SUPPORTS:支持当前事务,如果当前没有事务,就以非事务方式执行。
NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
NEVER:以非事务方式执行,如果当前存在事务,则抛出异常
事务隔离级别
在多个事务进行处理的时候锁住数据,保证统一时间只有一个事务对数据进行处理,针对不同的情况有不同的锁
DEFAULT(默认值):使用底层数据库事务隔离级别,MySql中使用select @@tx_isolation可以查询当前隔离级别
READ_COMMITTED:读已提交,读到已经提交的数据,可以防止脏读,但是对不可重复读和幻读,所以同一select可能返回不同结果
READ_UNCOMMITTED:读未提交,可以读取没有提交的数据,较少用。因为它的性能也不比其他级别好多少。读取未提交的数据,也被称之为脏读(Dirty Read)
REPEATABLE_READ:重复读取,读出去后自动加锁,其他事务不能修改,解决脏读,不可重复读。这是MySQL的默认事务隔离级别,它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行。不过理论上,这会导致另一个棘手的问题:幻读
SERIALIZABLE:串行化,事务一个排一个执行,一个事务执行完成后执行下一个。这是最高的隔离级别,它通过强制事务排序,使之不可能相互冲突,从而解决幻读问题。简言之,它是在每个读的数据行上加上共享锁。在这个级别,可能导致大量的超时现象和锁竞争。
// 注意@Transactional配置propagation指定隔离范围,rollbackFor指定发生什么情况回滚 @Transactional(propagation=Propagation.REQUIRED,rollbackFor= {NullPointerException.class}) public int updateNames() {
dao层的处理;
}