Недавно я столкнулся с проблемами параллелизма в бизнесе компании, и это все еще очень распространенная проблема параллелизма, которая считается ошибкой низкого уровня. Поскольку бизнес компании относительно сложен и не подходит для публичного раскрытия, здесь мы используем очень распространенный бизнес для восстановления сцены и рассказываем, как пессимистичные блокировки и оптимистичные блокировки решают такие проблемы параллелизма.
Бизнес компании это самая распространенная проблема "заказ+аккаунт".После решения проблемы компании я повернул голову и подумал,мой проект блогаFameТак же есть такая же проблема в (хотя в трафике вообще не нужно учитывать проблему параллелизма...), то возьму это как пример.
восстановление бизнеса
Первая среда: Spring Boot 2.1.0 + data-jpa + mysql + lombok
Дизайн базы данных
В системе блогов с функцией комментариев обычно есть две таблицы: 1. Таблица статей 2. Таблица комментариев. В таблице статей, помимо сохранения некоторой информации о статье и т.д., также есть поле для сохранения количества комментариев. Мы разрабатываем минимальную структуру таблицы для восстановления этого бизнес-сценария.
статья статья таблица
поле | тип | Примечание |
---|---|---|
id | INT | Auto-Accrepretion Первичный ключ ID |
title | VARCHAR | название статьи |
comment_count | INT | Количество комментариев к статье |
форма комментариев
поле | тип | Примечание |
---|---|---|
id | INT | автоматическое увеличение идентификатора первичного ключа |
article_id | INT | Комментарий к статье ID. |
content | VARCHAR | Комментарии |
Когда пользователь комментирует, 1. Получить статью в соответствии с идентификатором статьи 2. Вставить запись комментария 3. Количество комментариев к статье увеличивается и сохраняется
Код
Сначала введем соответствующие зависимости в maven
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.0.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
Затем напишите класс сущности, соответствующий базе данных
@Data
@Entity
public class Article {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
private Long commentCount;
}
@Data
@Entity
public class Comment {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private Long articleId;
private String content;
}
Затем создайте репозиторий, соответствующий этим двум классам сущностей, благодаря Spring-jpa-dataCrudRepository
Для нас были реализованы наиболее распространенные CRUD-операции, поэтому нашему репозиторию нужно только наследоватьCrudRepository
Интерфейс больше ничего не делает.
public interface ArticleRepository extends CrudRepository<Article, Long> {
}
public interface CommentRepository extends CrudRepository<Comment, Long> {
}
Затем мы просто реализуем интерфейс Controller и класс реализации Service.
@Slf4j
@RestController
public class CommentController {
@Autowired
private CommentService commentService;
@PostMapping("comment")
public String comment(Long articleId, String content) {
try {
commentService.postComment(articleId, content);
} catch (Exception e) {
log.error("{}", e);
return "error: " + e.getMessage();
}
return "success";
}
}
@Slf4j
@Service
public class CommentService {
@Autowired
private ArticleRepository articleRepository;
@Autowired
private CommentRepository commentRepository;
public void postComment(Long articleId, String content) {
Optional<Article> articleOptional = articleRepository.findById(articleId);
if (!articleOptional.isPresent()) {
throw new RuntimeException("没有对应的文章");
}
Article article = articleOptional.get();
Comment comment = new Comment();
comment.setArticleId(articleId);
comment.setContent(content);
commentRepository.save(comment);
article.setCommentCount(article.getCommentCount() + 1);
articleRepository.save(article);
}
}
Анализ проблем параллелизма
Процесс этой простой функции комментирования можно увидеть из реализации кода только что: когда пользователь инициирует запрос комментария, класс сущности соответствующей статьи находится в базе данных.Article
, а затем создайте соответствующий класс объекта комментария в соответствии с информацией о статье.Comment
, и вставить в базу, затем увеличить количество комментариев к статье, а затем обновить исправленную статью в базу.Весь процесс выглядит следующим образом.
В этом процессе есть проблема: когда несколько пользователей одновременно оставляют комментарии, они одновременно переходят на шаг 1, чтобы получить статью, затем вставляют соответствующий комментарий и, наконец, обновляют количество комментариев на шаге 3 в базе данных. Просто потому, что они являются статьями одновременно, значение их article.commentcount одинаково, тогда значение Article.commentCount + 1, сохраненное на шаге 3, такое же, тогда количество комментариев, которое должно быть +3, должно быть добавлено только 1.
Давайте попробуем это с кодом тестового примера
@RunWith(SpringRunner.class)
@SpringBootTest(classes = LockAndTransactionApplication.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class CommentControllerTests {
@Autowired
private TestRestTemplate testRestTemplate;
@Test
public void concurrentComment() {
String url = "http://localhost:9090/comment";
for (int i = 0; i < 100; i++) {
int finalI = i;
new Thread(() -> {
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("articleId", "1");
params.add("content", "测试内容" + finalI);
String result = testRestTemplate.postForObject(url, params, String.class);
}).start();
}
}
}
Здесь мы открываем 100 тем и одновременно отправляем запросы на комментарии, а соответствующий идентификатор статьи равен 1.
Перед отправкой запроса данные базы данных
select * from article
select count(*) comment_count from comment
После отправки запроса данные базы данных
select * from article
select count(*) comment_count from comment
Очевидно, что значение comment_count в таблице article не равно 100. Это значение не обязательно равно 14 на моей картинке, но оно должно быть не больше 100, а количество таблиц комментариев должно быть равно 100.
Это показывает проблемы параллелизма, упомянутые в начале статьи.Эта проблема на самом деле очень распространена.Пока существует система, подобная вышеупомянутой функции комментариев, вы должны быть осторожны, чтобы избежать этой проблемы.
Ниже приведен пример, показывающий, как предотвратить параллельные проблемы с данными с помощью пессимистической блокировки и оптимистической блокировки. В то же время дано решение SQL и собственное решение JPA. Решение SQL можно использовать в «любой системе», даже без языка ограничений, в то время как решение JPA очень быстрое.Если вы тоже используете JPA, вы можете просто использовать оптимистическую или пессимистическую блокировку. Наконец, мы сравним некоторые различия между оптимистичными и пессимистичными блокировками в зависимости от бизнеса.
Пессимистические блокировки решают проблемы параллелизма
Пессимистические блокировки, как следует из названия, пессимистичны в отношении того, что данные, которыми они оперируют, будут управляться другими потоками, поэтому они должны монополизировать сами данные, что можно понимать как «исключительные блокировки». в явеsynchronized
иReentrantLock
Ожидающие блокировки — это пессимистичные блокировки, а также блокировки таблиц, строк и чтения-записи в базе данных.
Использование SQL для решения проблем параллелизма
Блокировка строки предназначена для блокировки этой строки данных при работе с данными.Другие потоки должны ожидать чтения и записи, но другие потоки по-прежнему могут обрабатывать другие данные в той же таблице. Просто добавьте sql, который нужно запросить послеfor update
, вы можете заблокировать строку запроса,В частности, следует отметить, что условие запроса должно быть столбцом индекса, если это не индекс, он станет блокировкой таблицы, блокируя всю таблицу.
Теперь измените его на основе исходного кода, сначала вArticleRepository
Добавьте ручной метод запроса sql.
public interface ArticleRepository extends CrudRepository<Article, Long> {
@Query(value = "select * from article a where a.id = :id for update", nativeQuery = true)
Optional<Article> findArticleForUpdate(Long id);
}
тогда поставьCommentService
Метод запроса, используемый в оригиналеfindById
Изменить на наш пользовательский метод
public class CommentService {
...
public void postComment(Long articleId, String content) {
// Optional<Article> articleOptional = articleRepository.findById(articleId);
Optional<Article> articleOptional = articleRepository.findArticleForUpdate(articleId);
...
}
}
Итак, мы узналиArticle
,Никакой другой поток не может получить модификацию, пока мы не зафиксируем ее в транзакции., что гарантирует, что только один поток может обрабатывать соответствующие данные одновременно.
Теперь проверьте это с помощью тестового примера,article.comment_count
должно быть 100.
Использование JPA для решения проблем параллелизма блокировки строк
За только что упомянутое увеличение задней части sqlfor update
, JPA имеет более элегантный способ, то есть@Lock
Annotation, параметру этой аннотации можно передать желаемый уровень блокировки.
сейчас наArticleRepository
Добавьте метод блокировки JPA вLockModeType.PESSIMISTIC_WRITE
Параметр является блокировкой строки.
public interface ArticleRepository extends CrudRepository<Article, Long> {
...
@Lock(value = LockModeType.PESSIMISTIC_WRITE)
@Query("select a from Article a where a.id = :id")
Optional<Article> findArticleWithPessimisticLock(Long id);
}
так жеCommentService
Измените метод запроса наfindArticleWithPessimisticLock()
, а затем протестируйте тестовый пример, проблем с параллелизмом не будет. И посмотрите на информацию о печати консоли в это время и обнаружите, что фактически запрошенный sql все еще добавленfor update
, но JPA добавила его для нас.
Оптимистическая блокировка решает проблемы параллелизма
Оптимистическая блокировка, как следует из названия, очень оптимистична. Она думает, что ресурсы, полученные ею самой, не будут управляться другими потоками, поэтому она не блокируется. Она предназначена только для того, чтобы судить о том, были ли изменены данные при вставке в базу данных. . Таким образом, пессимистическая блокировка ограничивает другие потоки, а оптимистическая блокировка ограничивает себя.Хотя его имя имеет блокировку, на самом деле она не заблокирована.Это только для того, чтобы определить, как действовать в конечной операции.
Оптимистичная блокировка обычно является механизмом номера версии или алгоритм CAS
Используйте SQL для достижения номера версии для решения проблем параллелизма
Механизм номера версии заключается в том, чтобы добавить поле в базу данных в качестве номера версии, например, мы добавляем версию поля. Тогда получите этоArticle
Это принесет номер версии, например, полученная версия 1, а затем выArticle
Одна операция прохода, после завершения операции ее необходимо вставить в базу данных. Обнаружил упс, что произошло в базе данныхArticle
Версия 2, которая отличается от версии в моей руке, указывая на то, что версия в моей рукеArticle
Если он не актуален, он не может быть помещен в базу данных. Это позволяет избежать проблемы конфликта данных во время параллелизма.
Итак, теперь мы добавляем версию поля в таблицу статей.
статья статья таблица
поле | тип | Примечание |
---|---|---|
version | INT DEFAULT 0 | номер версии |
Затем соответствующий класс сущности также добавляет поле версии
@Data
@Entity
public class Article {
...
private Long version;
}
затем вArticleRepository
Добавьте метод обновления. Обратите внимание, что это метод обновления, который отличается от добавления метода запроса при использовании пессимистических блокировок.
public interface ArticleRepository extends CrudRepository<Article, Long> {
@Modifying
@Query(value = "update article set comment_count = :commentCount, version = version + 1 where id = :id and version = :version", nativeQuery = true)
int updateArticleWithVersion(Long id, Long commentCount, Long version);
}
Вы можете увидеть обновление, где есть критерий, который определяет версию и устанавливает версию = версия + 1. Это гарантирует, что данные обновляются только тогда, когда обновляются номер версии базы данных и номер версии класса сущностей.
затем вCommentService
Немного измените код.
// CommentService
public void postComment(Long articleId, String content) {
Optional<Article> articleOptional = articleRepository.findById(articleId);
...
int count = articleRepository.updateArticleWithVersion(article.getId(), article.getCommentCount() + 1, article.getVersion());
if (count == 0) {
throw new RuntimeException("服务器繁忙,更新数据失败");
}
// articleRepository.save(article);
}
Первый дляArticle
Метод запроса требует только обычногоfindById()
Метод работает без блокировок.
затем обновитеArticle
использовать только что добавленныйupdateArticleWithVersion()
метод. Вы можете видеть, что этот метод имеет возвращаемое значение. Это возвращаемое значение представляет количество обновленных строк базы данных. Если значение равно 0, это означает, что нет строк, удовлетворяющих условиям, которые можно обновить.
После этого нам решать, что с ним делать.Вот прямой откат, spring поможет нам откатить предыдущие операции с данными и отменить все операции на этот раз, чтобы обеспечить согласованность данных.
Теперь используйте тестовый пример для проверки
select * from article
select count(*) comment_count from comment
смотри сейчасArticle
comment_count в иComment
Число уже не 100.Но два значения должны быть одинаковыми. Потому что, когда мы имели дело с этим только что, еслиArticle
Если данные таблицы конфликтуют, то они не будут обновлены в базе данных.В это время выдается исключение, чтобы заставить транзакцию откатиться, чтобы гарантировать отсутствие обновленияArticle
когдаComment
Не вставляется, и проблема данных не однородна.
Этот метод прямого отката имеет плохой пользовательский опыт.Article
Когда количество обновлений равно 0, он попытается снова запросить информацию из базы данных и снова изменить ее, попытаться снова обновить данные и, если это не удастся, снова запросить, пока она не будет обновлена. Конечно, это не будет операция, такая как беспроводная петля.Будет установлена онлайн-линия.Например, если невозможно запросить, изменить и обновить петлю три раза, в это время будет выдано исключение .
Используйте JPA для реализации версии сейчас, чтобы решить проблемы параллелизма.
В JPA есть способ реализовать пессимистичные блокировки, и, естественно, есть и оптимистичные блокировки.Теперь мы используем методы, поставляемые с JPA, для реализации оптимистичных блокировок.
первый вArticle
Добавить в поле версии класса сущности@Version
Аннотация, давайте посмотрим на аннотации исходного кода в аннотациях, и мы видим, что некоторые из них написаны:
The following types are supported for version properties: int, Integer, short, Short, long, Long, java.sql.Timestamp.
В примечании говорится, что тип номера версии поддерживает три основных типа данных: int, short, long и их классы-оболочки и Timestamp, сейчас мы используем тип Long.
@Data
@Entity
public class Article {
...
@Version
private Long version;
}
Тогда просто нужноCommentService
Просто измените процесс комментирования в этом обратно на бизнес-код, который «вызовет проблемы параллелизма» в начале. Это показывает, что эта оптимистичная реализация блокировки JPA не является навязчивой.
// CommentService
public void postComment(Long articleId, String content) {
Optional<Article> articleOptional = articleRepository.findById(articleId);
...
article.setCommentCount(article.getCommentCount() + 1);
articleRepository.save(article);
}
Как и прежде, используйте тестовые примеры, чтобы проверить, можно ли предотвратить проблемы параллелизма.
select * from article
select count(*) comment_count from comment
такой жеArticle
comment_count в иComment
Число тоже не 100, но эти два числа определенно одинаковы. Взгляните на консоль IDEA, и вы обнаружите, что система выдаетObjectOptimisticLockingFailureException
исключение.
Это похоже на нашу собственную реализацию оптимистической блокировки только что: если данные не будут успешно обновлены, будет выдано исключение, и данные будут отброшены для обеспечения согласованности данных. Если вы хотите реализовать процесс повторной попытки, вы можете захватитьObjectOptimisticLockingFailureException
Для этого исключения обычно используются AOP + пользовательские аннотации для реализации глобального общего механизма повторных попыток. Это должно быть расширено в соответствии с конкретной бизнес-ситуацией. Если вы хотите узнать больше, вы можете найти решение самостоятельно.
Сравнение пессимистической блокировки и оптимистичной блокировки
Пессимистичные блокировки подходят для сценариев с большим количеством операций записи и меньшим количеством операций чтения.. Поскольку поток будет монополизировать этот ресурс при его использовании, в примере этой статьи это статья с определенным идентификатором.Если есть большое количество операций комментирования, целесообразно использовать пессимистическую блокировку.В противном случае, если пользователь просто просматривает статью и не имеет комментариев. Использование пессимистической блокировки часто приводит к блокировке, что увеличивает потребление ресурсов для блокировки и разблокировки.
Оптимистичная блокировка подходит для сценариев, где пишут меньше и Подробнее.由于乐观锁在发生冲突的时候会回滚或者重试,如果写的请求量很大的话,就经常发生冲突,经常的回滚和重试,这样对系统资源消耗也是非常大。
Таким образом, пессимистическая блокировка и оптимистичная блокировка не являются абсолютно хорошими или плохими., вы должны решить, какой метод использовать в сочетании с конкретной деловой ситуацией. Кроме того, это также упоминается в руководстве по разработке Alibaba:
Если вероятность каждого конфликта доступа меньше 20%, рекомендуется оптимистическая блокировка, в противном случае используется пессимистическая блокировка. Оптимистичные попытки блокировки Число должно быть не менее 3 раз.
Alibaba рекомендует использовать значение вероятности конфликта 20% в качестве разделительной линии для принятия решения об использовании оптимистичных и пессимистичных блокировок.Хотя это значение не является абсолютным, оно также является хорошим эталоном, как подытожили большие шишки Alibaba.
Оригинальный адрес:Spring Boot+SQL/JPA борется с пессимистичной блокировкой и оптимистичной блокировкой