内容输出来源:拉钩教育Java就业训练营
锁:我们在多线程中接触过,作用就是让当前的资源不会被其他线程访问!
在zookeeper中使用传统的锁引发的 “羊群效应” :1000个人创建节点,只有一个人能成功,999人需要等待!
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tu2wtPme-1617846797778)(分布式锁实现商品秒杀.assets/zookeeper详解.jpg)]
避免“羊群效应”,zookeeper采用分布式锁
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zzA1EjZU-1617846797780)(分布式锁实现商品秒杀.assets/zookeeper详解-1617780648149.jpg)]
- 所有请求进来,在/lock下创建 临时顺序节点 ,放心,zookeeper会帮你编号排序
- 判断自己是不是/lock下最小的节点
- 是,获得锁(创建节点)
- 否,对前面小我一级的节点进行监听
- 获得锁请求,处理完业务逻辑,释放锁(删除节点),后一个节点得到通知(比你年轻的死了,你成为最年轻的了)
- 重复步骤2
实现步骤
1 初始化数据库
-- 商品表
create table product(
id int primary key auto_increment, -- 商品编号
product_name varchar(20) not null, -- 商品名称
stock int not null, -- 库存
version int not null -- 版本
)
insert into product (product_name,stock,version) values('锦鲤-清空购物车-大奖',5,0)
-- 订单表
create table `order`(
id varchar(100) primary key, -- 订单编号
pid int not null, -- 商品编号
userid int not null -- 用户编号
)
2 搭建工程
工程构造
实体类Order和Product自行创建
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fqgTpRT3-1617846797781)(分布式锁实现商品秒杀.assets/微信截图_20210407161310.png)]
<packaging>war</packaging>
<properties>
<spring.version>5.2.7.RELEASE</spring.version>
</properties>
<dependencies>
<!-- Spring -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>${spring.version}</version>
</dependency>
<!-- Mybatis -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.5</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>2.0.5</version>
</dependency>
<!-- 连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.10</version>
</dependency>
<!-- 数据库 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.20</version>
</dependency>
<!-- junit -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<!-- maven内嵌的tomcat插件 -->
<plugin>
<groupId>org.apache.tomcat.maven</groupId>
<!-- 目前apache只提供了tomcat6和tomcat7两个插件 -->
<artifactId>tomcat7-maven-plugin</artifactId>
<configuration>
<port>8001</port>
<path>/</path>
</configuration>
<executions>
<execution>
<!-- 打包完成后,运行服务 -->
<phase>package</phase>
<goals>
<goal>run</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
mybatis配置文件
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!-- 后台的日志输出:针对开发者-->
<settings>
<setting name="logImpl" value="STDOUT_LOGGING"/>
</settings>
</configuration>
spring配置文件
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd">
<!-- 1.扫描包下的注解 -->
<context:component-scan base-package="controller,service,mapper"/>
<!-- 2.创建数据连接池对象 -->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"
destroy-method="close">
<property name="url" value="jdbc:mysql://192.168.204.131:3306/zkproduct?
serverTimezone=GMT" />
<property name="driverClassName" value="com.mysql.jdbc.Driver" />
<property name="username" value="root" />
<property name="password" value="123123" />
<property name="maxActive" value="10" />
<property name="minIdle" value="5" />
</bean>
<!-- 3.创建SqlSessionFactory,并引入数据源对象 -->
<bean id="sqlSessionFactory"
class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"></property>
<property name="configLocation" value="classpath:mybatis/mybatis-config.xml"></property>
</bean>
<!-- 4.告诉spring容器,数据库语句代码在哪个文件中-->
<!-- mapper.xDao接口对应resources/mapper/xDao.xml-->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="mapper"></property>
</bean>
<!-- 5.将数据源关联到事务 -->
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!-- 6.开启事务 -->
<tx:annotation-driven/>
</beans>
webxml
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
version="3.1">
<servlet>
<servlet-name>springMVC</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring/spring.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
<async-supported>true</async-supported>
</servlet>
<servlet-mapping>
<servlet-name>springMVC</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
OrderMapper
@Mapper
@Component
public interface OrderMapper {
// 生成订单
@Insert("insert into `order` (id,pid,userid) values (#{id},#{pid},#{userid})")
int insert(Order order);
}
ProducttionMapper
@Mapper
@Component
public interface ProductMapper {
// 查询商品(目的查库存)
@Select("select * from product where id = #{id}")
Product getProduct(@Param("id") int id);
// 减库存
@Update("update product set stock = stock-1 where id = #{id}")
int reduceStock(@Param("id") int id);
}
ProductService
public interface ProductService {
//减库存
void reduceStock(int id) throws Exception;
}
ProductServiceImpl
@Service
public class ProductServiceImpl implements ProductService {
@Autowired
ProductMapper productMapper;
@Autowired
OrderMapper orderMapper;
public void reduceStock(int id) throws Exception {
// 获取库存(根据商品id查询商品)
Product product = productMapper.getProduct(id);
// 模拟网络延迟
Thread.sleep(1000);
if(product.getStock() <= 0)
throw new RuntimeException("已抢光!");
// 2.减库存
int i = productMapper.reduceStock(id);
if(i == 1){
Order order = new Order();
order.setId(UUID.randomUUID().toString());
order.setPid(id);
order.setUserId(101);
orderMapper.insert(order);
}else
throw new RuntimeException("减库存失败,请重试!");
}
}
controller
@Controller
public class ProductAction {
@Autowired
private ProductService productService;
@GetMapping("/prodeuct/reduce")
@ResponseBody
public Object reduce(int id) throws Exception {
productService.reduceStock(id);
return "ok";
}
}
3 启动测试
-
启动两次工程,端口号分别8001和8002
-
使用nginx做负载均衡
upstream sga{ server 192.168.163.128:8001; server 192.168.163.128:8002; } server { listen 80; server_name localhost; #charset koi8-r; #access_log logs/host.access.log main; location / { proxy_pass http://sga; root html; index index.html index.htm; }
-
使用 JMeter 模拟1秒内发出10个http请求
4. apahce提供的zookeeper客户端
基于zookeeper原生态的客户端类实现分布式是非常麻烦的,我们使用apahce提供了一个zookeeper客户端来实现
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>4.2.0</version>
</dependency>
5. 在控制层中加入分布式锁的逻辑代码
@Controller
public class ProductAction {
@Autowired
private ProductService productService;
private static String connectString =
"192.168.204.141:2181,192.168.204.142:2181,192.168.204.143:2181";
@GetMapping("/product/reduce")
@ResponseBody
public Object reduce( int id) throws Exception {
// 重试策略 (1000毫秒试1次,最多试3次)
RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);
//1.创建curator工具对象
CuratorFramework client =
CuratorFrameworkFactory.newClient(connectString, retryPolicy);
client.start();
//2.根据工具对象创建“内部互斥锁”
InterProcessMutex lock = new InterProcessMutex(client, "/product_"+id);
try {
//3.加锁
lock.acquire();
productService.reduceStock(id);
}catch(Exception e){
if(e instanceof RuntimeException){
throw e;
}
}finally{
//4.释放锁
lock.release();
}
return "ok";
}
}
k = new InterProcessMutex(client, “/product_”+id);
try {
//3.加锁
lock.acquire();
productService.reduceStock(id);
}catch(Exception e){
if(e instanceof RuntimeException){
throw e;
}
}finally{
//4.释放锁
lock.release();
}
return “ok”;
}
}