mysql之乐观锁
- 乐观锁
- 什么是乐观锁
- 乐观锁解决购买商品时候的并发问题
- 展示购买商品时候的并发问题
- 乐观锁解决上面的并发问题
- 但是上面的乐观锁有一个问题,就是如果我的商品的数量不是1,而是100的时候,此时就会造成jack这个线程还是购买不到
- 怎么解决这个问题呢,就是增加一个for循环
- 乐观锁的隔离级别问题
乐观锁
什么是乐观锁
什么是乐观锁呢,就是给数据库的数据表添加一个字段version,在更新数据库记录是将version加1,从而在修改数据时通过检查版本号是否改变判断出当前更新基于的查询是否已经是过时的版本。不是太好理解,这里我么就借助实例来讲解乐观锁(下面实例讲解的过程中,商品的数量就可以直接相当于字段version了,所以就没有在加version字段了)。
乐观锁解决购买商品时候的并发问题
什么是购买商品的并发问题呢?
就是有一个商品的数量是1,然后有两个用户a和b都来购买这个商品,两个用户也就是两个线程,然后呢用户购买一个商品有下面的几步
1)查询该商品的数量,
2)进行库存判断,
3)往订单表里面进行插入这个数据,
4)然后把该商品的数量减1,
假设两个线程都执行到了第2步结束,然后呢,这样就会造成一个问题,就是此时的商品数量是1,然后两个用户都执行到了第2步结束,a用户先完成第3和第4步,此时的商品的数量就是0了,然后b用户此时就有问题了,因为此时的商品已经是0了,那么b用户还买个毛的商品,这就是并发问题,就像下面的图片
展示购买商品时候的并发问题
首先数据库里面的数据如下,此时只有鸡腿这个商品的数量是1(因为商品的数量就可以直接相当于乐观锁里面的字段version了,所以就没有在加version字段了)
所有文件的路径如下
Web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" id="WebApp_ID" version="2.5">
<display-name>myweb</display-name>
<servlet>
<description></description>
<display-name>ProductServlet</display-name>
<servlet-name>ProductServlet</servlet-name>
<servlet-class>com.itcast.servlet.ProductServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>ProductServlet</servlet-name>
<url-pattern>/product</url-pattern>
</servlet-mapping>
</web-app>
Product
package com.itcast.domain;
@Data
public class Product {
private Integer pid;
private Double price;
private String pname;
private Integer pnum;
}
ProductServlet代码如下
此时我们访问两个url路径http://localhost:8080/myweb/product?uname=jack 和http://localhost:8080/myweb/product?uname=tom,首先tom这个线程先查询数据库里面的鸡腿的商品数量是1,然后呢因为下面的Thread.sleep(5000),tom这个线程就睡眠了,然后jack这个线程在查询数据库里面的鸡腿的数量还是1,所以先打印
,然后因为下面的Thread.sleep(5000),然后jack这个线程也睡眠了,然后tom这个线程就先购买成功了,所以会打印
,此时的数据库里面的鸡腿的数量就会从1变成0,然后呢jack这个线程在购买成功,所以会打印
,但是此时数据库里面的鸡腿的数据就会从0变成-1,这是不合理的,所以jack这个线程算没有购买成功的
package com.itcast.servlet;
import java.io.IOException;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.handlers.BeanHandler;
import com.itcast.domain.Product;
public class ProductServlet extends HttpServlet {
public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
Connection con=null;
String user = request.getParameter("uname");
try {
Class.forName("com.mysql.jdbc.Driver");
con= (Connection) DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "root", "root");
QueryRunner qr = new QueryRunner();
//开启事务
con.setAutoCommit(false);
String select_product = "SELECT * FROM product";
Product product =qr.query(con, select_product, new BeanHandler<Product>(Product.class));
System.out.println(user+"查询商品数量是"+product.getPnum());
Thread.sleep(5000);
if(product.getPnum()>0){
String delete_product = "update product set pnum=pnum-1 where 1";
int row = qr.update(con,delete_product);
if(row>0){
System.out.println(user+"购买成功了");
}else{
System.out.println(user+"没有购买成功");
}
}else{
System.out.println(user+"没有购买成功");
}
con.commit();
} catch (Exception e) {
try {
System.out.println(user+"没有购买成功");
con.rollback();
} catch (SQLException e1) {
e1.printStackTrace();
}
}
}
public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doGet(request, response);
}
}
然后我们看到数据库里面的鸡腿的商品数量变成-1了,这也就说明后一个jack这个用户并没有购买成功,因为jack购买的是0到-1的那个商品,那个商品是不存在的
乐观锁解决上面的并发问题
看下面的代码
数据库里面的数据如下,此时可以看到数据库里面的鸡腿这个商品的数量只有1个了(因为商品的数量就可以直接相当于乐观锁里面的字段version了,所以就没有在加version字段了
所有文件的路径如下
Web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" id="WebApp_ID" version="2.5">
<display-name>myweb</display-name>
<servlet>
<description></description>
<display-name>ProductServlet</display-name>
<servlet-name>ProductServlet</servlet-name>
<servlet-class>com.itcast.servlet.ProductServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>ProductServlet</servlet-name>
<url-pattern>/product</url-pattern>
</servlet-mapping>
</web-app>
Product
package com.itcast.domain;
@Data
public class Product {
private Integer pid;
private Double price;
private String pname;
private Integer pnum;
}
ProductServlet代码如下,
此时访问两个url路径http://localhost:8080/myweb/product?uname=jack 和http://localhost:8080/myweb/product?uname=tom的时候,然后tom线程先查询商品的数量,然后获取这个商品的数量值1,然后呢我们在tom这个线程里面把这个1赋值给一个old_num变量,然后呢因为Thread.sleep(5000),tom这个线程休眠了,然后呢jack获取到这个商品的数量值,此时获取的也是1,然后呢我们在jack这个线程里面把这个1赋值给一个old_num变量,然后jack这个线程也因为Thread.sleep(5000)也休眠了,所以就会有红框里面的结果打印
,然后肯定是tom这个线程先进行修改商品的数量,就是执行下面的蓝色代码里面的sql语句,此时的sql语句执行成功,所以鸡腿的个数就会从1变成0,然后就会打印这个红框
,然后呢jack这个线程在进行执行下面的蓝色代码,此时呢sql语句就会是这样的"update product set pnum=pnum-1 where 1 and pnum =1",但是此时数据库里面的鸡腿的个数是0,所以此时这个蓝色代码是执行不成功的,所以下面的红色代码里面的row的结果就是0,然后此时jack就没有购买成功了,所以此时就会打印红框
package com.itcast.servlet;
import java.io.IOException;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.handlers.BeanHandler;
import com.itcast.domain.Product;
public class ProductServlet extends HttpServlet {
public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
Connection con=null;
String user = request.getParameter("uname");
try {
Class.forName("com.mysql.jdbc.Driver");
con= (Connection) DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "root", "root");
QueryRunner qr = new QueryRunner();
//开启事务
con.setAutoCommit(false);
String select_product = "SELECT * FROM product";
Product product =qr.query(con, select_product, new BeanHandler<Product>(Product.class));
int old_num = product.getPnum();
System.out.println(user+"查询商品数量是"+product.getPnum());
Thread.sleep(5000);
if(product.getPnum()>0){
// 蓝色代码注释开始
String delete_product = "update product set pnum=pnum-1 where 1 and pnum ="+old_num;
// 蓝色代码注释结束
// 红色代码注释开始
int row = qr.update(con,delete_product);
// 红色代码注释结束
if(row>0){
System.out.println(user+"购买成功了");
}else{
System.out.println(user+"没有购买成功");
}
}else{
System.out.println(user+"没有购买成功");
}
con.commit();
} catch (Exception e) {
try {
System.out.println(user+"没有购买成功");
con.rollback();
} catch (SQLException e1) {
e1.printStackTrace();
}
}
}
public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doGet(request, response);
}
}
结束之后,鸡腿商品的数量就变成了0
但是上面的乐观锁有一个问题,就是如果我的商品的数量不是1,而是100的时候,此时就会造成jack这个线程还是购买不到
比如下面的代码
数据库里面的数据如下,此时可以看到数据库里面的鸡腿这个商品的数量有100个(因为商品的数量就可以直接相当于乐观锁里面的字段version了,所以就没有在加version字段了)
所有文件的路径如下
Web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" id="WebApp_ID" version="2.5">
<display-name>myweb</display-name>
<servlet>
<description></description>
<display-name>ProductServlet</display-name>
<servlet-name>ProductServlet</servlet-name>
<servlet-class>com.itcast.servlet.ProductServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>ProductServlet</servlet-name>
<url-pattern>/product</url-pattern>
</servlet-mapping>
</web-app>
Product
package com.itcast.domain;
@Data
public class Product {
private Integer pid;
private Double price;
private String pname;
private Integer pnum;
}
ProductServlet代码如下
此时当访问两个url路径http://localhost:8080/myweb/product?uname=jack 和http://localhost:8080/myweb/product?uname=tom的时候,然后tom线程先查询商品的数量,然后获取这个商品的数量值100,然后呢我们在tom这个线程里面把这个100赋值给一个old_num变量,然后呢因为Thread.sleep(5000),tom这个线程休眠了,然后呢jack获取到这个商品的数量值,此时获取的也是100,然后呢我们在jack这个线程里面把这个100赋值给一个old_num变量,然后jack这个线程也因为Thread.sleep(5000)也休眠了,所以就会有红框里面的结果打印,
,然后肯定是tom这个线程先进行修改商品的数量,就是执行下面的蓝色代码里面的sql语句,此时的sql语句执行成功,所以鸡腿的个数就会从100变成99,然后就会打印这个红框
,然后呢jack这个线程在进行执行下面的蓝色代码,此时呢sql语句就会是这样的"update product set pnum=pnum-1 where 1 and pnum =100",但是此时数据库里面的鸡腿的个数是99,所以此时这个蓝色代码是执行不成功的,所以下面的红色代码里面的row的结果就是0,然后此时jack就没有购买成功了,所以此时就会打印红框
package com.itcast.servlet;
import java.io.IOException;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.handlers.BeanHandler;
import com.itcast.domain.Product;
public class ProductServlet extends HttpServlet {
public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
Connection con=null;
String user = request.getParameter("uname");
try {
Class.forName("com.mysql.jdbc.Driver");
con= (Connection) DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "root", "root");
QueryRunner qr = new QueryRunner();
//开启事务
con.setAutoCommit(false);
String select_product = "SELECT * FROM product";
Product product =qr.query(con, select_product, new BeanHandler<Product>(Product.class));
int old_num = product.getPnum();
System.out.println(user+"查询商品数量是"+product.getPnum());
Thread.sleep(5000);
if(product.getPnum()>0){
// 蓝色代码注释开始
String delete_product = "update product set pnum=pnum-1 where 1 and pnum ="+old_num;
// 蓝色代码注释结束
// 红色代码注释开始
int row = qr.update(con,delete_product);
// 红色代码注释结束
if(row>0){
System.out.println(user+"购买成功了");
}else{
System.out.println(user+"没有购买成功");
}
}else{
System.out.println(user+"没有购买成功");
}
con.commit();
} catch (Exception e) {
try {
System.out.println(user+"没有购买成功");
con.rollback();
} catch (SQLException e1) {
e1.printStackTrace();
}
}
}
public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doGet(request, response);
}
}
怎么解决这个问题呢,就是增加一个for循环
就让上面的jack线程当在购买失败的时候(注意只有购买失败的时候,才会在此进行for循环,如果购买成功了,直接break跳出循环)在进行重新查询商品的数量,然后修改商品的数量,视频里面说循环次数3次就够了,为什么循环三次就够了,这个我觉得自己定义,如果觉得3次不够,那就10次,这个自己决定
比如下面的代码
数据库里面的数据如下,此时可以看到数据库里面的鸡腿这个商品的数量有100个(因为商品的数量就可以直接相当于乐观锁里面的字段version了,所以就没有在加version字段了)
所有文件的路径如下
Web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" id="WebApp_ID" version="2.5">
<display-name>myweb</display-name>
<servlet>
<description></description>
<display-name>ProductServlet</display-name>
<servlet-name>ProductServlet</servlet-name>
<servlet-class>com.itcast.servlet.ProductServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>ProductServlet</servlet-name>
<url-pattern>/product</url-pattern>
</servlet-mapping>
</web-app>
Product
package com.itcast.domain;
@Data
public class Product {
private Integer pid;
private Double price;
private String pname;
private Integer pnum;
}
ProductServlet代码如下,此时当访问两个url路径http://localhost:8080/myweb/product?uname=jack 和http://localhost:8080/myweb/product?uname=tom的时候,首先进行第一次for循环,此时的i值是0,然后tom线程先查询商品的数量,然后获取这个商品的数量值100,然后呢我们在tom这个线程里面把这个100赋值给一个old_num变量,然后呢因为Thread.sleep(5000),tom这个线程休眠了,然后呢jack获取到这个商品的数量值,此时获取的也是100,然后呢我们在jack这个线程里面把这个100赋值给一个old_num变量,然后jack这个线程也因为Thread.sleep(5000)也休眠了,所以就会有红框里面的结果打印,
,然后肯定是tom这个线程先进行修改商品的数量,就是执行下面的蓝色代码里面的sql语句,此时的sql语句执行成功,所以鸡腿的个数就会从100变成99,然后就会打印这个红框
,然后呢jack这个线程在进行执行下面的蓝色代码,此时呢sql语句就会是这样的"update product set pnum=pnum-1 where 1 and pnum =100",但是此时数据库里面的鸡腿的个数是99,所以此时这个蓝色代码是执行不成功的,所以下面的红色代码里面的row的结果就是0,然后此时jack就没有购买成功了,所以此时就会打印红框
,然后呢jack这个线程就会进行第二次for循环,此时的i值是1,然后就会打印结果
,然后jack线程机会进行第三次循环,此时的i值是2,然后就是打印结果
,为什么jack第二次和第三次的获取的商品的数量都是100呢,继续往下看
package com.itcast.servlet;
import java.io.IOException;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.handlers.BeanHandler;
import com.itcast.domain.Product;
public class ProductServlet extends HttpServlet {
public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
Connection con=null;
String user = request.getParameter("uname");
try {
Class.forName("com.mysql.jdbc.Driver");
con= (Connection) DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "root", "root");
QueryRunner qr = new QueryRunner();
//开启事务
con.setAutoCommit(false);
for(int i=0;i<3;i++){
String select_product = "SELECT * FROM product";
Product product =qr.query(con, select_product, new BeanHandler<Product>(Product.class));
int old_num = product.getPnum();
System.out.println(user+"查询商品数量是"+product.getPnum());
Thread.sleep(5000);
if(product.getPnum()>0){
// 蓝色代码注释开始
String delete_product = "update product set pnum=pnum-1 where 1 and pnum ="+old_num;
// 蓝色代码注释结束
// 红色代码注释开始
int row = qr.update(con,delete_product);
// 红色代码注释结束
if(row>0){
System.out.println(user+"购买成功了");
break;
}else{
System.out.println(user+"没有购买成功");
continue;
}
}else{
System.out.println(user+"没有购买成功");
}
}
con.commit();
} catch (Exception e) {
try {
System.out.println(user+"没有购买成功");
con.rollback();
} catch (SQLException e1) {
e1.printStackTrace();
}
}
}
public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doGet(request, response);
}
}
乐观锁的隔离级别问题
为什么商品的jack线程在第二次循环获取商品的数据的时候还是100,按理说tom这个线程在提交事务之后,数据库里面的商品的数据应该是99啊,为什么jack线程第二次循环的时候获取的还是100呢,这是因为mysql的事务的隔离级别默认是REPEATABLE-READ,就是因为隔离级别是REPEATABLE-READ,所以就会避免不可重复读,而避免了不可以重复读就会导致jack线程在第二次循环的时候获取的值还是100
不懂去看不可重复读的知识点,链接在这里:
我们把事务的隔离级别永久设置成read commited之后,然后在执行上面的代码就好了
比如下面的代码
数据库里面的数据如下,此时可以看到数据库里面的鸡腿这个商品的数量有100个(因为商品的数量就可以直接相当于乐观锁里面的字段version了,所以就没有在加version字段了)
所有文件的路径如下
Web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" id="WebApp_ID" version="2.5">
<display-name>myweb</display-name>
<servlet>
<description></description>
<display-name>ProductServlet</display-name>
<servlet-name>ProductServlet</servlet-name>
<servlet-class>com.itcast.servlet.ProductServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>ProductServlet</servlet-name>
<url-pattern>/product</url-pattern>
</servlet-mapping>
</web-app>
Product
package com.itcast.domain;
@Data
public class Product {
private Integer pid;
private Double price;
private String pname;
private Integer pnum;
}
ProductServlet代码如下,此时当访问两个url路径http://localhost:8080/myweb/product?uname=jack 和http://localhost:8080/myweb/product?uname=tom的时候,首先进行第一次for循环,此时的i值是0,然后tom线程先查询商品的数量,然后获取这个商品的数量值100,然后呢我们在tom这个线程里面把这个100赋值给一个old_num变量,然后呢因为Thread.sleep(5000),tom这个线程休眠了,然后呢jack获取到这个商品的数量值,此时获取的也是100,然后呢我们在jack这个线程里面把这个100赋值给一个old_num变量,然后jack这个线程也因为Thread.sleep(5000)也休眠了,所以就会有红框里面的结果打印,
,然后肯定是tom这个线程先进行修改商品的数量,就是执行下面的蓝色代码里面的sql语句,此时的sql语句执行成功,所以鸡腿的个数就会从100变成99,然后就会打印这个红框
,然后呢jack这个线程在进行执行下面的蓝色代码,此时呢sql语句就会是这样的"update product set pnum=pnum-1 where 1 and pnum =100",但是此时数据库里面的鸡腿的个数是99,所以此时这个蓝色代码是执行不成功的,所以下面的红色代码里面的row的结果就是0,然后此时jack就没有购买成功了,所以此时就会打印红框
,然后呢jack这个线程就会进行第二次for循环,此时的i值是1,然后就会再一次查询商品的数量,然后在修改商品的数量,所以此时的打印结果就是
package com.itcast.servlet;
import java.io.IOException;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.handlers.BeanHandler;
import com.itcast.domain.Product;
public class ProductServlet extends HttpServlet {
public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
Connection con=null;
String user = request.getParameter("uname");
try {
Class.forName("com.mysql.jdbc.Driver");
con= (Connection) DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "root", "root");
QueryRunner qr = new QueryRunner();
//开启事务
con.setAutoCommit(false);
for(int i=0;i<3;i++){
String select_product = "SELECT * FROM product";
Product product =qr.query(con, select_product, new BeanHandler<Product>(Product.class));
int old_num = product.getPnum();
System.out.println(user+"查询商品数量是"+product.getPnum());
Thread.sleep(5000);
if(product.getPnum()>0){
// 蓝色代码注释开始
String delete_product = "update product set pnum=pnum-1 where 1 and pnum ="+old_num;
// 蓝色代码注释结束
// 红色代码注释开始
int row = qr.update(con,delete_product);
// 红色代码注释结束
if(row>0){
System.out.println(user+"购买成功了");
break;
}else{
System.out.println(user+"没有购买成功");
continue;
}
}else{
System.out.println(user+"没有购买成功");
}
}
con.commit();
} catch (Exception e) {
try {
System.out.println(user+"没有购买成功");
con.rollback();
} catch (SQLException e1) {
e1.printStackTrace();
}
}
}
public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doGet(request, response);
}
}
结束之后,鸡腿商品的数量就变成了98了,这样乐观锁就完成了。哈哈搞定☺
能看到这里的同学,就帮忙右上角点个赞吧,Thanks♪(・ω・)ノ