PHP项目Symfony form与悲观锁

wen PHP项目 1

Symfony Form 与悲观锁的实现方案

悲观锁的基本概念

悲观锁假设并发冲突经常发生,在操作数据时直接锁定记录,防止其他事务修改。

PHP项目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,
        ]);
    }
}

使用建议

  1. 选择合适的锁模式

    • PESSIMISTIC_READ: 防止写入,允许读取
    • PESSIMISTIC_WRITE: 完全锁定,防止读写
  2. 事务管理:确保在事务内使用悲观锁

  3. 超时设置:设置合理的锁等待超时时间

  4. 监控与日志:记录锁等待和死锁信息

  5. 应用层锁:配合 Symfony Lock 组件使用双重保障

# config/packages/lock.yaml
framework:
    lock:
        product_edit: 'semaphore'
        product_purchase: 'redis://localhost:6379'

这种实现确保了在高并发场景下数据的一致性,同时提供了足够的灵活性来处理不同的业务需求。

抱歉,评论功能暂时关闭!