Reputation: 405
The issue here is that both threads are executing the first SELECT
at the same time. Considering that saveUser
is a @Transactional
method, should't the second thread wait until the first thread commit/rollback?
Code:
@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 protected]"));
t1.setName("Thread #1");
t1.start();
Thread t2 = new Thread(() -> us.saveUser("[email protected]"));
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;
}
}
Output:
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);
Upvotes: 1
Views: 2237
Reputation: 26064
@Transactional has isolation parameter that specifies isolation level of your transactions.
The transaction isolation level. Defaults to Isolation.DEFAULT.
Default means the default isolation selected by your db. The behaviour you want is described by Serializable isolation level, which, most likely is not the default (Just to name a few: Postgres, MsSql Server, Oracle default to Read Commited)
Upvotes: 2
Reputation: 117
if you want to control multiple threads in transactional environment, you have to use isolation levels.
one example:
@Transactional(isolation = Isolation.SERIALIZABLE)
Upvotes: 2
Reputation: 178
You will have to set the Transaction context for each individual thread for that to happen.
Upvotes: 0