-- Table structure for tb_stock
-- ----------------------------
DROP TABLE IF EXISTS `tb_stock`;
CREATE TABLE `tb_stock` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`product_code` varchar(20) NOT NULL,
`warehouse` varchar(20) NOT NULL,
`count` int(11) NOT NULL,
`version` int(11) NOT NULL,
PRIMARY KEY (`id`),
KEY `idx_pc` (`product_code`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of tb_stock
-- ----------------------------
INSERT INTO `tb_stock` VALUES ('1', '1001', '北京仓', '0', '5009');
INSERT INTO `tb_stock` VALUES ('2', '1001', '上海仓', '4999', '0');
INSERT INTO `tb_stock` VALUES ('3', '1002', '深圳仓', '4997', '0');
INSERT INTO `tb_stock` VALUES ('4', '1002', '上海仓', '5000', '0');
接下来我们来看下代码
第一基础版本
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.11</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.atguigu</groupId>
<artifactId>distributed-lock</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>distributed-lock</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.3.4</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
application.properties
server.port=10010
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://192.168.175.129:3306/distributed-lock
spring.datasource.username=root
spring.datasource.password=123456
StockMapper
package com.atguigu.distributed.lock.Mapper;
import com.atguigu.distributed.lock.pojo.Stock;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
public interface StockMapper extends BaseMapper<Stock> {
}
Stock
package com.atguigu.distributed.lock.pojo;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
@Data
@TableName("tb_stock")
public class Stock {
private Long id;
private String productCode;
private String warehouse;
private Integer count ;
}
StockService
import com.atguigu.distributed.lock.Mapper.StockMapper;
import com.atguigu.distributed.lock.pojo.Stock;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Service;
import java.util.concurrent.locks.ReentrantLock;
@Service
public class StockService {
//private Stock stock = new Stock();
@Autowired
private StockMapper stockMapper;
private ReentrantLock lock = new ReentrantLock();
public void deduct(){
lock.lock();
try {
Stock stock = stockMapper.selectOne(new QueryWrapper<Stock>().eq("product_code","1001"));
if(stock!=null && stock.getCount() > 0){
stock.setCount(stock.getCount()-1);
stockMapper.updateById(stock);
}
} finally {
lock.unlock();
}
}
}
StockController
import com.atguigu.distributed.lock.service.StockService;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@MapperScan("com.atguigu.distributed.lock.Mapper")
public class StockController {
@Autowired
private StockService stockService;
@GetMapping("stock/deduct")
public String deduct(){
this.stockService.deduct();
return "hello stock deduct!!";
}
}
上述代码就允许起来了,但是上述代码可能是存在超卖的情况出现的,在多线程的情况下,如何解决了。
第一种解决办法使用一个sql语句搞定
update tb_stock set count = count -1 where product_code= '1001' and count > 0;
通过一个sql语句来解决上面的问题,在跟新的时候首先判断count是否库存大于0,如果库存大于0,才进行更新
接下来我们来跟新代码 StockMapper
package com.atguigu.distributed.lock.Mapper;
import com.atguigu.distributed.lock.pojo.Stock;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Update;
public interface StockMapper extends BaseMapper<Stock> {
@Update("update tb_stock set count=count - #{count} where product_code=#{productCode} and count > 0")
int updatestock (@Param("productCode") String productCode, @Param("count")Integer count);
}
StockService
package com.atguigu.distributed.lock.service;
import com.atguigu.distributed.lock.Mapper.StockMapper;
import com.atguigu.distributed.lock.pojo.Stock;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Service;
import java.util.concurrent.locks.ReentrantLock;
@Service
public class StockService {
//private Stock stock = new Stock();
@Autowired
private StockMapper stockMapper;
private ReentrantLock lock = new ReentrantLock();
public void deduct2(){
try {
stockMapper.updatestock("1001",1);
} finally {
}
}
}
经过验证之后,就能够解决上面在多实例、多线程下超卖的问题,但是使用单独的一个sql语句,还存在一定的问题 就是整个语句到底是表锁,还是行锁的问题,要提高整个线程的并发性
我们可以采用下面的方式进行验证
线程1:执行更新1001的库存操作,在更新操作未提交事务之前,在开启一个线程2,更新1002操作,默认情况下,可以看到在1001的更新未提交事务之前,线程2更新1002的操作是阻塞的,默认情况下是表锁,把整个表锁定的。
线程1:执行更新1001的库存操作,在更新操作未提交事务之前,在开启一个线程2,更新1002操作,默认情况下,可以看到在1001的更新未提交事务之前,线程2更新1002的操作是阻塞的,默认情况下是表锁,把整个表锁定的。
默认情况是表锁,要提高效率,变成行锁,如何解决了
只需要个给更新和查询的字段创建索引就可以了,所以只需要给tb_stock表中的更新字段product_code创建一个索引就可以了;
-- ----------------------------
DROP TABLE IF EXISTS `tb_stock`;
CREATE TABLE `tb_stock` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`product_code` varchar(20) NOT NULL,
`warehouse` varchar(20) NOT NULL,
`count` int(11) NOT NULL,
`version` int(11) NOT NULL,
PRIMARY KEY (`id`),
KEY `idx_pc` (`product_code`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;
在创建索引成功之后接下来我们来看下面的操作;
线程1更新编号为1001的记录,线程2同时更新编号为1002的记录,变成行锁之后,两个不同行记录的操作是互不影响的。
线程2
第二个条件,查询的条件必须是具体的值,不能是模糊的值,如下面使用!=或者like表达式,就会导致整个表都会被锁 如下
线程2操作的时候,会一直被阻塞,直到线程1事务提交,是一个表锁
所以在数据库的表的更新操作中一定要注意表锁和行锁,这里一定要非常的注意。
第二种解决方案:select for update 悲观锁
我们首先做一个直观的操作,让大家知道这个效果
在线程1上面开启一个 for update 操作
在线程1上面开启一个 for update 操作,更新商品编号为1001的操作,如果线程1没有提交commit操作,线程2开启一个任务执行也更新编号为1001的操作,操作同一个行记录,因为线程1没有执行commit操作,这个时候线程2是无法操作成功的,必须等待线程1的操作完成提交后,线程2的更新的操作才能完成,for update是能够保证行锁操作的,这就是原理
使用for update悲观锁实现并发操作,需要注意下面的几个事情
悲观锁: 比较悲观,一旦加锁,自身增删查改,其他线程无法任何操作,不能与其他锁并存。加锁方式 for update for update 来解决并发重复查询,保证每次只有只能一个线程执行查询
for update 来解决并发重复查询,保证每次只有只能一个线程执行查询
** 观锁,正如其名,具有强烈的独占和排他特性。上来就锁住,把事情考虑的比较悲观,它是采用数据库机制实现的,数据库被锁之后其它用户将无法查看,直到提交或者回滚,锁释放之后才可查看。所以悲观锁具有其霸道性。
简单说其悲观锁的功能就是,锁住读取的记录,防止其它事物读取和更新这些记录,而其他事物则会一直堵塞,知道这个事物结束。**
使用悲观锁要注意下面的两个条件: 1.悲观锁:假设每一次拿数据,都有认为会被修改,所以给数据库的行或表上锁。要注意for update要用在索引上,不然会锁表。类似上面的一个sql语句,需要叫上更新和查询的字段要叫上索引; 2.就是一定要在事务中执行
转载自博客:cloud.tencent.com/developer/a…
接下来我们通过代码来实现
StockMapper
package com.atguigu.distributed.lock.Mapper;
import com.atguigu.distributed.lock.pojo.Stock;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
import java.util.List;
public interface StockMapper extends BaseMapper<Stock> {
@Update("update tb_stock set count=count - #{count} where product_code=#{productCode} and count > 0")
int updatestock (@Param("productCode") String productCode, @Param("count")Integer count);
** @Select("select * from tb_stock where product_code=#{productCode} for update")
List<Stock> querystock(String productCode);**
}
业务层级的代码如下所示:
package com.atguigu.distributed.lock.service;
import com.atguigu.distributed.lock.Mapper.StockMapper;
import com.atguigu.distributed.lock.pojo.Stock;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.concurrent.locks.ReentrantLock;
@Service
public class StockService {
//private Stock stock = new Stock();
@Autowired
private StockMapper stockMapper;
@Transactional
public void deduct2(){
try {
List<Stock> stockList = stockMapper.querystock("1001");
if (stockList!= null){
Stock stock = stockList.get(0);
if(stock != null && stock.getCount() > 0){
stock.setCount(stock.getCount()-1);
stockMapper.updateById(stock);
}
}
} finally {
}
}
}
第四章方案,乐观锁实现高并发
乐观锁:就是很乐观,每次去拿数据的时候都认为别人不会修改。更新时如果version变化了,更新不会成功。
update status set name='nike',version=(version+1) where id=1 and version=version;
接下来我们来看代码的实现
import com.atguigu.distributed.lock.Mapper.StockMapper;
import com.atguigu.distributed.lock.pojo.Stock;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.concurrent.locks.ReentrantLock;
@Service
public class StockService {
//private Stock stock = new Stock();
@Autowired
private StockMapper stockMapper;
private ReentrantLock lock = new ReentrantLock();
//乐观锁实现
public void deduct22w() throws InterruptedException {
try {
// 1. 查询库存
List<Stock> stockList = stockMapper.selectList(new QueryWrapper<Stock>().eq("product_code", "1001"));
//2. 这里选择第一个库存
if(stockList !=null && stockList.size() > 0){
Stock stock = stockList.get(0);
// 3.判断库存是否充足,然后扣减库存
if(stock !=null && stock.getCount() > 0){
stock.setCount(stock.getCount() -1);
Integer version = stock.getVersion();
// 4.更新乐观锁的vesion
stock.setVersion(version+1);
// 依据version 更新数据库
int update = stockMapper.update(stock, new UpdateWrapper<Stock>().eq("id", stock.getId()).eq("version", version));
// update表示更新的行数记录,如果大于1 ,说明更新成功
if(update == 0) {
//说明更新失败,需要自旋,重新获取锁,再次进行跟新
Thread.sleep(20);
this.deduct22w();
}else
{
System.out.println("获取锁成功,完成库存扣减");
}
}
}
} finally {
}
}
}
1.乐观锁的并发效率在jemter压测情况下只有200,乐观锁的并发效率还没有悲观锁的高。 2.乐观锁底层就是依赖数据库的锁资源,千万不能再代码层面添加@Transactional,乐观锁不需要 @Transactional注解
2.乐观锁底层就是依赖数据库的锁资源,千万不能再代码层面添加@Transactional,乐观锁不需要 @Transactional注解,如果使用了该注解会报下面的错误
3.乐观锁在主从架构下存在问题,version在主节点上面已经跟新了,但是因为网络原因,在从节点上面还是原来的version的值,所以会导致更新成功,被重复跟新。