Многопоточные проблемы при использовании Hibernate SessionFactory - PullRequest
4 голосов
/ 12 февраля 2011

Есть таблица 'temp'. Код:

CREATE TABLE `temp` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  `student_id` bigint(20) unsigned NOT NULL,
  `current` tinyint(1) NOT NULL DEFAULT '1',
  `closed_at` datetime NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `unique_index` (`student_id`,`current`,`closed_at`),
  KEY `studentIndex` (`student_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8

Соответствующий Java pojo: http://pastebin.com/JHZwubWd.Эта таблица имеет уникальное ограничение, так что только одна запись для каждого учащегося может быть активной.

2) У меня есть тестовый код, который пытается постоянно добавлять записи для учащегося (каждый раз, когда старшая активная становится неактивной и добавляется новая активная запись), а также в другом потоке, обращающемся к некоторому случайномуне связанные) таблицы.Код:

public static void main(String[] args) throws Exception {
        final SessionFactory sessionFactory = new AnnotationConfiguration().configure().buildSessionFactory();
        ExecutorService executorService = Executors.newFixedThreadPool(1);
        int runs = 0;
        while(true) {
            Temp testPojo = new Temp();
            testPojo.setStudentId(1L);
            testPojo.setCurrent(true);
            testPojo.setClosedAt(new Date(0));
            add(testPojo, sessionFactory);
            Thread.sleep(1500);

            executorService.submit(new Callable<Object>() {
                @Override
                public Object call() throws Exception {
                    Session session = sessionFactory.openSession();
                    // Some dummy code to print number of users in the system.
                    // Idea is to "touch" the DB/session in this background
                    // thread.
                    System.out.println("No of users: " + session.createCriteria(User.class).list().size());
                    session.close();
                    return null;
                }
            });
            if(runs++ > 100) {
                break;
            }
        }

        executorService.shutdown();
        executorService.awaitTermination(1, TimeUnit.MINUTES);
    }

private static void add(final Temp testPojo, final SessionFactory sessionFactory) throws Exception {
        Session dbSession = null;
        Transaction transaction = null;
        try {
            dbSession = sessionFactory.openSession();
            transaction = dbSession.beginTransaction();

            // Set all previous state of the student as not current.
            List<Temp> oldActivePojos = (List<Temp>) dbSession.createCriteria(Temp.class)
                    .add(Restrictions.eq("studentId", testPojo.getStudentId())).add(Restrictions.eq("current", true))
                    .list();
            for(final Temp oldActivePojo : oldActivePojos) {
                oldActivePojo.setCurrent(false);
                oldActivePojo.setClosedAt(new Date());

                dbSession.update(oldActivePojo);
                LOG.debug(String.format("  Updated old state as inactive:%s", oldActivePojo));
            }
            if(!oldActivePojos.isEmpty()) {
                dbSession.flush();
            }

            LOG.debug(String.format("  saving state:%s", testPojo));
            dbSession.save(testPojo);
            LOG.debug(String.format("  new state saved:%s", testPojo));

            transaction.commit();

        }catch(Exception exception) {
            LOG.fatal(String.format("Exception in adding state: %s", testPojo), exception);
            transaction.rollback();
        }finally {
            dbSession.close();
        }
    }

При выполнении кода после нескольких запусков я получаю исключение ограничения индекса.Это происходит потому, что по какой-то странной причине он не находит последнюю активную запись, а вместо этого устаревшую устаревшую активную запись и пытается пометить ее как неактивную перед сохранением (хотя БД фактически уже имеет новую активную запись).* Обратите внимание, что оба кода используют одну и ту же сессионную фабрику, и оба кода работают с совершенно разными таблицами.Я предполагаю, что некоторое внутреннее состояние кэша становится грязным.Если я использую 2 разные сессионные фабрики для переднего плана и фонового потока, он работает нормально.

Еще одна странная вещь заключается в том, что в фоновом потоке (где я печатаю номер пользователя), если я заключаю его в транзакцию (даже если это только операция чтения), код работает нормально!Sp выглядит так, как будто мне нужно обернуть все операции с БД (независимо от чтения / записи) в транзакцию, чтобы она работала в многопоточной среде.

Может кто-нибудь указать на проблему?

1 Ответ

0 голосов
/ 13 февраля 2011

Да, в принципе, демаркация транзакции всегда необходима:

Документация Hibernate говорит:

Всегда необходимы границы базы данных или системы, транзакции. Никакой связи с базой данных не может происходить вне транзакции базы данных (это, кажется, сбивает с толку многих разработчиков, которые привыкли к режиму автоматической фиксации). Всегда используйте четкие границы транзакции, даже для операций только для чтения. В зависимости от уровня изоляции и возможностей базы данных это может не потребоваться, но в этом нет недостатка, если вы всегда четко разграничиваете транзакции.

При попытке воспроизвести ваши настройки я столкнулся с некоторыми проблемами, вызванными отсутствием разграничения транзакций (хотя и не таким, как у вас). Дальнейшие исследования показали, что иногда, в зависимости от конфигурации пула соединений, add() выполняется в той же транзакции базы данных, что и предыдущая call(). Добавление beginTransaction() / commit() к call() решило эту проблему. Это поведение может быть причиной вашей проблемы, поскольку, в зависимости от уровня изоляции транзакции, add() может работать с устаревшим снимком базы данных, сделанным в начале транзакции, то есть во время предыдущего call().

...