# db-lock-demo **Repository Path**: su-miaomiao/db-lock-demo ## Basic Information - **Project Name**: db-lock-demo - **Description**: No description available - **Primary Language**: Unknown - **License**: Apache-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2020-02-13 - **Last Updated**: 2020-12-19 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # db-lock-demo 控制MySQL并发操作的各种锁实现 1. [数据库更新时的乐观锁](#jump1) 2. [unique索引防止数据重复插入](#jump2) # Demo1: optimistic-lock-db-demo > 乐观锁防止(查询->更新)产生脏数据 Java+MySQL实现乐观锁 在高并发下,经常需要处理SELECT之后,在业务层处理逻辑,再执行UPDATE的情况。 若两个连接并发查询同一条数据,然后在执行一些逻辑判断或业务操作后,执行UPDATE,可能出现与预期不相符的结果。 在不使用悲观锁与复杂SQL的前提下,可以使用乐观锁处理该问题,同时兼顾性能。 --- ## 场景模拟 假设一张表,三个字段: |字段名|说明| |-----|----| |id|商品ID| |goodsName|商品名称| |goodsStock|库存| 里面有一条数据: id = 1, goodsName = 并发商品, goodsStock = 5 用户每购买一件商品,商品的库存减1。 现在有若干个用户在深夜12:00抢购“并发商品”,每个人限购1件。 在高并发情况下,会遇到一种问题: 假设数据表中有一条记录为:id = 1, goodsName = 并发商品, goodsStock = 5 A用户与B用户两个连接并发查询id=1商品的库存,都执行下列SQL: ```SQL SELECT goodsStock FROM table WHERE id=1; -- 查询库存 ``` A先执行,得到id=1的goodsStock是5,之后在程序里做了一些逻辑判断或业务操作后执行SQL: ```java goodsStock = goodsStock - 1 if (goodsStock > 0) ↓ ``` ```SQL UPDATE table SET goodsStock = 4 WHERE id=1; -- 扣减库存 ``` 在A做判断且没有update之前,B也执行了查询SQL,发现goodsStock是5,之后它也会执行SQL: ```java goodsStock = goodsStock - 1 if (goodsStock > 0) ↓ ``` ```SQL UPDATE table SET goodsStock = 4 WHERE id=1; -- 扣减库存 ``` 这样A、B两个用户都买到了“并发商品”,然而库存却从5变成了4,这样库存计算错误。 如果有成百上千个用户并发操作,会造成“虚卖”,即实际库存已没,但是系统持续计算错误,仍然持续卖出。 --- 处理步骤如下: 1. 添加第3个字段version,int类型,default值为0。version值每次update时作加1处理。 |字段名|说明| |-----|----| |id|商品ID| |goodsName|商品名称| |goodsStock|库存| |version|数据版本号| 2. SELECT时同时获取version值(例如version为1、goodsStock为5)。 ```SQL SELECT goodsStock, version FROM table WHERE id=1; ``` 3. UPDATE时检查version值是否为第2步获取到的值。 ```SQL UPDATE table SET version=2, goodsStock=4 WHERE id=1 AND version=1; ``` - 如果UPDATE的记录数为1,则表示成功。 - 如果UPDATE的记录数为0,则表示已经被其他连接UPDATE过了,需作异常处理。 例如本例中使用JDBC的executeUpdate()方法获取UPDATE的记录数。 ```java int affectRows = sql1.executeUpdate(); ``` --- ## 快速开始 1. git clone 2. 使用eclipse导入项目 3. 将db目录下的表导入到自己的数据库中 4. 配置`DbUtil.java`,修改数据库连接参数 5. `Demo1.java`文件中的 6. 找到main方法,调节`preson`参数的值(代表有多少线程并发),切换buy、newBuy两个方法进行测试 |方法名|说明| |-----|----| |buy(currentIndex)|传统开发方法| |newBuy(currentIndex)|版本号控制并发| --- ## 测试结果 - 未使用并发控制方法时,计算错误 ![](https://www.webpro.ltd/blog1/wp-content/uploads/2020/02/oold_1_1.png) ![](https://www.webpro.ltd/blog1/wp-content/uploads/2020/02/oold_1_2.png) - 使用并发控制方法时,问题解决 ![](https://www.webpro.ltd/blog1/wp-content/uploads/2020/02/oold_2_1.png) ![](https://www.webpro.ltd/blog1/wp-content/uploads/2020/02/oold_2_2.png) [Top](#jump0) --- --- # Demo2: unique-prevent-insert-repeat ## 场景模拟 假设一张表,两个字段: |字段名|说明| |-----|----| |id|用户id| |username|用户名| 现在有新用户注册,假设注册的用户名为 test 一般情况下我们会先执行查询: ```SQL SELECT username from table where username=test; ``` 如果返回0行,也就是没有用户test存在时,执行插入 ```SQL INSERT INTO table (username) VALUES ('test'); ``` 假设此时有两个线程A、B,恰好同时注册test用户,那么: |线程名称|时间段α|时间段β| |-------|------|-------| |线程A->|查询数据库中test是否存在->不存在->|插入test用户到数据库| |线程B->|查询数据库中test是否存在->不存在->|插入test用户到数据库| 显然,两个重复的用户名在数据库中,违背了我们在写注册业务逻辑时username不重复的原则。 --- 处理步骤如下: 1. 设置username为unique索引 ```SQL ALTER TABLE table ADD unique(`username`) ``` 2. 在原来代码的基础上,执行update方法后,捕获`MySQLIntegrityConstraintViolationException`异常 ```java // for example try { // ... sql.executeUpdate(); // ... } catch (MySQLIntegrityConstraintViolationException e){ // 违反唯一约束原则 System.out.println("用户名已被注册"); } ``` ## 测试结果 - 未设置username为unique索引时 ![](https://www.webpro.ltd/blog1/wp-content/uploads/2020/02/demo2_1_1.png) ![](https://www.webpro.ltd/blog1/wp-content/uploads/2020/02/demo2_1_2.png) - 设置username为unique索引时 ![](https://www.webpro.ltd/blog1/wp-content/uploads/2020/02/demo2_2_1.png) ![](https://www.webpro.ltd/blog1/wp-content/uploads/2020/02/demo2_2_2.png) > 个人认为这种方法只适用于并发情况较小的注册或其他插入场景,如果在高并发插入防止数据重复的处理中,不宜使用该方法,因为这样将大量无效的SQL都压到了数据库,对数据库的性能会造成一定影响,最好在业务逻辑中先行处理。 [Top](#jump0) --- ---