首页 > 数据库 >一次分布式锁与数据库事务的纠缠

一次分布式锁与数据库事务的纠缠

时间:2022-11-17 15:16:06浏览次数:48  
标签:事务 transaction READ 数据库 纠缠 开启 线程 public 分布式

 

  有一个需要保证并发安全性的方法,考虑到锁的粒度与分布式要求,选择了基于 Redis 的分布式锁。

  需要保证并发安全性的原因是该方法会并发操作数据库某表中的数据。大概过程是这样的:

@Transactional(isolation = Isolation.REPEATABLE_READ)
public class TestService {
public void domain() throws Exception {
String lockKey = "xxxxxx";
redisLockUtils.tryLock(lockKey, 3);
try {
// 查询数据库中的一条数据
// 修改数据回写
} catch (Exception e) {
e.printStackTrace();
} finally {
redisLockUtils.unlock(lockKey);
}
}
}

  乍一看没什么问题,操作共享资源(数据库中的数据)的代码段通过分布式锁实现互斥,保证了共享资源的线程安全。

  但是经过测试,却出现了并发问题,多个线程并发操作同一条数据时,总有一些线程的操作会丢失。

  但经过分析日志,每条线程都竞争到了锁,并正常执行了加锁部分的代码段。

  代码的互斥通过锁机制保证了,再三确认自认为没什么漏洞,只能将思路放到数据库。

  该方法在 service 中,service 类上有 @Transactional(isolation = Isolation.REPEATABLE_READ) 注解,该类中的方法都被嵌套了声明式事务,且事务隔离级别为可重复读。

  回顾一下事务的隔离级别:

隔离级别脏读不可重复读幻象读
READ UNCOMMITTED 允许 允许 允许
READ COMMITED 不允许 允许 允许
REPEATABLE READ 不允许 不允许 允许
SERIALIZABLE 不允许 不允许 不允许

  

  REPEATABLE READ 为可重复读(也是 mysql 的默认隔离级别),即在一个事务范围内相同的查询会返回相同的数据。

  即在事务刚刚开始时,数据库就对事务中已涉及的数据进行了快照,在此期间,其它事务对这些数据的修改不会影响该事务。因为在该事务内读的都是快照。

  那么问题就找到了:

  多个线程等待在竞争锁的 redisLockUtils.tryLock(lockKey, 3) 这行代码时,每个线程其实都早已开启了自己的事务。因此当持有锁的线程对数据进行了修改并释放锁,等待的线程竞争到锁后,竞争到锁的线程并看不到上一个持有锁的线程对数据库做的修改。

  因为该线程所在的事务在进入方法时就早已开启,不管别的线程如何改动数据,该线程都是读取的事务开启时的快照。

  那么解决方法就很明确了:

  1、关闭事务
  2、修改事务隔离级别为读已提交

  3、在加锁代码段中开启和提交事务,不要先开启事务再去竞争锁
  综合看方法 3 最适合当前场景,因为:

  1、对数据库的操作有多条,事务是肯定要开启的。
  2、修改隔离级别治标不治本,逻辑结构上还是有缺陷。先开启事务、再竞争锁,会有很多事务等待在竞争锁的代码上,给数据库增加很多没有必要的压力。

  那么实现下方案3。方案 3 的实现有两种方式:
  1、继续使用声明式事务,将事务相关的代码抽取出来封装为一个单独的方法

  2、在加锁代码中手动开启提交事务

  我选择使用更灵活的方案 2 ,手动操作事务的方法封装如下:

@Component
public class TransactionUtils {
    @Resource
    private DataSourceTransactionManager transactionManager;

    //开启事务,传入隔离级别
    public TransactionStatus begin(int isolationLevel) {
        DefaultTransactionDefinition def = new DefaultTransactionDefinition();
        // 事物隔离级别,开启新事务 TransactionDefinition.ISOLATION_READ_COMMITTED
        def.setIsolationLevel(isolationLevel);
        // 事务传播行为
        def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
        //默认事务
        TransactionStatus transaction = transactionManager.getTransaction(def);
        //将拿到的事务返回进去,才能提交。
        return transaction;
    }

    //提交事务
    public void commit(TransactionStatus transaction) {
        //提交事务
        transactionManager.commit(transaction);
    }

    //回滚事务
    public void rollback(TransactionStatus transaction) {
        transactionManager.rollback(transaction);
    }
}

  service 代码改为:

 public class TestService {
        public void domain() throws Exception {
            String lockKey = "xxxxxx";
            redisLockUtils.tryLock(lockKey, 3);
            try {
                // 查询数据库中的一条数据

                // 手动开启事务
                TransactionStatus transaction = transactionUtils.begin(TransactionDefinition.ISOLATION_READ_COMMITTED);
                try {
                    // 修改数据回写

                    // 手动提交事务
                    transactionUtils.commit(transaction);
                } catch (Exception e) {
                    // 手动回滚事务
                    transactionUtils.rollback(transaction);
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                redisLockUtils.unlock(lockKey);
            }
        }
    }

  在加锁的代码段内,需要事务的最小范围上开启事务。

  至此问题解决。

  但手动 开启/提交/回滚 事务时也需注意,因为大部分项目主流是使用声明式事务,使用手动事务会显得很突兀,也不利于代码风格的统一。

  此外,手工开启的事务与声明式事务的混用会打乱事务的传播行为(待证实,目前测试是这样的)。比如,内层的声明式事务是无法加入到外层的手工开启的事务的,这就会造成事务的加入变成了事务的嵌套,在不经意间就会造成数据库的死锁。

  但是,声明式事务的最小单位是方法,粒度很粗,单纯的为一个事务去封装一个方法,在语义上有时会比较别扭。

  因此在遇到类似情形时,还是具体问题具体分析,根据项目中事务嵌套情况的复杂度和个人对代码风格的要求来选择。

标签:事务,transaction,READ,数据库,纠缠,开启,线程,public,分布式
From: https://www.cnblogs.com/niuyourou/p/16897035.html

相关文章

  • phpmyadmin 数据库导出数据到excel(图文版)
    查询到想要的数据后,点击上方或下方的“导出”按钮格式选择“CSVforMSExcel” 如果快速导出的数据乱码,可以选择“导出方式”->“自定义-显示所有可用的选项”。......
  • 数据库平滑扩容方案剖析
    1.扩容方案剖析1.1扩容问题在项目初期,我们部署了三个数据库A、B、C,此时数据库的规模可以满足我们的业务需求。为了将数据做到平均分配,我们在Service服务层使用uid%3进......
  • Redis缓存中的数据和数据库不一致
    首先关于两者数据的一致性包含有两种情况:(1)缓存中有数据时,那数据库中的数据要和缓存中的数据相同;(2)缓存中没有数据时,数据库中的数据必须是最新的。如果不符合以上两种......
  • 使用存储过程备份数据库
    数据库备份脚本:CREATEPROCEDURE[dbo].[SP_BackupDB](@BackPathNVARCHAR(200),--备份路径,如:D:\Backup\@BackDbNameNVARCHAR(50),......
  • 分布式系统架构master-worker
    概述Master-Workers架构(粗译为主从架构)是分布式系统中常见的一种组织方式。面对分布式系统中一堆分离的机器资源,主从架构是一种最自然、直白的组织方式——就像一群人,有......
  • Command10,Access数据库
    我的按钮名为Command10,Access文档新建在当前目录下,代码如下PrivateSubCommand10_Click() DimcatAsADOX.Catalog Setcat=NewADOX.Cata......
  • 提示‘操作无法完成,应为文件已在SQL Server(MSSQLSERVER)中打开’,移动或删除数据库相
    移动或删除数据库相关文件时出现提示‘操作无法完成,应为文件已在SQLServer(MSSQLSERVER)中打开’ 解决方法:在开始菜单附近的搜索中搜索服务  找到SQLServer(......
  • 数据库查重
    查重语句select*fromteamwhereteamIdin(selectteamIdfromteamgroupbyteamIdhavingcount(teamId)>1)select*fromteamwhereID=2groupbyIDhavin......
  • 数据库表中常用的查询实验
    实验1练习1、请查询表DEPT中所有部门的情况。select*fromdept;练习2、查询表DEPT中的部门号、部门名称两个字段的所有信息。selectdeptno,dnamefromdept;练习3、请从......
  • SQL Server 高可用(always on)配置指南之数据库侦听器及高可用
    1.简介1、参考SQLServer高可用(alwayson)配置指南之域(AD)环境搭建  ​​https://blog.51cto.com/waringid/5851856​​完成域控服务器(DomainControl,以下简称DC)2、......