Pretendo apresentar um código com uma lógica de negócio simples que funciona bem na perspectiva do negócio mas que quebra miseravelmente em ambientes minimamente concorrentes. Para isso, vou demonstrar através de testes de integração como identificar o Race Condition no código e principalmente como resolvê-lo através de mecanismos de locking e Isolation Level do seu banco de dados;
- Por que estudamos esse assunto mais a fundo?
- 1.1. Bootcamp Seniors
- 1.2. Handora: Curso CRUD e Atualizações Concorrentes com JPA/Hibernate - by Jordi Silva
- 1.3. A importância do treino: o que eu levei alguns bons anos para aprender o Jordi levou 6 meses
- 1.4. Indo além, validando a teoria na prática
- Entendendo o problema
- 2.1. Nossa lógica de negócio
- 2.2. Cobrindo a lógica com testes de integração
- 2.3. Nossa lógica funciona em ambientes concorrentes?
- 2.4. Deu xabu!! 😱😱 Race Condition do tipo Lost Update
- 2.5. Entendendo o que aconteceu
- 2.6. Cuidado com anti-pattern Read-Modify-Write
- Tentando resolver o problema com soluções ingênuas
- 3.1. Basta um
@Transactional
e pronto! [NaiveATMService
] - 3.2. Concorrência? Moleza, a gente resolve com
synchronized
no método! [NaiveJavaSynchronizedATMService
]
- 3.1. Basta um
- O que o banco de dados pode fazer por mim? O poder do ACID!
- 4.1. Banco é mais do que um "repositório de dados"
- 4.2. Pessimistic Locking: Mais simples que isso não dá! [
PessimisticLockingATMService
] - 4.3. Optimistic Locking: Deixa a aplicação cuidar disso para mim [
OptimisticLockingATMService
] - 4.4. Constraints CHECK(): as vezes tudo se resolve com uma constraint [
AtomicUpdateWithCheckConstraintATMService
] - 4.5. Atomic Update: levando a lógica pro banco
- 4.5.1. Escreva antes de validar:
AtomicUpdateWithPostValidationATMService
- 4.5.2. ...se possivel evite round-trips:
AtomicUpdateWithReturningClauseATMService
- 4.5.3. ...se possivel faz tudo no banco:
AtomicUpdateWithWhereClauseATMService
- 4.5.1. Escreva antes de validar:
- 4.6. Isolation Levels: indo além do básico com concorrência e banco de dados
- 4.6.1. Conhecendo os níveis de isolamento:
READ_UNCOMMITTED
: eu faço leituras sujas;READ_COMMITTED
: eu só leio o que foi comitado;REPEATABLE_READ
: o que eu li no começo é verdade até o fim;SERIALIZABLE
: eu enxergo como se tudo acontecesse de forma sequencial
- 4.6.2. Qual o default?
READ_UNCOMMITTED
para maioria dos bancos - 4.6.3. REPEATABLE_READ: respeitando o ciclo Read-Mofify-Write [
RepeatableReadIsolationLevelATMService
] - 4.6.4. SERIALIZABLE: as vezes ser o mais restrito eh o caminho [
SerializableIsolationLevelATMService
] - 4.6.5. Nada vem de graça. Tradeoffs que chama né?
- 4.6.1. Conhecendo os níveis de isolamento:
- Que mané banco de dados
- 5.1. Usa um lock distribuído com Redis ou Mongo, ou mete numa fila com Kafka ou RabbitMQ. Ou, como gosto de pensar: "DevOps pra que né?"
- 5.2. O banco relacional é mais do que um repositório de dados
- 5.3. Distributed Lock: Postgres e seu Advisory Lock
- Concluindo
- O banco de dados relacional é uma engine completa de concorrência
- Você não precisa aprender tudo isso, mas pode ser bastante útil em sistemas criticos e com alta-concorrência
Alguns artigos: