目录
1. 添加pom依赖
<?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.7.5</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.jmh</groupId>
<artifactId>seckill</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>seckill</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<!--<spring-boot.version>2.4.1</spring-boot.version>-->
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
<version>5.1.44</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- mybatis plus依赖 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.1</version>
</dependency>
<!--mybatis-plus生成器-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.5.2</version>
</dependency>
<!-- MD5依赖 -->
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.12.0</version>
</dependency>
<!-- valid验证依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!--redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--hariki-->
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
</dependency>
<!--rabbitmq 消息队列-->
<dependency>
<groupId>org.springframework.amqp</groupId>
<artifactId>spring-rabbit-test</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<!--zookeeper客户端-->
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-framework</artifactId>
<version>5.1.0</version>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>5.1.0</version>
</dependency>
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<!-- 需要与集群中版本号一致 -->
<version>3.6.2</version>
</dependency>
<!-- redisson -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.5.0</version>
</dependency>
<!--commons-pool2-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
2. 配置application.yml
#配置redission
spring:
redis:
#服务端IP
host: 127.0.0.1
#端口
port: 6379
#密码
password: 1234
#选择数据库
database: 0
#超时时间
timeout: 10000ms
#Lettuce的连接是基于Netty的,连接实例(StatefulRedisConnection)可以在多个线程间并发访问
#Lettuce线程安全,Jedis线程非安全
lettuce:
pool:
#最大连接数,默认8
max-active: 8
#最大连接阻塞等待时间,默认-1
max-wait: 10000ms
#最大空闲连接,默认8
max-idle: 200
#最小空闲连接,默认0
min-idle: 5
#配置zookeeper
zookeeper:
#连接IP地址
server: 192.168.119.128:2181
#会话超时时间
sessionTimeoutMs: 60000
#连接超时时间
connectionTimeoutMs: 60000
#最大重试次数
maxRetries: 3
#重试间隔时间
baseSleepTimeMs: 1000
3. zookeeper模块
-
ZookeeperClient
package com.jmh.seckill.zookeeper;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.recipes.cache.*;
import org.apache.curator.retry.ExponentialBackoffRetry;
import org.apache.curator.utils.CloseableUtils;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.data.Stat;
import java.util.List;
@Data
@Slf4j
@SuppressWarnings("all")
public class ZookeeperClient {
/*
zookeepernode节点的类型:
1.持久化目录节点(PERSISTENT), 创建好之后就永久存在
2.持久化顺序编号节点(PERSISTENT_SEQUENTIAL),创建好节点后还可以默认带个自增的编号
3.临时目录节点(EPHEMERAL),和sessionId绑定的,当客户端被关闭之后,对于的临时目录节点会被删除
4.历史顺序编号目录节点(EPHEMERAL_SEQUENTIAL),临时节点带个自增的编号
5.容器节点(Container),3.5.3新增的特性,没有子节点的容器节点会被清除掉。
6.TTL节点,3.5.3新增的特性,位节点设定了失效时间。具体失效时间却决于后台检测失效线程的轮询频率。
*/
private CuratorFramework client;
//NodeCache 监听节点对应增、删、改操作
private NodeCache nodeCache;
//PathChildrenCache 监听节点下一级子节点的增、删、改操作
private PathChildrenCache pathChildrenCache;
//TreeCache 可以将指定的路径节点作为根节点,对其所有的子节点操作进行监听,呈现树形目录的监听
private TreeCache treeCache;
//CuratorCache用于替换NodeCache/TreeCache/PathChildrenCache
//private CuratorCache curatorCache;
private String zookeeperServer;
private int sessionTimeoutMs;
private int connectionTimeoutMs;
private int baseSleepTimeMs;
private int maxRetries;
/**
* 初始化
*/
public void init() {
client = CuratorFrameworkFactory.builder()
.connectString(zookeeperServer)
.sessionTimeoutMs(sessionTimeoutMs)
.retryPolicy(new ExponentialBackoffRetry(baseSleepTimeMs, maxRetries))
.build();
client.start();
log.info("===============> zookeeper init");
}
/**
* 销毁
*/
public void destory() {
if (null != client)
CloseableUtils.closeQuietly(client);
if (null != nodeCache)
CloseableUtils.closeQuietly(nodeCache);
if (null != treeCache)
CloseableUtils.closeQuietly(treeCache);
if (null != pathChildrenCache)
CloseableUtils.closeQuietly(pathChildrenCache);
/* if(null!=curatorCache)
CloseableUtils.closeQuietly(curatorCache);*/
log.info("===============> zookeeper destory");
}
/**
* 创建永久Zookeeper节点
*
* @param nodePath 节点路径(如果父节点不存在则会自动创建父节点),如:/curator
* @param nodeValue 节点数据
* @return 返回创建成功的节点路径
*/
public String createPersistentNode(String nodePath, String nodeValue) {
try {
return client.create().creatingParentsIfNeeded()
.forPath(nodePath, nodeValue.getBytes());
} catch (Exception e) {
log.error("创建永久Zookeeper节点失败,nodePath:{},nodeValue:{},e={}", nodePath, nodeValue, e.getMessage());
e.printStackTrace();
}
return null;
}
/**
* 创建永久有序Zookeeper节点
*
* @param nodePath 节点路径(如果父节点不存在则会自动创建父节点),如:/curator
* @param nodeValue 节点数据
* @return 返回创建成功的节点路径
*/
public String createSequentialPersistentNode(String nodePath, String nodeValue) {
try {
return client.create().creatingParentsIfNeeded()
.withMode(CreateMode.PERSISTENT_SEQUENTIAL)
.forPath(nodePath, nodeValue.getBytes());
} catch (Exception e) {
log.error("创建永久有序Zookeeper节点失败,nodePath:{},nodeValue:{}", nodePath, nodeValue);
}
return null;
}
/**
* 创建临时Zookeeper节点
*
* @param nodePath 节点路径(如果父节点不存在则会自动创建父节点),如:/curator
* @param nodeValue 节点数据
* @return 返回创建成功的节点路径
*/
public String createEphemeralNode(String nodePath, String nodeValue) {
try {
return client.create().creatingParentsIfNeeded()
.withMode(CreateMode.EPHEMERAL)
.forPath(nodePath, nodeValue.getBytes());
} catch (Exception e) {
log.error("创建临时Zookeeper节点失败,nodePath:{},nodeValue:{}", nodePath, nodeValue);
}
return null;
}
/**
* 创建临时有序Zookeeper节点
*
* @param nodePath 节点路径(如果父节点不存在则会自动创建父节点),如:/curator
* @param nodeValue 节点数据
* @return 返回创建成功的节点路径
*/
public String createSequentialEphemeralNode(String nodePath, String nodeValue) {
try {
return client.create().creatingParentsIfNeeded()
.withMode(CreateMode.EPHEMERAL_SEQUENTIAL)
.forPath(nodePath, nodeValue.getBytes());
} catch (Exception e) {
log.error("创建临时有序Zookeeper节点失败,nodePath:{},nodeValue:{}", nodePath, nodeValue);
}
return null;
}
/**
* 检查Zookeeper节点是否存在
*
* @param nodePath 节点路径
* @return 如果存在则返回true
*/
public boolean checkExists(String nodePath) {
try {
Stat stat = client.checkExists().forPath(nodePath);
return stat != null;
} catch (Exception e) {
log.error("检查Zookeeper节点是否存在出现异常,nodePath:{}", nodePath);
}
return false;
}
/**
* 获取某个Zookeeper节点的所有子节点
*
* @param nodePath 节点路径
* @return 返回所有子节点的节点名
*/
public List<String> getChildren(String nodePath) {
try {
return client.getChildren().forPath(nodePath);
} catch (Exception e) {
log.error("获取某个Zookeeper节点的所有子节点出现异常,nodePath:{}", nodePath);
}
return null;
}
/**
* 获取某个Zookeeper节点的数据
*
* @param nodePath 节点路径
* @return
*/
public String getData(String nodePath) {
try {
return new String(client.getData().forPath(nodePath));
} catch (Exception e) {
log.error("获取某个Zookeeper节点的数据出现异常,nodePath:{}", nodePath);
}
return null;
}
/**
* 设置某个Zookeeper节点的数据
*
* @param nodePath 节点路径
*/
public void setData(String nodePath, String newNodeValue) {
try {
client.setData().forPath(nodePath, newNodeValue.getBytes());
} catch (Exception e) {
e.printStackTrace();
log.error("设置某个Zookeeper节点的数据出现异常,nodePath:{}", nodePath);
}
}
/**
* 删除某个Zookeeper节点
*
* @param nodePath 节点路径
*/
public void delete(String nodePath) {
try {
client.delete().guaranteed().forPath(nodePath);
} catch (Exception e) {
log.error("删除某个Zookeeper节点出现异常,nodePath:{}", nodePath);
}
}
/**
* 级联删除某个Zookeeper节点及其子节点
*
* @param nodePath 节点路径
*/
public void deleteChildrenIfNeeded(String nodePath) {
try {
client.delete().guaranteed().deletingChildrenIfNeeded().forPath(nodePath);
} catch (Exception e) {
log.error("级联删除某个Zookeeper节点及其子节点出现异常,nodePath:{}", nodePath);
}
}
/**
* <p><b>注册节点监听器</b></p>
* NodeCache: 对一个节点进行监听,监听事件包括指定路径的增删改操作
*
* @param nodePath 节点路径
* @return void
*/
public NodeCache registerNodeCacheListener(String nodePath) {
try {
//1. 创建一个NodeCache
nodeCache = new NodeCache(client, nodePath);
//2. 添加节点监听器
nodeCache.getListenable().addListener(() -> {
ChildData childData = nodeCache.getCurrentData();
if (childData != null) {
System.out.println("Path: " + childData.getPath());
System.out.println("Stat:" + childData.getStat());
System.out.println("Data: " + new String(childData.getData()));
}
});
//3. 启动监听器
nodeCache.start();
//4. 返回NodeCache
return nodeCache;
} catch (Exception e) {
log.error("注册节点监听器出现异常,nodePath:{}", nodePath);
}
return null;
}
/**
* <p><b>注册子目录监听器</b></p>
* PathChildrenCache:对指定路径节点的一级子目录监听,不对该节点的操作监听,对其子目录的增删改操作监听
*
* @param nodePath 节点路径
* @param listener 监控事件的回调接口
* @return PathChildrenCache
*/
public PathChildrenCache registerPathChildListener(String nodePath,
PathChildrenCacheListener listener) {
try {
//1. 创建一个PathChildrenCache
pathChildrenCache = new PathChildrenCache(client, nodePath, true);
//2. 添加目录监听器
pathChildrenCache.getListenable().addListener(listener);
//3. 启动监听器
pathChildrenCache.start(PathChildrenCache.StartMode.BUILD_INITIAL_CACHE);
//4. 返回PathChildrenCache
return pathChildrenCache;
} catch (Exception e) {
log.error("注册子目录监听器出现异常,nodePath:{}", nodePath);
}
return null;
}
/**
* <p><b>注册目录监听器</b></p>
* TreeCache:综合NodeCache和PathChildrenCahce的特性,可以对整个目录进行监听,同时还可以设置监听深度
*
* @param nodePath 节点路径
* @param maxDepth 自定义监控深度
* @param listener 监控事件的回调接口
* @return TreeCache
*/
public TreeCache registerTreeCacheListener(String nodePath, int maxDepth, TreeCacheListener listener) {
try {
//1. 创建一个TreeCache
treeCache = TreeCache.newBuilder(client, nodePath)
.setCacheData(true)
.setMaxDepth(maxDepth)
.build();
//2. 添加目录监听器
treeCache.getListenable().addListener(listener);
//3. 启动监听器
treeCache.start();
//4. 返回TreeCache
return treeCache;
} catch (Exception e) {
log.error("注册目录监听器出现异常,nodePath:{},maxDepth:{1}", nodePath);
}
return null;
}
}
- ZookeeperConfiguration
package com.jmh.seckill.zookeeper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@Slf4j
@SuppressWarnings("all")
//@ConfigurationProperties(prefix = "zookeeper")
public class ZookeeperConfiguration {
@Value("${zookeeper.server}")
private String zookeeperServer;
@Value("${zookeeper.sessionTimeoutMs}")
private int sessionTimeoutMs;
@Value("${zookeeper.connectionTimeoutMs}")
private int connectionTimeoutMs;
@Value("${zookeeper.maxRetries}")
private int maxRetries;
@Value("${zookeeper.baseSleepTimeMs}")
private int baseSleepTimeMs;
@Bean(initMethod = "init", destroyMethod = "destory")
public ZookeeperClient zookeeperClient() {
ZookeeperClient zookeeperClient = new ZookeeperClient();
zookeeperClient.setZookeeperServer(zookeeperServer);
zookeeperClient.setSessionTimeoutMs(sessionTimeoutMs);
zookeeperClient.setConnectionTimeoutMs(connectionTimeoutMs);
zookeeperClient.setBaseSleepTimeMs(baseSleepTimeMs);
zookeeperClient.setMaxRetries(maxRetries);
return zookeeperClient;
}
}
4. 添加RedissonConfig配置文件
package com.jmh.seckill.config;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RedissonConfig {
@Value("${spring.redis.host}")
private String host;
@Value("${spring.redis.port}")
private String port;
@Value("${spring.redis.password}")
private String password;
@Bean
public RedissonClient redissonClient(){
Config config=new Config();
String url="redis://"+host+":"+port;
config.useSingleServer().setAddress(url).setPassword(password).setDatabase(0);
return Redisson.create(config);
}
}
5. 参考核心代码如下
package com.jmh.seckill.controller;
import com.jmh.seckill.exception.BusinessException;
import com.jmh.seckill.pojo.SeckillGoods;
import com.jmh.seckill.pojo.SeckillOrder;
import com.jmh.seckill.pojo.User;
import com.jmh.seckill.service.IRedisService;
import com.jmh.seckill.service.ISeckillGoodsService;
import com.jmh.seckill.service.ISeckillOrderService;
import com.jmh.seckill.utils.JsonResponseBody;
import com.jmh.seckill.utils.JsonResponseStatus;
import com.jmh.seckill.zookeeper.ZookeeperClient;
import lombok.extern.slf4j.Slf4j;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.recipes.cache.ChildData;
import org.apache.curator.framework.recipes.cache.PathChildrenCacheEvent;
import org.apache.curator.framework.recipes.cache.PathChildrenCacheListener;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* <p>
* 秒杀订单信息表 前端控制器
* </p>
*
* @author jmh
* @since 2023-01-12
*/
@RestController
@RequestMapping("/seckillOrder")
@Slf4j
public class SeckillOrderController implements InitializingBean {
@Autowired
private ISeckillOrderService seckillOrderService;
@Autowired
private IRedisService redisService;
@Autowired
private ISeckillGoodsService seckillGoodsService;
@Autowired
private ZookeeperClient zookeeperClient;
@Autowired
private RedissonClient redissonClient;
//定义内存标记
private Map<String,Boolean> stockMap=new ConcurrentHashMap<>();
/**
* 服务预热方法
* @throws Exception 异常
*/
@Override
public void afterPropertiesSet() throws Exception {
seckillGoodsService.list().forEach(e->{
redisService.setSeckillGoods(e.getGoodsId(),Long.valueOf(e.getStockCount()));
//创造内存标记
String key="/seckillGoodsId/"+e.getGoodsId();
//设置内存标记相应的key和值,false代表没卖完,true代表已卖完
stockMap.put(key,false);
//给zookeeper制造key和值
if (zookeeperClient.checkExists(key))
zookeeperClient.setData(key, "false");
else
zookeeperClient.createPersistentNode(key, "false");
log.warn(e.getGoodsId() + "/" + e.getStockCount());
});
//注册zookeeper目录监听器,用于监听指定目录下的节点值变化
zookeeperClient.registerPathChildListener("/seckillGoodsId", new PathChildrenCacheListener() {
@Override
public void childEvent(CuratorFramework client,
PathChildrenCacheEvent event) throws Exception {
ChildData data = event.getData();
String flag = new String(data.getData());
switch (event.getType()) {
case CHILD_ADDED:
log.info("子节点增加, path={}, data={}", data.getPath(), flag);
break;
case CHILD_UPDATED:
log.info("子节点更新, path={}, data={}", data.getPath(), flag);
// 如果zookeeper中的商品节点标记为true,则代表秒杀商品已经秒杀完毕
// 修改内存标记为true(代表秒杀商品库存不足)
if ("true".equals(flag)) {
stockMap.put(data.getPath(), true);
}
break;
case CHILD_REMOVED:
log.info("子节点删除, path={}, data={}", data.getPath(), flag);
break;
default:
break;
}
}
});
}
/**
* 秒杀商品订单生成方法
* @param goodsId 商品编号
* @param user 用户对象
* @return 结果
*/
@RequestMapping("/addOrder")
public JsonResponseBody<?> addOrder(Long goodsId, User user){
//制造Jvm内存标记判断秒杀商品是否已经卖完
if (stockMap.get("/seckillGoodsId/"+goodsId))
throw new BusinessException(JsonResponseStatus.ORDER_COUNT);
//限购:判断用户是否重复抢购[redis]
SeckillOrder byUidAndGoodsIdToSeckillOrder = redisService.getByUidAndGoodsIdToSeckillOrder(user.getId(), goodsId);
if (null!=byUidAndGoodsIdToSeckillOrder)
throw new BusinessException(JsonResponseStatus.ORDER_RESP);
//使用redisson分布式锁
RLock clientLock = redissonClient.getLock("/seckillGoodsId/"+goodsId);
clientLock.lock();
try {
//判断秒杀商品库存是否充足[redis]
if (redisService.decrement(goodsId) < 0) {
redisService.increment(goodsId);
//将zookeeper监听的值改为true
zookeeperClient.setData("/seckillGoodsId/" + goodsId, "true");
throw new BusinessException(JsonResponseStatus.ORDER_COUNT);
}
}catch (Exception e){
if (e instanceof BusinessException){
throw new BusinessException(JsonResponseStatus.ORDER_COUNT);
}
throw new BusinessException(JsonResponseStatus.ERROR);
} finally {
//解锁
clientLock.unlock();
}
//生成秒杀订单
SeckillOrder msOrder=new SeckillOrder();
msOrder.setUserId(user.getId());
msOrder.setGoodsId(goodsId);
//将订单数据交给rabbitMQ进行异步处理订单
seckillOrderService.sendSeckillOrderToRabbitMQ(msOrder);
return new JsonResponseBody<>();
}
}