记录生活中的点点滴滴

0%

一个简单的Java秒杀系统

这两天花了些时间做了一个简单的Java秒杀项目这是单机的不涉及分布式还有就是也没有设计前端的一些逻辑需求什么的相当于只算是一个入门级的但是麻雀虽小五脏俱全其中的一些思想和逻辑我感觉受益匪浅让我比较清晰地明白了一些逻辑什么的巩固了Java的学习很好

这个项目是跟着B站的一位UP主做的项目的视频地址https://www.bilibili.com/video/BV13a4y1t7Wh

这个项目是有Spring Boot框架搭建有下面一些知识点

  • 实现防止超卖
  • 使用乐观锁避免并发问题
  • 令牌桶进行限流使用谷歌的guaua实现
  • redis缓存验证限制抢购次数

秒杀系统

秒杀系统

秒杀场景

  • 电商抢购限量商品
  • 卖周董演唱会的门票
  • 火车票抢座
  • 12306 ……….

为什么要做这个系统

果你的项目流量非常小完全不用担心有并发的购买请求那么做这样一个系统意义不大但如果你的系统要像12306那样接受高并发访问和下单的考验那么你就需要一套完整的流程保护措施来保证你系统在用户流量高峰期不会被搞挂了

  • 严格防止超卖库存100件你卖了120件等着辞职吧
  • 防止黑产防止不怀好意的人群通过各种技术手段把你本该下发给群众的利益全收入了囊中
  • 保证用户体验高并发下别网页打不开了支付不成功了购物车进不去了地址改不了了这个问题非常之大涉及到各种技术也不是一下子就能讲完的甚至根本就没法讲完

保护措施有哪些

  • 乐观锁防止超卖
  • 令牌桶限流
  • Redis缓存
  • 消息队列异步处理订单
  • ………

防止超卖

毕竟你网页可以卡住最多是大家没参与到活动上网口吐芬芳骂你一波但是你要是卖多了本该拿到商品的用户可就不乐意了轻则投诉你重则找漏洞起诉赔偿让你吃不了兜着走

数据库表

先创建数据库表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
-- ----------------------------
-- Table structure for stock
-- ----------------------------
DROP TABLE IF EXISTS `stock`;
CREATE TABLE `stock` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(50) NOT NULL DEFAULT '' COMMENT '名称',
`count` int(11) NOT NULL COMMENT '库存',
`sale` int(11) NOT NULL COMMENT '已售',
`version` int(11) NOT NULL COMMENT '乐观锁版本号',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- ----------------------------
-- Table structure for stock_order
-- ----------------------------
DROP TABLE IF EXISTS `stock_order`;
CREATE TABLE `stock_order` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`sid` int(11) NOT NULL COMMENT '库存ID',
`name` varchar(30) NOT NULL DEFAULT '' COMMENT '商品名称',
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

上面的 create_time 那个字段的意思是执行update语句改变列值之后自动将改动时间填充到这个字段还是很实用的

分析业务

下面我们就新建SpringBoot项目来进行

构建好的pom.xml中的依赖项

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<dependencies>
<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.2</version>
</dependency>

<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.38</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.8</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.19</version>
</dependency>
</dependencies>

初始化如下

设计秒杀项目其实不难理解首先我们有两个实体类 Stock Order对应数据库中的两个表然后我们思考前端传来请求之后什么逻辑

其实就是拿传来的id去查询对应商品的库存看是否还有有了扣除库存然后创建订单即可

所以关键的sql就三个StockDao 有两个查库存扣除库存OrderDao 有一个创建订单

大致逻辑就是这样我们按照各层去创建类并完善逻辑代码即可了

下面我贴一下代码

controller层的 StockController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@RestController
@RequestMapping("stock")
public class StockController {
//注入Service
@Autowired
private OrderService orderService;

//开发秒杀方法
@GetMapping("kill")
public String kill(Integer id){
System.out.println("秒杀商品的id"+id);
try {
//根据秒杀商品的id调用秒杀业务
int orderId = orderService.kill(id);
return "秒杀成功订单id为" + String.valueOf(orderId);
} catch (Exception e){
e.printStackTrace();
return e.getMessage();
}

}
}

dao层的 StockDaoOrderDao

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Mapper
@Repository
public interface OrderDao {
//生成订单
void createOrder(Order order);
}

@Mapper
@Repository
public interface StockDao {
//根据商品id查询库存信息
Stock checkStock(Integer id);

//根据商品id扣除库存
void updateSale(Stock stock);
}

entity的两个实体类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
@Accessors(chain = true)
public class Order {
private Integer id;
private Integer sid;
private String name;
private Date createDate;
}

@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
@Accessors(chain = true)//允许链式set
public class Stock {
private Integer id;//id
private String name;//名称
private Integer count;//库存
private Integer sale;//已售
private Integer version;//乐观锁版本号
}

service层的逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public interface OrderService {
//用来处理秒杀返回订单id
int kill(Integer id);
}

@Service
@Transactional
public class OrderServiceImpl implements OrderService {
//注入StockDaoorderService
@Autowired
private StockDao stockDao;
@Autowired
private OrderDao orderDao;
@Override
public int kill(Integer id) {
//检验库存
Stock stock = stockDao.checkStock(id);
if(stock.getSale().equals(stock.getCount())){
throw new RuntimeException("库存不足!!!");
}else {
//扣除库存
stock.setSale(stock.getSale()+1);
stockDao.updateSale(stock);
//创建订单
Order order = new Order();
order.setSid(stock.getId()).setName(stock.getName()).setCreateDate(new Date());
orderDao.createOrder(order);
return order.getId();
}
}
}

两个mapper文件

1
2
3
4
5
6
7
8
9
10
11
12
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="cn.gs.dao.OrderDao">
<!--创建订单-->
<insert id="createOrder" parameterType="Order" useGeneratedKeys="true" keyProperty="id">
insert into stock_order values(#{id},#{sid},#{name},#{createDate})
</insert>

</mapper>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="cn.gs.dao.StockDao">
<!--根据秒杀商品id查询库存-->
<select id="checkStock" parameterType="int" resultType="Stock">
select id,name,count,sale,version from stock where id = #{id}
</select>

<!--根据商品id扣除库存-->
<update id="updateSale" parameterType="Stock">
update stock set sale = #{sale} where id = #{id}
</update>

</mapper>

这就是所有代码了

正常测试

然后往数据库中写入值然后进行测试是没有问题的

使用Jmeter进行压力测试

官网: https://jmeter.apache.org/

介绍

Apache JMeter是Apache组织开发的基于Java的压力测试工具用于对软件做压力测试它最初被设计用于Web应用测试但后来扩展到其他测试领域 它可以用于测试静态和动态资源例如静态文件Java 小服务程序CGI 脚本Java 对象数据库FTP 服务器 等等JMeter 可以用于对服务器网络或对象模拟巨大的负载来自不同压力类别下测试它们的强度和分析整体性能另外JMeter能够对应用程序做功能/回归测试通过创建带有断言的脚本来验证你的程序返回了你期望的结果

安装Jmeter
1
2
3
4
5
6
7
8
9
10
11
12
13
# 1.下载jmeter
https://jmeter.apache.org/download_jmeter.cgi
下载地址:https://mirrors.bfsu.edu.cn/apache//jmeter/binaries/apache-jmeter-5.4.1.zip
# 2.解压缩
backups ---用来对压力测试进行备份目录
bin ---Jmeter核心执行脚本文件
docs ---官方文档和案例
extras ---额外的扩展
lib ---第三方依赖库
licenses ---说明
printable_docs ---格式化文档

# 3.安装Jmeter

具体过程参考 jmeter安装及环境配置

然后启动后配置一下Group和http请求

配置好了执行就可以看到已经执行了两次了

下面我们模拟多次请求直接干到2000次采用命令的形式

1
D:\Program Files (x86)\apache-jmeter-5.4.1\bin>jmeter -n -t D:\\桌面\\HTTP Request.jmx

记得先把jmx文件保存到合适位置

测试完之后发现卖出了100个没毛病但是却生成了800个左右的订单这样确实有安全问题

使用悲观锁解决

多线程下面的确会存在安全问题所以我们使用最常见的同步的方法进行解决也就是悲观锁

不过同步方法不能放在事务中所以可以这样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//开发秒杀方法
@GetMapping("kill")
public String kill(Integer id){
System.out.println("秒杀商品的id"+id);
try {
//根据秒杀商品的id调用秒杀业务
synchronized (this){
int orderId = orderService.kill(id);
return "秒杀成功订单id为" + String.valueOf(orderId);
}
} catch (Exception e){
e.printStackTrace();
return e.getMessage();
}
}

放在最开始的位置不要放在Service中

这样测试后发现只有100个订单确实解决了并发安全问题但是这样的效率太低接下来我们通过乐观锁的方式解决

使用乐观锁解决

使用乐观锁就是利用version看前后的查询的值是否相同如果不同则告诉用户抢购失败

我解释一下我们再回顾我们最开始的逻辑如果想要实现秒杀就三步查库存扣除库存创建订单

看version在查库存和扣除库存的时候是否一致来确定用户是否能继续第三步这样就可以解决并发问题

把Service代码稍微改一下扣除库存再加一个判断条件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
@Service
@Transactional
public class OrderServiceImpl implements OrderService {
//注入StockDaoorderService
@Autowired
private StockDao stockDao;
@Autowired
private OrderDao orderDao;
@Override
public int kill(Integer id) {
//检验库存
Stock stock = checkStock(id);
//扣除库存
updateSale(stock);
//创建订单
return createOrder(stock);
}
//检验库存
private Stock checkStock(Integer id){
Stock stock = stockDao.checkStock(id);
if(stock.getSale().equals(stock.getCount()))
throw new RuntimeException("库存不足!!!");
return stock;
}
//更新库存
private void updateSale(Stock stock){
int updateRows = stockDao.updateSale(stock);
if(updateRows==0)
throw new RuntimeException("购买失败请重试");
}
//创建订单
private Integer createOrder(Stock stock){
Order order = new Order();
order.setSid(stock.getId()).setName(stock.getName()).setCreateDate(new Date());
orderDao.createOrder(order);
return order.getId();
}
}

还改了一下update语句

1
2
3
4
<!--根据商品id扣除库存-->
<update id="updateSale" parameterType="Stock">
update stock set sale=sale+1,version=version+1 where id = #{id} and version = #{version}
</update>

重新进行测试没有超卖问题

接口限流

限流:是对某一时间窗口内的请求数进行限制保持系统的可用性和稳定性防止因流量暴增而导致的系统运行缓慢或宕机

接口限流

在面临高并发的抢购请求时我们如果不对接口进行限流可能会对后台系统造成极大的压力大量的请求抢购成功时需要调用下单的接口过多的请求打到数据库会对系统的稳定性造成影响

如何使用接口限流

常用的限流算法有令牌桶和和漏桶(漏斗算法)而Google开源项目Guava中的RateLimiter使用的就是令牌桶控制算法在开发高并发系统时有三把利器用来保护系统缓存``降级限流

  • 缓存缓存的目的是提升系统访问速度和增大系统处理容量
  • 降级降级是当服务器压力剧增的情况下根据当前业务情况及流量对一些服务和页面有策略的降级一次释放服务器资源以保证核心任务的正常运行
  • 限流限流的目的是通过对并发访问\请求进行限制或者对一个事件窗口内的请求进行限速来保护系统一旦达到限制速率则可以拒绝服务排队或等待降级处理

令牌桶和漏洞算法

  • 漏斗算法漏桶算法的思路很简单水请求先进入到漏桶中漏桶以一定的速度出水当水流入速度过大会直接溢出可以看出漏桶算法能强行限制数据的传输速率
  • 令牌桶算法最初来源于计算机网络在网络传输数据时为了防止网络拥塞需限制流出网络的流量使流量以比较均匀的速度向外发送大小固定的令牌桶可自行以恒定的速率源源不断地产生令牌如果令牌不被消耗或者被消耗的速度小于产生的速度令牌就会不断的增多直到把桶填满后面如果再产生的令牌就会从桶中溢出最后桶中可以保存的最大令牌数永远不会大于超过桶的大小这意味着面对瞬时大流量该算法可以在短时间内请求到大量令牌而且拿令牌的过程并不会消耗很大的资源

令牌桶简单使用

  1. 加入依赖

    1
    2
    3
    4
    5
    <dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>30.1-jre</version>
    </dependency>
  2. 使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    //创建令牌桶实例,最多含有40个令牌
    private RateLimiter rateLimiter = RateLimiter.create(40);

    //乐观锁+令牌桶解决
    @GetMapping("killToken")
    public String killToken(Integer id){
    System.out.println("秒杀商品的id"+id);
    //如果2秒内没有获取令牌则请求失败抛弃该请求
    if(!rateLimiter.tryAcquire(2, TimeUnit.SECONDS)){
    log.info("抛弃请求: 抢购失败,当前秒杀活动过于火爆,请重试");
    return "抢购失败,当前秒杀活动过于火爆,请重试";
    }
    try {
    //根据秒杀商品的id调用秒杀业务
    int orderId = orderService.kill(id);
    return "秒杀成功订单id为" + String.valueOf(orderId);
    } catch (Exception e){
    e.printStackTrace();
    return e.getMessage();
    }
    }

然后更改一下请求的接口再进行测试

发现2000个请求有一些因为时间超时被抛弃了只卖出了六十多个商品没有卖完

确实这样会存在卖不完商品的问题如果想解决可以增加令牌数或者增大请求时间即可

隐藏秒杀接口

在前几次课程中我们完成了防止超卖商品和抢购接口的限流已经能够防止大流量把我们的服务器直接搞炸这篇文章中我们要开始关心一些细节问题我们现在设计的系统还有一些问题

  1. 我们应该在一定的时间内执行秒杀处理不能在任意时间内都接受秒杀请求如何加入时间验证
  2. 还有就是对于那些脚本抢购的怎么办
  3. 秒杀之后如何限制单个用户的请求

这个阶段主要讲解秒杀系统中关于抢购下单接口相关的单用户防刷措施主要有下面内容

  • 限时抢购redis实现
  • 抢购接口隐藏增加一个token验证
  • 单用户限制频率主要还是redis缓存验证秒杀次数

限时抢购的实现

使用redis来记录秒杀商品的时间对秒杀过期的时间进行拒绝处理

先启动我们的redis服务器

1
./redis-server redis.conf

在maven项目中加入redis依赖利用spring-boot-stater-data-redis来操作redis

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

修改配置文件

1
2
3
4
5
spring:
redis:
database: 0
host: 192.168.138.131
port: 6379

随便进行测试一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Override
public int kill(Integer id) {
//先校验redis缓存时间
if (!stringRedisTemplate.hasKey("kill"+id)) {
throw new RuntimeException("秒杀超时活动已经结束了");
}

//检验库存
Stock stock = checkStock(id);
//扣除库存
updateSale(stock);
//创建订单
return createOrder(stock);
}

不过要记得把秒杀的商品id放入redis内存中并设置超时

1
2
127.0.0.1:6379> set kill1 1 EX 180
OK

这里以kill加商品id作为value设置180秒

然后这个时候启动项目并进行测试如果超时了也就是超过了180秒再去抢购就会发现秒杀结束了

秒杀接口隐藏

我们要进行秒杀接口隐藏否则一个随便懂计算机的人按按F12就会发现接口然后就能发现接口然后用脚本进行抢购所以我们要避免这种情况的发生

怎么解决

就是先给用户的id+商品的id生成一个验证值用户请求秒杀商品时要带上这个验证值验证值正确才能进行抢购

那么我们就需要增加一个表也就是user

1
2
3
4
5
6
7
8
9
10
11
12
13
14
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
`name` varchar(80) DEFAULT NULL COMMENT '用户名',
`password` varchar(40) DEFAULT NULL COMMENT '用户密码',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

SET FOREIGN_KEY_CHECKS = 1;

接下来就是实体类加三层dao-service-controller代码

先看实体类User

1
2
3
4
5
6
7
8
9
10
@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
@Accessors(chain = true)//允许链式set
public class User {
private Integer id;
private String name;
private String password;
}

dao层

1
2
3
4
5
@Mapper
@Repository
public interface UserDao {
User findById(Integer id);
}

对应的xml结构

1
2
3
4
<!--根据id查询用户-->
<select id="findById" parameterType="Integer" resultType="User">
select id,name,password from user where id = #{id}
</select>

然后业务层Service生成md5的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 给OrderService中增加一个接口
String getMd5(Integer id, Integer userId);

//具体实现
@Override
public String getMd5(Integer id, Integer userId) {
//检验用户的合法性
User user = userDao.findById(id);
if(user==null) throw new RuntimeException("用户信息不合法");
log.info("用户信息[{}]",user.toString());
Stock stock = stockDao.checkStock(id);
if(stock==null) throw new RuntimeException("商品信息不合法");
log.info("商品信息[{}]",stock.toString());
//生成hashkey
String hashKey = "KEY_"+userId+"_"+id;
//生成md5
String key = DigestUtils.md5DigestAsHex((userId+id+"Q*jS#").getBytes());
stringRedisTemplate.opsForValue().set(hashKey,key,60, TimeUnit.SECONDS);
log.info("Redis中写入[{}] [{}]",hashKey,key);
return key;
}

然后控制器那边也需要有一个方法

1
2
3
4
5
6
7
8
9
10
11
12
//生成md5方法
@RequestMapping("md5")
public String getMd5(Integer id, Integer userId){
String md5;
try {
md5 = orderService.getMd5(id,userId);
} catch (Exception e){
e.printStackTrace();
return "获取md5失败 " + e.getMessage();
}
return "获取md5的值为"+md5;
}

然后进行测试给数据库的user表中增加一条用户信息

记得把商品库存也改一下然后再浏览器数据地址进行测试

下面我们接着写携带这个验证值下单的逻辑代码

这次我们先从controller层进行写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@GetMapping("killTokenMd5")
public String killToken(Integer id,Integer userId,String md5){
System.out.println("秒杀商品的id"+id);
//如果2秒内没有获取令牌则请求失败抛弃该请求
if(!rateLimiter.tryAcquire(2, TimeUnit.SECONDS)){
log.info("抛弃请求: 抢购失败,当前秒杀活动过于火爆,请重试");
return "抢购失败,当前秒杀活动过于火爆,请重试";
}
try {
//根据秒杀商品的id调用秒杀业务
int orderId = orderService.kill(id,userId,md5);
return "秒杀成功订单id为" + String.valueOf(orderId);
} catch (Exception e){
e.printStackTrace();
return e.getMessage();
}
}

Service层的接口代码及具体实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//用来处理秒杀返回订单id加入md5
int kill(Integer id,Integer userId,String md);

//kill的升级版加入了md5
@Override
public int kill(Integer id, Integer userId, String md) {
//先验证签名
String hashKey = "KEY_"+userId+"_"+id;
String s = stringRedisTemplate.opsForValue().get(hashKey);
if(s==null) throw new RuntimeException("没有携带用户签名请求不合法");
if(!s.equals(md)) throw new RuntimeException("当前请求数据不合法请稍后再试");

//检验库存
Stock stock = checkStock(id);
//扣除库存
updateSale(stock);
//创建订单
return createOrder(stock);
}

这个验证是在redis中进行的所以不需要写dao层代码这样就可以了

也没有什么要测试的但是其实还是有一个问题

我们既然是为了防止别人写脚本进行抢购但是这样照样还是会被发现你看既然我们是先获取用户id加商品id获取验证值但是如果别人发现了这个获取md5验证值的接口然后获得验证值还是可以写出脚本进行抢购所以我们还需要一步就是限制单个用户的抢购次数

限制单用户抢购次数

这个逻辑其实也很简单就是验证一下这个用户的抢购次数是否超出了某一范围如果超过了的话就可以直接抛出异常结束这次抢购了

验证抢购次数这一操作我们应该让它在redis缓存中完成

下面开始写代码先从controller层写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//乐观锁+令牌桶+md5+限制单用户频率解决
@GetMapping("killTokenMd5Limit")
public String killTokenMd5Limit(Integer id,Integer userId,String md5){
System.out.println("秒杀商品的id"+id);
//如果2秒内没有获取令牌则请求失败抛弃该请求
if(!rateLimiter.tryAcquire(2, TimeUnit.SECONDS)){
log.info("抛弃请求: 抢购失败,当前秒杀活动过于火爆,请重试");
return "抢购失败,当前秒杀活动过于火爆,请重试";
}
try {
//加入单用户限制
int count = userService.saveUserCount(userId);
log.info("用户访问的次数为[{}]",count);
boolean isBanned = userService.getUserCount(userId);
if(isBanned){
log.info("购买失败超过频率限制");
return "购买失败超过频率限制";
}

//根据秒杀商品的id调用秒杀业务
int orderId = orderService.kill(id,userId,md5);
return "秒杀成功订单id为" + String.valueOf(orderId);
} catch (Exception e){
e.printStackTrace();
return e.getMessage();
}
}

Service层的接口代码以及具体实现

1
2
3
4
5
6
7
public interface UserService {
//往redis中写入用户次数
int saveUserCount(Integer userId);

//查询是否超过调用次数
boolean getUserCount(Integer userId);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@Service
@Transactional
@Slf4j
public class UserServiceImpl implements UserService {
@Autowired
private StringRedisTemplate stringRedisTemplate;

@Override
public int saveUserCount(Integer userId) {
//根据不同用户id生成调用次数key
String limitKey = "LIMIT" + "_" + userId;
String limitNum = stringRedisTemplate.opsForValue().get(limitKey);
int limit = 0;
if(limitNum==null){
//s为null说明是第一次
stringRedisTemplate.opsForValue().set(limitKey,"0",60, TimeUnit.SECONDS);
}else {
//不是第一次
limit = Integer.parseInt(limitNum)+1;
stringRedisTemplate.opsForValue().set(limitKey,String.valueOf(limit),60,TimeUnit.SECONDS);
}
return limit;
}

//验证用户是否超过限额true表示超过false表示没有
@Override
public boolean getUserCount(Integer userId) {
String limitKey = "LIMIT" + "_" + userId;
String limitNum = stringRedisTemplate.opsForValue().get(limitKey);
if(limitNum==null){
log.error("该用户没有访问申请验证值记录疑似异常");
return true;
}
return Integer.parseInt(limitNum) > 10;
}
}

这样就可以了