Symfony Form 与悲观锁的实现方案
悲观锁的基本概念
悲观锁假设并发冲突经常发生,在操作数据时直接锁定记录,防止其他事务修改。

Symfony Doctrine 悲观锁实现
use Doctrine\DBAL\LockMode;
use Doctrine\ORM\EntityManagerInterface;
class ProductController extends AbstractController
{
#[Route('/product/{id}/edit', name: 'product_edit')]
public function edit(int $id, EntityManagerInterface $em, Request $request): Response
{
// 锁定模式
$modes = [
'PESSIMISTIC_READ' => LockMode::PESSIMISTIC_READ,
'PESSIMISTIC_WRITE' => LockMode::PESSIMISTIC_WRITE,
'OPTIMISTIC' => LockMode::OPTIMISTIC,
];
$dbalConnection = $em->getConnection();
$dbalConnection->beginTransaction();
try {
// 获取实体并加悲观写锁
$product = $em->find(Product::class, $id, LockMode::PESSIMISTIC_WRITE);
if (!$product) {
throw $this->createNotFoundException('Product not found');
}
$form = $this->createForm(ProductType::class, $product);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$em->flush();
$dbalConnection->commit();
return $this->redirectToRoute('product_list');
}
return $this->render('product/edit.html.twig', [
'form' => $form->createView(),
'product' => $product,
]);
} catch (\Exception $e) {
$dbalConnection->rollBack();
throw $e;
}
}
}
使用 Repository 封装悲观锁逻辑
namespace App\Repository;
use Doctrine\DBAL\LockMode;
use Doctrine\ORM\EntityRepository;
class ProductRepository extends EntityRepository
{
/**
* 获取记录并加悲观写锁
*/
public function findWithPessimisticWriteLock(int $id): ?Product
{
$entityManager = $this->getEntityManager();
$dql = "SELECT p FROM App\Entity\Product p WHERE p.id = :id";
$query = $entityManager->createQuery($dql)
->setParameter('id', $id);
// 原生SQL方式获取悲观锁
$query->setLockMode(LockMode::PESSIMISTIC_WRITE);
return $query->getOneOrNullResult();
}
/**
* 使用QueryBuilder获取悲观锁
*/
public function findWithLockQueryBuilder(int $id): ?Product
{
$entityManager = $this->getEntityManager();
$qb = $this->createQueryBuilder('p')
->where('p.id = :id')
->setParameter('id', $id);
$query = $qb->getQuery();
$query->setLockMode(LockMode::PESSIMISTIC_WRITE);
return $query->getOneOrNullResult();
}
/**
* 原生SQL实现悲观锁(MySQL示例)
*/
public function findWithNativeLock(int $id): ?array
{
$conn = $this->getEntityManager()->getConnection();
$sql = "SELECT * FROM product WHERE id = :id FOR UPDATE";
$stmt = $conn->prepare($sql);
$stmt->execute(['id' => $id]);
return $stmt->fetch();
}
}
完整的控制器示例(含超时处理)
namespace App\Controller;
use App\Entity\Product;
use App\Form\ProductType;
use Doctrine\DBAL\LockMode;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Lock\LockFactory;
class ProductController extends AbstractController
{
private EntityManagerInterface $entityManager;
private LockFactory $lockFactory;
public function __construct(
EntityManagerInterface $entityManager,
LockFactory $lockFactory
) {
$this->entityManager = $entityManager;
$this->lockFactory = $lockFactory;
}
#[Route('/product/{id}/edit', name: 'product_edit')]
public function edit(int $id, Request $request): Response
{
// 使用 Symfony Lock 组件实现应用层锁
$lock = $this->lockFactory->createLock('product_edit_' . $id, 30);
if (!$lock->acquire()) {
throw $this->createAccessDeniedException('该产品正在被其他人编辑');
}
try {
$this->entityManager->beginTransaction();
// 数据库悲观锁
$product = $this->entityManager->find(
Product::class,
$id,
LockMode::PESSIMISTIC_WRITE
);
if (!$product) {
throw $this->createNotFoundException('Product not found');
}
$form = $this->createForm(ProductType::class, $product);
$form->handleRequest($request);
if ($form->isSubmitted()) {
// 检查表单之前的版本号(用于乐观锁检测)
$originalStock = $product->getStock();
if ($form->isValid()) {
// 业务逻辑检查
if ($product->getStock() !== $originalStock) {
// 库存变化时的特殊处理
$this->addFlash('warning', '库存已被其他用户修改');
}
$this->entityManager->flush();
$this->entityManager->commit();
return $this->redirectToRoute('product_list');
}
}
return $this->render('product/edit.html.twig', [
'form' => $form->createView(),
'product' => $product,
]);
} catch (\Exception $e) {
$this->entityManager->rollback();
$this->addFlash('error', '编辑失败:' . $e->getMessage());
return $this->redirectToRoute('product_edit', ['id' => $id]);
} finally {
$lock->release();
}
}
#[Route('/product/{id}/purchase', name: 'product_purchase', methods: ['POST'])]
public function purchase(int $id, Request $request): Response
{
// 高并发场景下的锁定处理
$lockKey = 'product_purchase_' . $id;
$lock = $this->lockFactory->createLock($lockKey, 10);
if (!$lock->acquire()) {
throw $this->createNotFoundException('系统繁忙,请稍后重试');
}
try {
$this->entityManager->beginTransaction();
// 使用 DQL 进行锁定
$dql = "SELECT p FROM App\Entity\Product p
WHERE p.id = :id
AND p.stock > 0";
$query = $this->entityManager->createQuery($dql)
->setParameter('id', $id)
->setLockMode(LockMode::PESSIMISTIC_WRITE);
$product = $query->getOneOrNullResult();
if (!$product) {
throw new \RuntimeException('商品已售罄');
}
// 更新库存
$product->setStock($product->getStock() - 1);
$this->entityManager->flush();
$this->entityManager->commit();
return $this->json(['success' => true, 'stock' => $product->getStock()]);
} catch (\Exception $e) {
$this->entityManager->rollback();
return $this->json(['success' => false, 'message' => $e->getMessage()], 500);
} finally {
$lock->release();
}
}
}
悲观锁的超时与重试机制
namespace App\Service;
use Doctrine\DBAL\Connection;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Lock\LockFactory;
class PessimisticLockService
{
private EntityManagerInterface $entityManager;
private LockFactory $lockFactory;
private int $maxRetries = 3;
private int $retryDelay = 100; // 毫秒
public function __construct(
EntityManagerInterface $entityManager,
LockFactory $lockFactory
) {
$this->entityManager = $entityManager;
$this->lockFactory = $lockFactory;
}
/**
* 带重试机制的悲观锁操作
*/
public function executeWithLock(int $entityId, callable $callback, int $timeout = 5): mixed
{
$attempts = 0;
$lastException = null;
while ($attempts < $this->maxRetries) {
try {
$this->entityManager->beginTransaction();
// 设置锁超时
$connection = $this->entityManager->getConnection();
$connection->executeStatement('SET innodb_lock_wait_timeout = ?', [$timeout]);
// 获取实体并锁定
$entity = $this->entityManager->find(
'App\Entity\Product',
$entityId,
\Doctrine\DBAL\LockMode::PESSIMISTIC_WRITE
);
if (!$entity) {
throw new \RuntimeException('Entity not found');
}
// 执行回调
$result = $callback($entity);
$this->entityManager->flush();
$this->entityManager->commit();
return $result;
} catch (\Doctrine\DBAL\Exception\LockWaitTimeoutException $e) {
$this->entityManager->rollback();
$lastException = $e;
$attempts++;
if ($attempts < $this->maxRetries) {
usleep($this->retryDelay * 1000);
}
} catch (\Exception $e) {
$this->entityManager->rollback();
throw $e;
}
}
throw new \RuntimeException(
'Failed to acquire lock after ' . $this->maxRetries . ' attempts',
0,
$lastException
);
}
}
Form 中的版本号处理(辅助乐观锁)
namespace App\Form;
use App\Entity\Product;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class ProductType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('name')
->add('price')
->add('stock')
->add('_version', HiddenType::class, [
'mapped' => false,
'data' => $options['current_version'] ?? 0,
'attr' => ['class' => 'version-field']
])
;
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => Product::class,
'current_version' => 0,
'csrf_protection' => true,
]);
}
}
使用建议
-
选择合适的锁模式:
PESSIMISTIC_READ: 防止写入,允许读取PESSIMISTIC_WRITE: 完全锁定,防止读写
-
事务管理:确保在事务内使用悲观锁
-
超时设置:设置合理的锁等待超时时间
-
监控与日志:记录锁等待和死锁信息
-
应用层锁:配合 Symfony Lock 组件使用双重保障
# config/packages/lock.yaml
framework:
lock:
product_edit: 'semaphore'
product_purchase: 'redis://localhost:6379'
这种实现确保了在高并发场景下数据的一致性,同时提供了足够的灵活性来处理不同的业务需求。