Spring @Transactional:第二个线程不应该等到第一个线程commit/rollback吗?
Spring @Transactional: Should't the second thread wait until the first thread commit/rollback?
这里的问题是两个线程同时执行第一个 SELECT
。考虑到 saveUser
是一个 @Transactional
方法,第二个线程不应该等到第一个线程 commit/rollback 吗?
代码:
@SpringBootApplication
public class TestApp
{
public static void main(String[] args)
{
ConfigurableApplicationContext app = SpringApplication.run(TestApp.class, args);
UserService us = (UserService) app.getBean("userService");
Thread t1 = new Thread(() -> us.saveUser("email@email.com"));
t1.setName("Thread #1");
t1.start();
Thread t2 = new Thread(() -> us.saveUser("email@email.com"));
t2.setName("Thread #2");
t2.start();
}
}
@Repository
public interface UserRepository extends CrudRepository<UserService.User, Long>
{
public UserService.User getByEmail(String email);
}
@AllArgsConstructor
@Service
public class UserService
{
private final UserRepository userRepository;
@Transactional
public boolean saveUser(String email)
{
if (userRepository.getByEmail(email) != null)
{
System.out.println("User already exists");
return false;
}
System.out.println(Thread.currentThread().getName() + ": User doesn't exists, sleeping..");
try
{
Thread.sleep(5000);
}
catch (InterruptedException e)
{
e.printStackTrace();
}
User user = new User();
user.email = email;
System.out.println(Thread.currentThread().getName() + ": Saving user..");
user = userRepository.save(user);
return user.id > 0;
}
@Table("user")
public static class User
{
@Id
public long id;
public String email;
}
}
输出:
Thread #1: User doesn't exists, sleeping..
Thread #2: User doesn't exists, sleeping..
Thread #1: Saving user..
Thread #2: Saving user..
Exception in thread "Thread #2" org.springframework.data.relational.core.conversion.DbActionExecutionException: Failed to execute DbAction.InsertRoot(entity=testapp.UserService$User@3ce548a)
[...]
Caused by: org.springframework.dao.DuplicateKeyException: PreparedStatementCallback;
[...]
Table:
create table user (`id` int primary key auto_increment, `email` varchar(50) unique);
您必须为每个单独的线程设置事务上下文才能实现。
如果你想在事务环境中控制多个线程,你必须使用隔离级别。
一个例子:
@Transactional(isolation = Isolation.SERIALIZABLE)
@Transactional 有 isolation 指定事务隔离级别的参数。
The transaction isolation level.
Defaults to Isolation.DEFAULT.
默认表示您的数据库选择的默认隔离。
您想要的行为由 Serializable 隔离级别描述,这很可能不是默认的(仅举几例:Postgres、MsSql Server、Oracle 默认为 Read Commited)
这里的问题是两个线程同时执行第一个 SELECT
。考虑到 saveUser
是一个 @Transactional
方法,第二个线程不应该等到第一个线程 commit/rollback 吗?
代码:
@SpringBootApplication
public class TestApp
{
public static void main(String[] args)
{
ConfigurableApplicationContext app = SpringApplication.run(TestApp.class, args);
UserService us = (UserService) app.getBean("userService");
Thread t1 = new Thread(() -> us.saveUser("email@email.com"));
t1.setName("Thread #1");
t1.start();
Thread t2 = new Thread(() -> us.saveUser("email@email.com"));
t2.setName("Thread #2");
t2.start();
}
}
@Repository
public interface UserRepository extends CrudRepository<UserService.User, Long>
{
public UserService.User getByEmail(String email);
}
@AllArgsConstructor
@Service
public class UserService
{
private final UserRepository userRepository;
@Transactional
public boolean saveUser(String email)
{
if (userRepository.getByEmail(email) != null)
{
System.out.println("User already exists");
return false;
}
System.out.println(Thread.currentThread().getName() + ": User doesn't exists, sleeping..");
try
{
Thread.sleep(5000);
}
catch (InterruptedException e)
{
e.printStackTrace();
}
User user = new User();
user.email = email;
System.out.println(Thread.currentThread().getName() + ": Saving user..");
user = userRepository.save(user);
return user.id > 0;
}
@Table("user")
public static class User
{
@Id
public long id;
public String email;
}
}
输出:
Thread #1: User doesn't exists, sleeping..
Thread #2: User doesn't exists, sleeping..
Thread #1: Saving user..
Thread #2: Saving user..
Exception in thread "Thread #2" org.springframework.data.relational.core.conversion.DbActionExecutionException: Failed to execute DbAction.InsertRoot(entity=testapp.UserService$User@3ce548a)
[...]
Caused by: org.springframework.dao.DuplicateKeyException: PreparedStatementCallback;
[...]
Table:
create table user (`id` int primary key auto_increment, `email` varchar(50) unique);
您必须为每个单独的线程设置事务上下文才能实现。
如果你想在事务环境中控制多个线程,你必须使用隔离级别。
一个例子:
@Transactional(isolation = Isolation.SERIALIZABLE)
@Transactional 有 isolation 指定事务隔离级别的参数。
The transaction isolation level. Defaults to Isolation.DEFAULT.
默认表示您的数据库选择的默认隔离。 您想要的行为由 Serializable 隔离级别描述,这很可能不是默认的(仅举几例:Postgres、MsSql Server、Oracle 默认为 Read Commited)