目的:减库存的操作,模拟10个线程同时去操作。
方法:用nginx做反向代理,idea启动两个进程,用jmter同时去发10个请求,看看减库存会出现什么错误
框架:springboot+mybatis+mysql
1.添加依赖:
<?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.2.1.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.tuling</groupId>
<artifactId>distributedlock</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>distributedlock</name>
<description>Demo project for Spring Boot</description>
<repositories>
<repository>
<id>alimaven</id>
<name>Maven Aliyun Mirror</name>
<url>http://maven.aliyun.com/nexus/content/repositories/central/</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.34</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.curator/curator-recipes -->
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>5.0.0</version>
<exclusions>
<exclusion>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.5.8</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
2.Mapper层操作
package com.tuling.distributedlock.mapper;
import com.tuling.distributedlock.entity.Order;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Options;
public interface OrderMapper {
@Options(useGeneratedKeys = true,keyColumn = "id",keyProperty = "id")
@Insert(" insert into `order`(user_id,pid) values(#{userId},#{pid}) ")
int insert(Order order);
}
package com.tuling.distributedlock.mapper;
import com.tuling.distributedlock.entity.Product;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
public interface ProductMapper {
@Select(" select * from product where id=#{id} ")
Product getProduct(@Param("id") Integer id);
@Update(" update product set stock=stock-1 where id=#{id} ")
int deductStock(@Param("id") Integer id);
}
3.service层:
package com.tuling.distributedlock;
import com.tuling.distributedlock.entity.Order;
import com.tuling.distributedlock.entity.Product;
import com.tuling.distributedlock.mapper.OrderMapper;
import com.tuling.distributedlock.mapper.ProductMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
@Service
public class OrderService {
@Autowired
private ProductMapper productMapper;
@Autowired
private OrderMapper orderMapper;
@Transactional
public void reduceStock(Integer id){
// 1. 获取库存
Product product = productMapper.getProduct(id);
// 模拟耗时业务处理
sleep( 500); // 其他业务处理
if (product.getStock() <=0 ) {
throw new RuntimeException("out of stock");
}
// 2. 减库存
int i = productMapper.deductStock(id);
if (i==1){
Order order = new Order();
order.setUserId(UUID.randomUUID().toString());
order.setPid(id);
orderMapper.insert(order);
}else{
throw new RuntimeException("deduct stock fail, retry.");
}
}
/**
* 模拟耗时业务处理
* @param wait
*/
public void sleep(long wait){
try {
TimeUnit.MILLISECONDS.sleep( wait );
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
4.Controller层(这是已经加上分布式锁的代码):
package com.tuling.distributedlock;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.recipes.locks.InterProcessMutex;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
/***
* @Author 郭嘉 QQ:2790284115
* @Slogan 致敬大师,致敬未来的你
*/
@RestController
public class TestController {
@Autowired
private OrderService orderService;
@Value("${server.port}")
private String port;
@Autowired
CuratorFramework curatorFramework;
@PostMapping("/stock/deduct")
public Object reduceStock(Integer id) throws Exception {
InterProcessMutex interProcessMutex = new InterProcessMutex(curatorFramework, "/product_" + id);
try {
// ...
interProcessMutex.acquire();
orderService.reduceStock(id);
} catch (Exception e) {
if (e instanceof RuntimeException) {
throw e;
}
}finally {
interProcessMutex.release();
}
return "ok:" + port;
}
}
5.如何测呢?
添加一个线程组,模拟一秒钟发起10次访问
发起一个http请求,对ID为1的产品进行并发访问
添加一些监听,看看执行效果:
nginx配置:
部署两台项目,模拟分布式操作,一个8080端口,一个8081端口:
6.开始测:
启动nginx
启动8080和8081的两个服务
发起请求,看看效果,10线程去消费5个产品,竟然没有报错,肯定是有问题的。(注意:上面的代码已经加了分布式锁了,运行的话是不会出现这样的问题的。下面这个图模拟的是没有加分布式锁的情况)
这是加上分布式锁后的效果,此时数据的商品数量为0:
源码深入:
加锁的主逻辑在下图标注的这个类里,这个一会儿去看。先记得是在这个类里就行。
从这个方法进去看,加锁的主逻辑在这里面
下面这幅图是对上面这幅图代码的详细解释,主要分为3块来解释的。可重入锁
那么加锁是怎样实现的呢?主要从这个方法进入:
点进去这个方法,下面这个图标注的是这个方法的主要逻辑:
在由上图的这个方法进入(标记1):
点进去是这样的(这里创建的是容器节点且是临时顺序节点):
回到标记1那副图,创建好了节点之后,需要判断这个节点是不是最小的那个,这个逻辑在哪里判断呢,借用标记1的那副图:
这是上面那个方法点击进来的(看加锁,需要在while循环中看)标记3:
这个方法点进去(标记2)
这个方法其实是获取所有的子节点,并且进行排序,返回的其实已经是排好序的所有子节点了
点进去
接下来,很重要的一点,需要判断该节点在所有子节点中是不是最小的那一个,借用标记2的图片
这个方法点击去后,有好几个实现类,找准实现类喔(maxLeases:如果你创建的是互斥锁的话,这个值默认是1). 为null说明是最小的那个节点,因为他不需要监听任何节点。如果不是最小的,需要监听你前面那个节点.。然后返回一个实例。回到标记3的图处
回到标记3的图片处,进行继续的逻辑判断
对上面的图片再延长一点:
看有没有时间参数,并等待,等待什么呢,等待这个watcher监听
这个watcher点进去
收到通知后,唤醒其他线程,唤醒干嘛呢,然后又去获取所有的子节点,找最小节点,别忘了,这是一个while循环: