在数据库中读取数据时,可能会遇到以下三个常见的问题:脏读(Dirty Read)、不可重复读(Non-repeatable Read)和幻读(Phantom Read)。
这些问题主要涉及并发事务的隔离性和一致性。
- 脏读(Dirty Read): 脏读指的是一个事务读取了另一个事务未提交的数据。假设事务A读取了一行数据,但此时事务B修改了该行数据并未提交。如果事务A读取了未提交的数据,即读取到了不正确或无效的数据,就称为脏读。
示例: 事务A读取某个账户的余额,得到100元。 事务B在事务A未提交的情况下,将该账户的余额修改为200元。 事务A继续执行并提交,此时读取到的余额是200元,但实际上事务B后来又将余额改回了150元。
2.不可重复读(Non-repeatable Read): 不可重复读指的是在一个事务内,对同一行数据进行多次读取,但得到的结果不一致。这是因为在事务读取期间,其他事务修改了该行数据并提交。
示例: 事务A读取某个订单的金额,得到100元。 事务B在事务A读取的过程中修改了该订单的金额为200元,并提交。 事务A再次读取同一行数据,得到200元。
3.幻读(Phantom Read): 幻读指的是在一个事务内,多次查询某个范围的数据时,得到的结果集不一致。这是因为在事务读取期间,其他事务插入或删除了符合该范围条件的数据,并提交。
示例: 事务A查询某个范围内的商品数量,得到10个商品。 事务B在事务A查询的过程中插入了2个新的商品,并提交。 事务A再次查询同一范围内的商品数量,得到12个商品。
事务隔离级别:
1.读未提交(Read Uncommitted):
在该隔离级别下,事务可以读取其他事务尚未提交的数据。这可能导致脏读、不可重复读和幻读的问题。
2.读已提交(Read Committed):
在该隔离级别下,事务只能读取已经提交的数据。这可以解决脏读的问题,但仍可能遇到不可重复读和幻读的问题。
3.可重复读(Repeatable Read):
在该隔离级别下,事务保证在同一个事务内多次读取同一行数据时,得到的结果一致。这可以解决脏读和不可重复读的问题,但仍可能遇到幻读的问题。
可重复读的实现原理主要依赖于数据库的锁机制和多版本并发控制(MVCC)。
锁机制:可重复读可以通过共享锁(Shared Lock)和排他锁(Exclusive Lock)来实现。当一个事务需要读取某行数据时,它会获取该数据的共享锁。在共享锁被持有的期间,其他事务可以读取该行数据,但不能对其进行修改。因此,可确保在事务进行过程中,其他事务不会修改该行数据,从而实现可重复读。
当一个事务需要修改某行数据时,它会获取该数据的排他锁。在排他锁被持有期间,其他事务既不能读取也不能修改该行数据。这样,可以在事务提交之前保持数据的独占访问。
多版本并发控制(MVCC):MVCC通过为每个事务创建一个快照来确保可重复读。每个快照对应数据库在某一时刻的状态,而每个事务只能访问它开始时的快照。这样,即使其他事务在此期间修改数据,本事务也不会看到这些改变,因此可以实现可重复读。
在MVCC中,每个数据行都有两个额外的属性:创建时间戳和过期时间戳(或事务ID)。当事务读取数据时,它只会选择满足以下条件的数据行:创建时间戳早于事务开始时间,并且过期时间戳晚于事务开始时间。这样,事务可以确保读取到的数据行与它开始时的数据库状态一致,从而实现可重复读。
总之,可重复读隔离级别通过锁机制和多版本并发控制来确保在一个事务的生命周期内,同一行数据的多次读取结果是一致的,即使该数据在事务进行过程中被其他事务修改。
4.串行化(Serializable):
在该隔离级别下,事务完全隔离,每个事务按顺序依次执行。它可以解决脏读、不可重复读和幻读的问题,但也降低了并发性能。
示例: 事务A开始并查询某个范围内的商品数量,得到10个商品。 事务B在事务A查询的过程中试图插入新的商品,但被阻塞等待事务A完成。 事务A完成查询后,事务B插入商品。
在spring中可以使用 @Transactional 的 isolation 属性设置 事务的隔离级别:
@Transactional(isolation = Isolation.READ_COMMITTED)
table:
CREATE TABLE `account` (
`actno` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`balance` int(11) DEFAULT NULL,
PRIMARY KEY (`actno`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
dao interface
package com.dao;
import com.pojo.Account;
public interface AccountDao {
/**
* 根据账号查询余额
* @param actno
* @return
*/
Account selectByActno(String actno);
/**
* 更新账户
* @param act
* @return
*/
int update(Account act);
}
dao implement
package com.dao.impl;
import com.dao.AccountDao;
import com.pojo.Account;
import jakarta.annotation.Resource;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
@Repository("accountDao")
public class AccountDaoImpl implements AccountDao {
//JdbcTemplate 由spring提供用以简化jdbc
@Resource(name = "jdbcTemplate")
private JdbcTemplate jdbcTemplate;
@Override
public Account selectByActno(String actno) {
String sql = "select actno, balance from account where actno = ?";
Account account = jdbcTemplate.queryForObject(sql, new BeanPropertyRowMapper<>(Account.class), actno);
return account;
}
@Override
public int update(Account act) {
String sql = "update account set balance = ? where actno = ?";
int count = jdbcTemplate.update(sql, act.getBalance(), act.getActno());
return count;
}
}
pojo
package com.pojo;
public class Account {
private String actno;
private Double balance;
@Override
public String toString() {
return "Account{" +
"actno='" + actno + '\'' +
", balance=" + balance +
'}';
}
public Account() {
}
public Account(String actno, Double balance) {
this.actno = actno;
this.balance = balance;
}
public String getActno() {
return actno;
}
public void setActno(String actno) {
this.actno = actno;
}
public Double getBalance() {
return balance;
}
public void setBalance(Double balance) {
this.balance = balance;
}
}
service
package com.service;
import com.dao.AccountDao;
import com.pojo.Account;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Transactional;
/**
* @program: spring_learn
* @description:
* @author: Mr.Wang
* @create: 2023-06-09 07:09
**/
@Service
public class Service01 {
@Resource(name="accountDao")
public AccountDao accountDao;
@Transactional(isolation = Isolation.READ_UNCOMMITTED)
public void getByActno(String actno) {
Account account = accountDao.selectByActno(actno);
System.out.println("查询到的账户信息:" + account);
}
}
package com.service;
import com.dao.AccountDao;
import com.pojo.Account;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* @program: spring_learn
* @description:
* @author: Mr.Wang
* @create: 2023-06-09 07:09
**/
@Service
public class Service02 {
@Resource(name="accountDao")
public AccountDao accountDao;
@Transactional
public void update(Account act) {
accountDao.update(act);
// 睡眠一会
try {
Thread.sleep(1000 * 20);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
test:
package com;
import com.pojo.Account;
import com.service.Service01;
import com.service.Service02;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
/**
* @program: spring_learn
* @description:
* @author: Mr.Wang
* @create: 2023-06-09 07:47
**/
public class IsolationTest {
@Test
public void testIsolation1(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
Service01 i1 = applicationContext.getBean("service01", Service01.class);
i1.getByActno("004");
}
@Test
public void testIsolation2(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
Service02 i2 = applicationContext.getBean("service02", Service02.class);
Account act = new Account("004", 1000.0);
i2.update(act);
}
}
先执行testIsolation2 更新数据,再执行testIsolation1查询数据,更新的数据还未提交时读取数据,读到的是未提交的数据
因为使用了 @Transactional(isolation = Isolation.READ_UNCOMMITTED)
更新的数据还未提交
使用 @Transactional(isolation = Isolation.READ_COMMITTED) 读已提交
package com.service;
import com.dao.AccountDao;
import com.pojo.Account;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Transactional;
/**
* @program: spring_learn
* @description:
* @author: Mr.Wang
* @create: 2023-06-09 07:09
**/
@Service
public class Service01 {
@Resource(name="accountDao")
public AccountDao accountDao;
@Transactional(isolation = Isolation.READ_COMMITTED)
public void getByActno(String actno) {
Account account = accountDao.selectByActno(actno);
System.out.println("查询到的账户信息:" + account);
}
}
retest:
睡眠事件过,更新数据被提交后