首页 > 数据库 >java中如何保证数据库数据的一致性

java中如何保证数据库数据的一致性

时间:2023-09-23 09:55:04浏览次数:52  
标签:事务 old productDao nums 数据库 add 一致性 java public

本文使用的数据库是mysql

一、不考虑并发时的写法

假设现在有一张t_product表,我们先只考虑单实例部署时的情况

CREATE TABLE t_product(
id INT PRIMARY KEY,
NAME VARCHAR(50)
,nums INT
);
INSERT INTO t_product(id,NAME,nums) VALUES(1,'aa',1);

我们现在有一个加库存的接口会被并发调用,如果保证并发调用时存到数据库里的结果是对的。

先看下不考虑并发时接口一般怎么写。

service层

@Service
public class ProductService {

    @Autowired
    ProductDao productDao;

    public void add(){
        //查询旧的库存值
        int old = productDao.getProductNumById(1);
        //库存值加1更新到数据库
        productDao.add(old+1,1);
    }
}

	<select id="getProductNumById" resultType="int">
        SELECT nums FROM t_product WHERE id=#{id}
    </select>

    <update id="add">
        UPDATE t_product set nums=#{nums} WHERE id=#{id}
    </update>

这种写法中如果add方法被并发调用,线程t1先查询出old,然后线程t2已经把数据库更新了,这时t1接着执行就会造成数据不一致,可以用下面的代码模拟下这种场景

public class MyTest {

    public static void main(String[] args) throws IOException, InterruptedException {
        HttpClientBuilder builder = HttpClientBuilder.create();
        for (int i = 0; i < 10; i++) {
            Thread t = new Thread(new Runnable() {
                @Override
                public void run() {
                    CloseableHttpClient httpClient = builder.build();
                    HttpGet get = new HttpGet("http://localhost:8090/demo/product/add");
                    get.setHeader("Accept","*/*");
                    get.setHeader("Content-Type","application/json");
                    CloseableHttpResponse response = null;
                    try {
                        response = httpClient.execute(get);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                    System.out.println(response);
                }
            });
            t.start();
        }

    }
}

上边这段代码模拟了10个人同时调用add接口时的场景,理论上数据库中nums的值应该增加10,实际上因为并发更新的问题nums增加的值不是10

二、解决办法

2.1使用mysql的行锁

使用mysql提供的行锁,t1执行add时把表记录锁住让其他线程阻塞,

把getProductNumById的sql改成

SELECT nums FROM t_product WHERE id=#{id} for update;

前提时是add方法要使用事务,只有add方法有事务才能保证在事务提交前上边sql获取的行锁不会释放,如果add方法没事务那查询旧值的sql执行完后事务被自动提交获取的锁旧又被释放了,起不到阻塞别的线程的作用,所以测试时记得给add方法加上@Transactional 注解。

2.2 java代码中使用synchronize

这种方法的思路是把add方法变成同步方法,同时只有一个线程再执行,然后就可以保证查询旧值和更新新值这两个操作和起来是原子性的。可能会写出这样的代码,但这样的代码真的可以保证一致性吗?

	@Transactional
    public synchronized void add(){
        int old = productDao.getProductNumById(1);//(1)
        productDao.add(old+1,1);
    }

要注意数据库的事务隔离级别要是读已提交,mysql默认的隔离级别是可重复读,假设线程t1和t2都已经开启事务,t1先进入add方法执行,当它更新完数据库、释放锁、提交事务后t2再开始执行,那么t2能查询到nums的最新值吗,可重复读级别下是不能的,所以我们要修改这个方法使用的事务的隔离级别,所以我们对代码做出改进,

   //指定这个方法中使用的事务隔离级别是读已提交
    @Transactional(isolation = Isolation.READ_COMMITTED)
    public synchronized void add(){
        int old = productDao.getProductNumById(1);
        productDao.add(old+1,1);
    }

那么现在的代码看着好像没问题了,再思考两个问题,事务是什么时候开启的?锁又是什么时候加上去的?

spring的事务控制是基于aop实现的,那么开启事务和加锁是这样的顺序:

(1)aop开启事务

(2)开始执行目标方法加锁

(3)目标方法执行完成解锁

(4)aop提交事务

所以在解锁的时候事务还没有提交,别的线程这个时候去执行add方法查询到的还是旧值,所以为了保证数据一致性我们要实现先加锁再开启事务,那只能把add方法中查询和更新的逻辑抽取成另一个事务方法,这样才可以实现加锁后开启事务。

有因为同一个service中方法互相调用不走aop所以需要抽取到另一个service中

@Service
public class ProductService {

    @Autowired
    AddProductService addProductService;

    //因为这个方法现在不需要读取数据了所以可以不开启事务
    public synchronized void add(){
        addProductService.addNums();
    }
}

@Service
public class AddProductService {
    @Autowired
    ProductDao productDao;

    //指定这个方法中使用的事务隔离级别是读已提交
    @Transactional(isolation = Isolation.READ_COMMITTED)
    public  void addNums(){
        int old = productDao.getProductNumById(1);
        productDao.add(old+1,1);
    }
}

2.3 更新时使用旧值作为条件

UPDATE t_product set nums=#{nums} WHERE id=#{id} and nums=#{oldNums}

这种方式利用了cas的思想在更新的时候检查旧值对不对,如果不对update语句就不会执行成功,然后可以在代码里重新查询值

public class ProductService {

    @Autowired
    ProductDao productDao;

    //指定这个方法中使用的事务隔离级别是读已提交
    @Transactional(isolation = Isolation.READ_COMMITTED)
    public void add(){
        int old = productDao.getProductNumById(1);
        int res = productDao.add(old + 1, 1, old);
        while(res==0) {
            //进循环说明update语句执行的时候nums已经被更新过了
            old = productDao.getProductNumById(1);
            res = productDao.add(old + 1, 1, old);
        }
    }
}

因为要在service中查询旧值,所以也要注意事务的隔离级别,但因为这种方式不涉及在代码中加锁所以不需要单独抽象servcie去开启新事物。

那么这种方式可行的原理是什么呢?

mysql中的update语句会给某一行数据加上行锁,事务t1先执行update会给数据行加行锁,

那事务t2要执行update时也会先给数据加行锁,这时因为数据行已经有行锁就会被阻塞住直到t1提交,这时t2才能执行update,因为nums的值已经被改了所以update不会更新数据影响行数是0,所以代码里就会进入while循环重新查询。

2.4 使用分布式锁

标签:事务,old,productDao,nums,数据库,add,一致性,java,public
From: https://www.cnblogs.com/chengxuxiaoyuan/p/17723927.html

相关文章

  • foreach批量插入数据库
    批量插入数据库错误报告如下Cause:java.sql.SQLSyntaxErrorException:YouhaveanerrorinyourSQLsyntax;checkthemanualthatcorrespondstoyourMySQLserverversionfortherightsyntaxtousenear'insertintot_checkgroup_checkitem(checkitem_......
  • JavaScript 终于原生支持数组分组了!
    在日常开发中,很多时候需要对数组进行分组,每次都要手写一个分组函数,或者使用lodash的groupBy函数。好消息是,JavaScript现在正在引入全新的分组方法:Object.groupBy和Map.groupBy,以后再也不需要手写分组函数了,目前最新版本的Chrome(117)已经支持了这两个方法!以前的数组分组假设有一......
  • 20230922学习总结java连接HBASE
    连接条件:1、所有虚拟机上运行hadoop集群、运行zookeeper进程守护 2、向项目中导入即hbase安装目录下的conf文件夹中的两个文件 3、添加maven依赖<dependencies><dependency><groupId>org.apache.hbase</groupId><artifactId>hbase-server</ar......
  • JavaScript 终于原生支持数组分组了!
    在日常开发中,很多时候需要对数组进行分组,每次都要手写一个分组函数,或者使用lodash的groupBy函数。好消息是,JavaScript现在正在引入全新的分组方法:Object.groupBy和Map.groupBy,以后再也不需要手写分组函数了,目前最新版本的Chrome(117)已经支持了这两个方法!以前的数组分组假设有一......
  • Java笔记(细碎小知识点)1
    1.Dos命令:dir:打出当前目录结构;md:创建文件夹;cd+文件夹地址:跳转到当前目录下的对应文件夹;cd..:跳转到上一目录;rd+文件夹:删除文件夹中东西;del+文件(或“*.文件”类型这样的正则表达式):删除文件或这类文件;cd/:跳转到盘符;javac+文件名.java:编译java文件,生成class文件;java+文件名:运行jaca......
  • 数据库设计步骤
    首先我们需要建立一个ER模型 建立E-R模型的步骤1.定义实体集; 2.定义联系集 3.确定实体集与联系集的属性,4.标识出实体集与联系集的主码,形成完整的ER图 注意:实体在E-R图画成矩形联系在E-R图里画成菱形 两实体联系比值在连线上标识属性在E-R图里画成椭圆关......
  • 无涯教程-JavaScript - LARGE函数
    描述LARGE函数返回数据集中的第k个最大值。您可以使用此功能根据其相对地位选择一个值。语法LARGE(array,k)争论Argument描述Required/OptionalArrayThearrayorrangeofdataforwhichyouwanttodeterminethek-thlargestvalue.RequiredKTheposition......
  • 无涯教程-JavaScript - LOGEST函数
    描述在回归分析中,计算适合您数据的指数曲线,并返回描述该曲线的值数组。由于此函数返回值数组,因此必须将其作为数组公式输入。语法LOGEST(known_y's,[known_x's],[const],[stats])争论Argument描述Required/OptionalKnown_y's在关系y=b*m^x中,您已经知......
  • java任意视频转MP4
    Java任意视频转MP4目录Java任意视频转MP4场景FFmpeg介绍环境准备下载FFmpegwindows下载linux下载windows版解压使用测试案例视频测试案例代码Linux版解压使用场景在做视频上传功能时候,用户可能上传不同类型的视频文件,导致需要特定播放器才能播放,为了解决视频格式统一问题需要......
  • openGauss学习笔记-77 openGauss 数据库管理-内存优化表MOT管理-内存表特性-MOT特性及
    openGauss学习笔记-77openGauss数据库管理-内存优化表MOT管理-内存表特性-MOT特性及价值本节介绍了openGauss内存优化表(Memory-OptimizedTable,MOT)的特性及价值。77MOT特性及价值MOT在高性能(查询和事务延迟)、高可扩展性(吞吐量和并发量)以及高资源利用率(某些程度上节约成本)方面......