秒杀demo
**b站秒杀demo**
有些代码可能有些错误和不妥,欢迎指正
秒杀
秒杀方案
数据库需要新的秒杀商品及秒杀订单表(方便后期更改维护)
秒杀倒计时设置:
例子:
秒杀基本流程(未优化):
压力测试:
使用jmeter进行压力测试,下文链接有详细的描述:
Jmeter性能测试
linux安装mysql
服务优化
页面缓存,对象缓存
前后端未分离时做页面缓存。
对象缓存,相比于页面缓存,具有更细的粒度。
页面静态化
…
库存超卖问题
超卖问题产生在秒杀商品减少库存部分。
秒杀实现步骤为:
减少库存->生成订单->生成秒杀订单
减少库存前判断库存是否大于0,
为防止一个人重复抢购(同时发起两个请求)可以加锁(乐观锁)/加唯一索引:
将用户id和商品id绑定成唯一索引,(开启事务:@Transactional)
rabbitmq安装使用
springboot集成rabbitmq
引入依赖:
<!--amqp-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
在yml中配置:
#在spring的下级
#RabbitMQ
rabbitmq:
#服务器
host: 127.0.0.1
#用户名
username: guest
#密码
password: guest
#虚拟主机
virtual-host: /
#端口
port: 5672
listener:
simple:
#消费者最小数量
concurrency: 10
#消费者最大数量
max-concurrency: 10
#限制消费者每次只能处理一条消息,处理完在继续下一条消息
prefetch: 1
#启动是默认启动容器
auto-startup: true
#被拒绝时重新进入队列
default-requeue-rejected: true
template:
retry:
#发布重试,默认false
enabled: true
#重试时间,默认1000ms
initial-interval: 1000ms
#重试最大次数,默认3次
max-attempts: 3
#最大重试间隔时间
max-interval: 10000ms
#重试的间隔乘数,比如配2。0 第一等10s 第二次等20s 第三次等40s
multiplier: 1
创建配置类:
定义简单的发送者和接收者:
@Service
@Slf4j
public class MQReceiver {
@RabbitListener(queues = "queue")
public void receive(Object msg) {
System.out.println("接收到的消息" + msg);
}
}
@Service
@Slf4j
public class MQSender {
@Autowired
private RabbitTemplate rabbitTemplate;
public void send(Object msg) {
log.info("发送消息:" + msg);
// rabbitTemplate.convertAndSend("queue", msg);
rabbitTemplate.convertAndSend("queue", msg);
}
}
测试代码:
@RestController
@RequestMapping("/user")
@Api(value = "用户表", tags = "用户表")
public class TUserController {
@Autowired
private MQSender mqSender;
@RequestMapping(value = "/mq", method = RequestMethod.GET)
@ResponseBody
public void mq() {
mqSender.send("Hello");
}
}
访问下面的接口就可以在mq的management中看到相关信息:
rabbitmq交换机模式
Rabbit中的消息传递模型中,生产者是发送消息的用户应用程序。队列是存储淌息的缓冲区。使用者是接收消息的用户应用程序。RabbitMQ消息传递模型的核心思想是生产者从不直接向队列发送任何消息。生产者只能向交换器发送消息。交换是—件很简单的事情。它一边接收来自生产者的消息,另一边将消息推送到队列。交换器必须确切知道如何处理它接收到的消息。它应该被附加到一个特定的队列吗?它应该被附加到多个队列吗?或者它应该被丢弃。它的规则由交换类型定义。
交换机类型:direct(直连),Fanout(广播模式),topic模式(使用最频繁),Headers模式(效率低,很少用)
下文有详细介绍:
交换机模式介绍
如下图direct模式下可以选择不同的路由从而转发到对应的队列。
如下图为topic模式,也是有路由key,但为了方便管理路由key引入了通配符
*匹配精确的一个,#匹配0个或者多个
redis预减库存
为减轻数据库压力,对redis进行预减库存操作。
系统初始化时,将商品库存加载到redis中,当收到秒杀请求时,redis预先减少库存(避免对数据库进行操作),库存不足则直接返回。库存充足则进行下一步->
将请求封装成对象,发送给rabbitmq(进行异步操作,快速处理前期的大量请求。流量削峰)->返回结果为排队中。下一步->
客户端轮询,判断是否真正秒杀成功->存入数据库->redis。
预加载库存
首先需要预加载库存
controller中实现初始化接口,并实现初始化方法
//实现InitializingBean 接口
public class SeKillController implements InitializingBean {
...........
...........
/**
* 系统初始化,把商品库存数量加载到Redis
*
* @param
* @return void
**/
@Override
public void afterPropertiesSet() throws Exception {
List<GoodsVo> list = itGoodsService.findGoodsVo();
if (CollectionUtils.isEmpty(list)) {
return;
}
list.forEach(goodsVo -> {
redisTemplate.opsForValue().set("seckillGoods:" + goodsVo.getId(), goodsVo.getStockCount());
//内存标记商品,后续操作减少redis访问
EmptyStockMap.put(goodsVo.getId(), false);
});
}
预减库存
ValueOperations valueOperations = redisTemplate.opsForValue();
//判断是否重复抢购
TSeckillOrder tSeckillOrder = (TSeckillOrder) redisTemplate.opsForValue().get("order:" + user.getId() + ":" + goodsId);
if (tSeckillOrder != null) {
return RespBean.error(RespBeanEnum.REPEATE_ERROR);
}
//预减库存,返回递减之后的库存
Long stock = valueOperations.decrement("seckillGoods:" + goodsId);
if (stock < 0) {
//标记商品减轻redis压力操作
EmptyStockMap.put(goodsId, true);
//将库存置为0(上次递减为-1)
valueOperations.increment("seckillGoods:" + goodsId);
return RespBean.error(RespBeanEnum.EMPTY_STOCK);
}
在判断是否重复抢购时,如果相同用户的请求在排队中还未写入 redis,代码判断能通过,但因为数据库中加入了用户+商品的唯一索引,所以不会出错。
在减少库存时,不直接获取值判断大于0 是因为 它不是原子性的.如果另外的请求刚好拿到还没减1,这时候你拿到了还是1,但是另外的预减了,会造成脏读。
下单
…(在后续的mq中进行)
生成order
return order;
优化:加入mq的秒杀步骤
封装秒杀信息:
@Data
@AllArgsConstructor
@NoArgsConstructor
public class SeckillMessage {
private TUser tUser;
private Long goodsId;
}
mq配置:
定义配置类:
@Configuration
public class RabbitMQTopicConfig {
private static final String QUEUE = "seckillQueue";
private static final String EXCHANGE = "seckillExchange";
@Bean
public Queue queue() {
return new Queue(QUEUE);
}
@Bean
public TopicExchange topicExchange() {
return new TopicExchange(EXCHANGE);
}
@Bean
public Binding binding() {
return BindingBuilder.bind(queue()).to(topicExchange()).with("seckill.#");
}
}
定义发送者接收者:
发送者将订单发送至消息队列
@Service
@Slf4j
public class MQSender {
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 发送秒杀信息
* @operation add
* @param message
* @return void
**/
public void sendSeckillMessage(String message) {
log.info("发送消息" + message);
rabbitTemplate.convertAndSend("seckillExchange", "seckill.message", message);
}
消费者取出订单信息,接下来进行库存判断,重复抢购判断,并下单。
@Service
@Slf4j
public class MQReceiver {
//商品service
@Autowired
private ITGoodsService itGoodsServicel;
@Autowired
private RedisTemplate redisTemplate;
//订单service
@Autowired
private ITOrderService itOrderService;
/**
* 下单操作
*
* @param
* @return void
* @operation add
**/
@RabbitListener(queues = "seckillQueue")
public void receive(String message) {
log.info("接收消息:" + message);
SeckillMessage seckillMessage = JsonUtil.jsonStr2Object(message, SeckillMessage.class);
Long goodsId = seckillMessage.getGoodsId();
TUser user = seckillMessage.getTUser();
//判断库存
GoodsVo goodsVo = itGoodsServicel.findGoodsVobyGoodsId(goodsId);
if (goodsVo.getStockCount() < 1) {
return;
}
//判断是否重复抢购
TSeckillOrder tSeckillOrder = (TSeckillOrder) redisTemplate.opsForValue().get("order:" + user.getId() + ":" + goodsId);
if (tSeckillOrder != null) {
return;
}
//下单操作
itOrderService.secKill(user, goodsVo);
}
}
给mq发送消息,返回信息:
SeckillMessage seckillMessag = new SeckillMessage(user, goodsId);
mqSender.sendSeckillMessage(JsonUtil.object2JsonStr(seckillMessag));
return RespBean.success(0);
附json工具类
package com.example.seckill.util;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.util.List;
/**
* Json工具类
*
*/
public class JsonUtil {
private static ObjectMapper objectMapper = new ObjectMapper();
/**
* 将对象转换成json字符串
*
* @param obj
* @return
*/
public static String object2JsonStr(Object obj) {
try {
return objectMapper.writeValueAsString(obj);
} catch (JsonProcessingException e) {
//打印异常信息
e.printStackTrace();
}
return null;
}
/**
* 将字符串转换为对象
*
* @param <T> 泛型
*/
public static <T> T jsonStr2Object(String jsonStr, Class<T> clazz) {
try {
return objectMapper.readValue(jsonStr.getBytes("UTF-8"), clazz);
} catch (JsonParseException e) {
e.printStackTrace();
} catch (JsonMappingException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
/**
* 将json数据转换成pojo对象list
* <p>Title: jsonToList</p>
* <p>Description: </p>
*
* @param jsonStr
* @param beanType
* @return
*/
public static <T> List<T> jsonToList(String jsonStr, Class<T> beanType) {
JavaType javaType = objectMapper.getTypeFactory().constructParametricType(List.class, beanType);
try {
List<T> list = objectMapper.readValue(jsonStr, javaType);
return list;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
redis优化
因为预减库存时,如果库存预减完了,再次查询数据库会返回负数,预减失败,再次的大量操作会增加redis消耗。
可以通过内存进行标记,减少redis压力。
在contoller中定义一个标记map
private Map<Long, Boolean> EmptyStockMap = new HashMap<>();
在系统初始化加载库存时,对商品进行标记:
list.forEach(goodsVo -> {
redisTemplate.opsForValue().set("seckillGoods:" + goodsVo.getId(), goodsVo.getStockCount());
//内存标记
EmptyStockMap.put(goodsVo.getId(), false);
});
库存为空时标记为true
预减库存前对内存标记进行判断:
//内存标记,减少Redis的访问
if (EmptyStockMap.get(goodsId)) {
return RespBean.error(RespBeanEnum.EMPTY_STOCK);
}
//预减库存
Long stock = valueOperations.decrement("seckillGoods:" + goodsId);
if (stock < 0) {
EmptyStockMap.put(goodsId, true);
valueOperations.increment("seckillGoods:" + goodsId);
return RespBean.error(RespBeanEnum.EMPTY_STOCK);
}
因为前面用mq发送消息后返回0,所以还需要查询是否下单成功。
客户端轮询秒杀结果
客户端查询结果:
function getResult(goodsId) {
g_showLoading();
$.ajax({
url: "/seckill/getResult",
type: "GET",
data: {
goodsId: goodsId
},
success: function (data) {
if (data.code == 200) {
var result = data.object;
if (result < 0) {
layer.msg("对不起,秒杀失败");
} else if (result == 0) {
setTimeout(function () {
getResult(goodsId)
});
} else {
layer.confirm("恭喜您,秒杀成功!查看订单?", {btn: ["确定", "取消"]},
function () {
window.location.href = "/orderDetail.html?orderId=" + result;
},
function () {
layer.close();
}
)
}
}
},
error: function () {
layer.msg("客户端请求错误");
}
});
}
controller增加获取结果方法
/**
* 获取秒杀结果
*
* @param tUser
* @param goodsId
* @return orderId 成功 ;-1 秒杀失败 ;0 排队中
* @operation add
**/
@ApiOperation("获取秒杀结果")
@GetMapping("getResult")
@ResponseBody
public RespBean getResult(TUser tUser, Long goodsId) {
if (tUser == null) {
return RespBean.error(RespBeanEnum.SESSION_ERROR);
}
Long orderId = itSeckillOrderService.getResult(tUser, goodsId);
return RespBean.success(orderId);
}
//service层的方法实现:
@Override
public Long getResult(TUser tUser, Long goodsId) {
TSeckillOrder tSeckillOrder = tSeckillOrderMapper.selectOne(new QueryWrapper<TSeckillOrder>().eq("user_id", tUser.getId()).eq("goods_id", goodsId));
if (null != tSeckillOrder) {
return tSeckillOrder.getOrderId();
} else if (redisTemplate.hasKey("isStockEmpty:" + goodsId)) {
return -1L;
} else {
return 0L;
}
}
上面的service层中,对redis商品为空的判断需要在下单逻辑中进行设置:
@Transactional
@Override
public TOrder secKill(TUser user, GoodsVo goodsVo) {
ValueOperations valueOperations = redisTemplate.opsForValue();
//减少库存
TSeckillGoods seckillGoods = itSeckillGoodsService.getOne(new QueryWrapper<TSeckillGoods>().eq("goods_id", goodsVo.getId()));
seckillGoods.setStockCount(seckillGoods.getStockCount() - 1);
boolean seckillGoodsResult = itSeckillGoodsService.update(new UpdateWrapper<TSeckillGoods>()
.setSql("stock_count = " + "stock_count-1")
.eq("goods_id", goodsVo.getId())
.gt("stock_count", 0)
);
if (seckillGoods.getStockCount() < 1) {
//判断是否还有库存
valueOperations.set("isStockEmpty:" + goodsVo.getId(), "0");
return null;
}
.............
redis分布式锁
redisson是实现分布式锁较好的redis客户端,但这里用redisTemplate测试下分布式锁:
f分布式锁的思想就是占位,当别的进程也要占位时发现已经被占位就会放弃或者稍后再试。(setnx)
@Test
public void testLock01() {
ValueOperations valueOperations = redisTemplate.opsForValue();
//占位,如果key不存在才可以设置成功
Boolean isLock = valueOperations.setIfAbsent("k1", "v1");
if (isLock) {
valueOperations.set("name", "xxx");
String name = (String) valueOperations.get("name");
System.out.println("name=" + name);
//操作结束,删除锁
redisTemplate.delete("k1");
} else {
System.out.println("有线程在使用,请稍后再试");
}
}
但这样如果前面的线程没有释放锁(发生异常等情况)就会使后面的线程无法获得锁。
简单方法:上锁时配置超时时间,超时会自动释放锁。
-> 新的问题:可能发生现有线程会将其他线程的锁误删(过期时间设置较短,但线程执行时间长,后面的线程的锁会被前面的线程删掉),造成一连串的问题。
->上锁时每个线程对自己的锁加上特殊标记(例如:每个线程key对应的value是一个独有的随机值),只有在判断是自己的锁的情况下才能删锁。 (自己的锁自己删)
->但这个过程是不连续的,有加锁,获取锁,比较锁,删除锁的过程
->使用乐观锁或者lua脚本
->lua脚本可以原子性的执行多个redis命令
例:
if redis.call("get",KEYS[1])==ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
//配置类中配置lua脚本的调用
@Bean
public DefaultRedisScript<Long> script() {
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
//lock.lua脚本位置和application.yml同级目录
redisScript.setLocation(new ClassPathResource("stock.lua"));
redisScript.setResultType(Long.class);
return redisScript;
}
test
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private RedisScript<Boolean> redisScript;
@Test
public void testLock3() {
ValueOperations valueOperations = redisTemplate.opsForValue();
String value = UUID.randomUUID().toString();
Boolean isLock = valueOperations.setIfAbsent("k1", value, 5, TimeUnit.SECONDS);
if (isLock) {
valueOperations.set("name", "xxx");
String name = (String) valueOperations.get("name");
System.out.println("name=" + name);
//操作结束,删除锁
System.out.println(valueOperations.get("k1"));
Boolean result = (Boolean) redisTemplate.execute(redisScript, Collections.singletonList("k1"), value);
System.out.println(result);
} else {
System.out.println("有线程在使用,请稍后再试");
}
}
这样将获取锁,比较锁,删除锁三个过程原子性的执行。
分布式锁优化预减库存
保障线程安全
lua:
if(redis.call('exists',KEYS[1])==1) then
local stock =tonumber(redis.call('get',KEYS[1]));
if(stock>0) then
redis.call('incrby',KEYS[1],-1);
return stock;
end;
return -1;
end;
controller中修改:
//内存标记,减少Redis的访问
if (EmptyStockMap.get(goodsId)) {
return RespBean.error(RespBeanEnum.EMPTY_STOCK);
}
//预减库存,所用lua脚本执行
// Long stock = valueOperations.decrement("seckillGoods:" + goodsId);
Long stock = (Long) redisTemplate.execute(redisScript, Collections.singletonList("seckillGoods:" + goodsId), Collections.EMPTY_LIST);
if (stock < 0) {
EmptyStockMap.put(goodsId, true);
// valueOperations.increment("seckillGoods:" + goodsId);
return RespBean.error(RespBeanEnum.EMPTY_STOCK);
}
SeckillMessage seckillMessag = new SeckillMessage(user, goodsId);
mqSender.sendSeckillMessage(JsonUtil.object2JsonStr(seckillMessag));
return RespBean.success(0);
在订单处理模块,写入数据库时是不安全的,可以加入分布式锁(redissonClient).
安全优化
秒杀接口地址隐藏
前端获取秒杀路径
function getSeckillPath() {
var goodsId = $("#goodsId").val();
//验证码
var captcha = $("#captcha").val();
g_showLoading();
$.ajax({
url: "/seckill/path",
type: "GET",
data: {
goodsId: goodsId,
captcha:captcha
},
success: function (data) {
if (data.code == 200) {
var path = data.object;
doSecKill(path);
} else {
layer.msg(data.message);
}
},
error: function () {
layer.msg("客户端请求错误");
}
});
}
//秒杀方法
function doSecKill(path) {
$.ajax({
url: 'seckill/' + path + '/doSeckill',
type: "POST",
data: {
goodsId: $('#goodsId').val()
},
success: function (data) {
if (data.code == 200) {
// window.location.href="/orderDetail.html?orderId="+data.object.id;
getResult($("#goodsId").val());
} else {
layer.msg(data.message);
}
}, error: function () {
layer.msg("客户端请求出错");
}
});
}
控制层(seckillController)
@ApiOperation("获取秒杀地址")
@AccessLimit(second = 5, maxCount = 5, needLogin = true)
@GetMapping(value = "/path")
@ResponseBody
public RespBean getPath(TUser tuser, Long goodsId, String captcha, HttpServletRequest request) {
if (tuser == null) {
return RespBean.error(RespBeanEnum.SESSION_ERROR);
}
//校验验证码
boolean check = orderService.checkCaptcha(tuser, goodsId, captcha);
if (!check) {
return RespBean.error(RespBeanEnum.ERROR_CAPTCHA);
}
String str = orderService.createPath(tuser, goodsId);
return RespBean.success(str);
}
------------------------------------------------------------------------------------
//秒杀方法中加入随机path
@ApiOperation("秒杀功能")
@RequestMapping(value = "/{path}/doSeckill", method = RequestMethod.POST)
@ResponseBody
public RespBean doSecKill(@PathVariable String path, TUser user, Long goodsId) {
.............
boolean check = orderService.checkPath(user, goodsId, path);
if (!check) {
//请求非法
return RespBean.error(RespBeanEnum.REQUEST_ILLEGAL);
}
.............
}
Service层
//获取秒杀路径
//uuid生成唯一的秒杀地址
@Override
public String createPath(TUser user, Long goodsId) {
String str = MD5Util.md5(UUIDUtil.uuid() + "123456");
redisTemplate.opsForValue().set("seckillPath:" + user.getId() + ":" + goodsId, str, 1, TimeUnit.MINUTES);
return str;
}
-----------------------------------------------
//校验秒杀地址
@Override
public boolean checkPath(TUser user, Long goodsId, String path) {
if (user == null || goodsId < 0 || StringUtils.isEmpty(path)) {
return false;
}
String redisPath = (String) redisTemplate.opsForValue().get("seckillPath:" + user.getId() + ":" + goodsId);
return path.equals(redisPath);
}
验证码
作用:防止脚本;拉长短时间内的并发时间长度。
(前端省略)
可以在码云等找开源的工具:
导入依赖:
<!--验证码依赖-->
<dependency>
<groupId>com.github.whvcse</groupId>
<artifactId>easy-captcha</artifactId>
<version>1.6.2</version>
</dependency>
获取验证码
@ApiOperation("获取验证码")
@GetMapping(value = "/captcha")
public void verifyCode(TUser tUser, Long goodsId, HttpServletResponse response) {
if (tUser == null || goodsId < 0) {
throw new GlobalException(RespBeanEnum.REQUEST_ILLEGAL);
}
//设置请求头为输出图片的类型
response.setContentType("image/jpg");
response.setHeader("Pargam", "No-cache");
// 不需要缓存,确保每次获取的验证码不重复
response.setHeader("Cache-Control", "no-cache");
response.setDateHeader("Expires", 0);
//生成验证码
ArithmeticCaptcha captcha = new ArithmeticCaptcha(130, 32, 3);
redisTemplate.opsForValue().set("captcha:" + tUser.getId() + ":" + goodsId, captcha.text(), 300, TimeUnit.SECONDS);
try {
captcha.out(response.getOutputStream());
} catch (IOException e) {
log.error("验证码生成失败", e.getMessage());
}
}
校验验证码
传递一个验证码进去进行校验
// getPath方法
boolean check = orderService.checkCaptcha(tuser, goodsId, captcha);
if (!check) {
return RespBean.error(RespBeanEnum.ERROR_CAPTCHA);
}
//service层
@Override
public boolean checkCaptcha(TUser user, Long goodsId, String captcha) {
if (user == null || goodsId < 0 || StringUtils.isEmpty(captcha)) {
return false;
}
String redisCaptcha = (String) redisTemplate.opsForValue().get("captcha:" + user.getId() + ":" + goodsId);
return captcha.equals(redisCaptcha);
}
接口限流
常见的限流算法:计数器,漏斗,令牌桶
限流规则为设置为最大可承受qps的70%-80%
令牌桶(队列机制)容易造成请求失效
计数器方法:
@ApiOperation("获取秒杀地址")
@AccessLimit(second = 5, maxCount = 5, needLogin = true)
@GetMapping(value = "/path")
@ResponseBody
public RespBean getPath(TUser tuser, Long goodsId, String captcha, HttpServletRequest request) {
if (tuser == null) {
return RespBean.error(RespBeanEnum.SESSION_ERROR);
}
ValueOperations valueOperations = redisTemplate.opsForValue();
//限制访问次数,5秒内访问5次
String uri = request.getRequestURI();
//默认一个验证码
captcha = "0";
Integer count = (Integer) valueOperations.get(uri + ":" + tuser.getId());
if (count == null) {
valueOperations.set(uri + ":" + tuser.getId(), 1, 5, TimeUnit.SECONDS);
} else if (count < 5) {
valueOperations.increment(uri + ":" + tuser.getId());
} else {
return RespBean.error(RespBeanEnum.ACCESS_LIMIT_REACHED);
}
boolean check = orderService.checkCaptcha(tuser, goodsId, captcha);
if (!check) {
return RespBean.error(RespBeanEnum.ERROR_CAPTCHA);
}
String str = orderService.createPath(tuser, goodsId);
return RespBean.success(str);
}
优化针对不同的接口有不同的请求限流规则,所以需要通用的接口限流。
自定义注解实现(aop)
/**
* 自定义注解
* @ClassName: AccessLimit
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface AccessLimit {
//每秒最大访问量
int second();
int maxCount();
//是否需要登录
boolean needLogin() default true;
}
拦截器
/**
* @ClassName: AccessLimitInterceptor
*/
@Component
public class AccessLimitInterceptor implements HandlerInterceptor {
@Autowired
private ITUserService itUserService;
@Autowired
private RedisTemplate redisTemplate;
//执行之前
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (handler instanceof HandlerMethod) {
TUser tUser = getUser(request, response);
//将用户放入Threadlocal
UserContext.setUser(tUser);
HandlerMethod hm = (HandlerMethod) handler;
//获取方法上的注解
AccessLimit accessLimit = hm.getMethodAnnotation(AccessLimit.class);
if (accessLimit == null) {
return true;
}
int second = accessLimit.second();
int maxCount = accessLimit.maxCount();
boolean needLogin = accessLimit.needLogin();
String key = request.getRequestURI();
if (needLogin) {
if (tUser == null) {
render(response, RespBeanEnum.SESSION_ERROR);
}
key += ":" + tUser.getId();
}
ValueOperations valueOperations = redisTemplate.opsForValue();
Integer count = (Integer) valueOperations.get(key);
if (count == null) {
valueOperations.set(key, 1, second, TimeUnit.SECONDS);
} else if (count < maxCount) {
valueOperations.increment(key);
} else {
render(response, RespBeanEnum.ACCESS_LIMIT_REACHED);
return false;
}
}
return true;
}
//构建返回对象
private void render(HttpServletResponse response, RespBeanEnum respBeanEnum) throws IOException {
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json");
PrintWriter printWriter = response.getWriter();
RespBean bean = RespBean.error(respBeanEnum);
printWriter.write(new ObjectMapper().writeValueAsString(bean));
printWriter.flush();
printWriter.close();
}
private TUser getUser(HttpServletRequest request, HttpServletResponse response) {
String userTicket = CookieUtil.getCookieValue(request, "userTicket");
if (StringUtils.isEmpty(userTicket)) {
return null;
}
return itUserService.getUserByCookie(userTicket, request, response);
}
}
拦截器中使用TreadLocal存储用户信息,在之后的需求中就可以直接用UserContext调用,防止线程冲突(线程私有数据)
mvconfig中添加拦截器:
@Autowired
private AccessLimitInterceptor accessLimitInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(accessLimitInterceptor);
}
mm秒杀controller上添加注解:
@AccessLimit(second = 5, maxCount = 5, needLogin = true)