缓存预热,秒杀商品设置到Redis中,同时提供静态页面给用户使用
@RestController
@RequestMapping("/seckill")
@Slf4j
public class SeckillController {
@Resource
private RedisTemplate redisTemplate;
@Autowired
private ProductService productService;
@Autowired
private OrderService orderService;
@Resource
private KafkaTemplate<String, String> kafkaTemplate;
@Autowired
private RedisClient redisClient;
@Autowired
private DefaultRedisScript<Long> defaultRedisScript;
/**
* 秒杀商品设置到Redis中,同时提供静态页面给用户使用
* @author fan
* @date 2022/5/9 17:56
* @return java.util.List<com.fan.li.entity.Product>
*/
@RequestMapping(value = "/queryAll")
@ResponseBody
//@MyAcessLimter(count = 1000,timeout = 1)
public List<Product> queryAll(@RequestParam("seckillDate") String seckillDate) {
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
try {
ObjectMapper objectMapper = new ObjectMapper();
List<Product> productList = null;
String key = "userList_plan_all";
boolean hasKey = redisTemplate.hasKey(key);//判断redis中是否有键为key的缓存
if (hasKey) {
productList = redisClient.getStringList(key, 0, -1);
log.info("redis的数据list.size()-->" + productList.size());
} else {
productList = productService.queryAll();
redisClient.setStringList(key, productList);//把商品信息存入缓存,列表展示用
//list = userService.findPage(cp,ps);
redisTemplate.expire(key, 1_000 * 60 * 60, TimeUnit.MILLISECONDS);//设置过期时间1个小时
log.info("mysql的数据list.size()-->" + productList.size());
}
if (productList == null) {
return null;
}
for (Product product : productList) {
Long productId = product.getId();
redisTemplate.opsForValue().set("product_" + productId, objectMapper.writeValueAsString(product));
// 一个用户只买一件商品
// 商品购买用户Set
redisTemplate.opsForSet().add("product_buyers_" + product.getId(), "");
for (int i = 0; i < product.getStock(); i++) {
redisTemplate.opsForList().leftPush("product_stock_key_" + product.getId(), String.valueOf(i));
}
log.info("[queryAll()] 商品product:" + objectMapper.writeValueAsString(product));
}
redisTemplate.opsForValue().set("seckill_plan_" + seckillDate, objectMapper.writeValueAsString(productList));//把商品信息存入缓存,列表展示用
return productList;
}catch (Exception e){
e.printStackTrace();
}
return null;
}
/**
* 开始秒杀商品,并削峰限流
* @author fan
* @date 2022/5/10 1:03
* @param userId
* @param productId
* @return java.lang.String
*/
@RequestMapping(value = "/seckillProduct")
//@PostMapping(value = "seckillProduct")
@ResponseBody
@MyAcessLimter(count = 100,timeout = 1)
public synchronized String seckillProduct( String userId, String productId) throws JsonProcessingException {
List<String> list = Lists.newArrayList("product_stock_key_" + productId , "product_buyers_" + productId , userId );
Long code = (Long) redisTemplate.execute(defaultRedisScript, list, "");
if (code == -1) {
return "库存不足";
} else if (code == 2) {
return "不允许重复秒杀";
} else if (code == 1) {//整个秒杀过程在缓存中进行,秒杀结束后从缓存中拿数据库加入队列同步到数据库中
ObjectMapper objectMapper = new ObjectMapper();
String productJson = (String) redisTemplate.opsForValue().get("product_" + productId);
Gson gson = new Gson();
Product product = objectMapper.readValue(productJson , Product.class);
//gson.fromJson(productJson , Product.class);
Order order = new Order();
order.setProductId(Long.parseLong(productId));
order.setUserId(Long.parseLong(userId));
String id = String.valueOf(UUID.randomUUID());
order.setId(id);
order.setOrderName("抢购" + product.getName());
order.setProductName(product.getName());
MessageObject messageObject = new MessageObject(order, product);//MessageObject中status为1表示定时任务要处理的数据
//redisClient.setString("messageObject_" + productId, gson.toJson(messageObject));
// 把要发送的数据messageObject放到同一个表中,这里演示就放到Redis
String message = objectMapper.writeValueAsString(messageObject);
redisClient.putHash("messageObject_" , productId + "_" + userId , message);
kafkaTemplate.send("seckill_order", JSONUtil.objToString(order));
return "sueccss";
}
return "error";
}
}
消费者
@Component
@Slf4j
public class OrderSeckillConsumer {
@Autowired
private RedisClient redisClient;
@Autowired
private OrderService orderService;
@Autowired
private ProductService productService;
private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
@KafkaListener(topics = "seckill_order")
public void run(ConsumerRecord<?, ?> record){
Optional<?> kafkaMessage = Optional.ofNullable(record.value());
try {
if (kafkaMessage.isPresent()) {
String message = (String) kafkaMessage.get();
log.info("[run()-->]message: {}", message);
ObjectMapper objectMapper = new ObjectMapper();
Order order = objectMapper.readValue(message, Order.class);
if (order != null) { //先调通知成功的接口给用户后对订单数据入库
List<Order> orderList = orderService.selectOne(order.getId());
if (!CollectionUtils.isEmpty(orderList)){ // 防止消息重复消费,保证幂等性(存个key到Redis指定过期时间也可以,但要保证是同一条消息)
log.info("[该订单已存在]");
return;
}
log.info("当前时间:" + sdf.format(new Date()));
Long productId = order.getProductId();
int p = productService.updateProduct(productId);//开启事务
if (p > 0) {
int i = orderService.saveOrder(order);//开启事务
if (i > 0){
//日志写入略
String productJson = (String) redisClient.getString("product_" + productId);//product_
Product product = objectMapper.readValue(productJson, Product.class);
//Product product = productService.selectById(productId);
MessageObject messageObject = new MessageObject(order, product);//MessageObject中status为1表示定时任务要处理的数据
messageObject.setStatus("0");
String msg = objectMapper.writeValueAsString(messageObject);
redisClient.putHash("messageObject_" , productId + "_" + order.getUserId() , msg);//把状态更新回缓存(表)
}
}
}
}
} catch (Exception e){
e.printStackTrace();
log.error(e.getMessage());
}
}
}
定时任务
@Component
@Slf4j
public class MyKafkaTask {
private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
@Resource
private KafkaTemplate<String, String> kafkaTemplate;
@Autowired
private ProductService productService;
@Autowired
private OrderService orderService;
@Autowired
private RedisClient redisClient;
@Autowired
KafkaProducers kafkaProducers;
//@Scheduled(cron = "0/30 * * * * ?")
@Scheduled(initialDelay=2000, fixedRate=60000)
public void excuteTask() throws JsonProcessingException {
List<String> keySet = redisClient.getHash("messageObject_");
if (keySet != null && keySet.size() > 0) {
ObjectMapper objectMapper = new ObjectMapper();
for (String jonsonObjStr : keySet) {
MessageObject messageObject = objectMapper.readValue(jonsonObjStr , MessageObject.class);
String status = messageObject.getStatus();
if (!"1".equals(status) && !StringUtils.isEmpty(status)){
continue;
}
String id = messageObject.getId();
Long userId = messageObject.getUserId();
Long productId = messageObject.getProductId();
String productName = messageObject.getProductName();
String orderName = messageObject.getOrderName();
Order order = new Order();
order.setProductId(productId);
order.setUserId(userId);
order.setId(id);
order.setOrderName(orderName);
order.setProductName(productName);
List<Order> orderList = orderService.selectOne(id);
if (!CollectionUtils.isEmpty(orderList)){ // 防止消息重复消费,保证幂等性(存个key到Redis指定过期时间也可以,但要保证是同一条消息)
log.info("[该订单已存在] date={}" , sdf.format(new Date()));
continue;
}
Integer i = orderService.saveOrder(order);
Product product = new Product();
String name = messageObject.getName();
Integer stock = messageObject.getStock();
Date creatTime = messageObject.getCreatTime();
Date startTime = messageObject.getStartTime();
Date endTime = messageObject.getEndTime();
product.setId(productId);
product.setStock(stock);
product.setName(name);
product.setStartTime(startTime);
product.setEndTime(endTime);
product.setCreatTime(creatTime);
int p = productService.updateProduct(productId);
if (p > 0 && i > 0){ //更新到Redis(表)
messageObject.setStatus("0");
String message = objectMapper.writeValueAsString(messageObject);
redisClient.putHash("messageObject_" , productId + "_" + userId , message);//把状态更新回缓存
kafkaTemplate.send("seckill_order", JSONUtil.objToString(order));//重新发送到队列
}
}
}
}
}
限流开始
@Component
@Aspect
@Slf4j
public class MyAcessLimiterAspect {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Resource(name = "ipLimitLua")
private DefaultRedisScript<Boolean> ipLimitLua;
@Resource
StringRedisTemplate stringRedisTemplate;
// 1: 切入点 创建的注解类
@Pointcut("@annotation(com.fan.li.myspringboot.limit.MyAcessLimter)")
public void myLimiterPonicut() {
}
@Before("myLimiterPonicut()")
public void limiter(JoinPoint joinPoint) {
log.info("限流进来了......." + LocalDate.now());
// 1:获取方法的签名作为key
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();
String classname = methodSignature.getMethod().getDeclaringClass().getName();
String packageName = methodSignature.getMethod().getDeclaringClass().getPackage().getName();
log.info("method:{},classname:{},packageName:{}",method,classname,packageName);
// 4: 读取方法的注解信息获取限流参数
MyAcessLimter annotation = method.getAnnotation(MyAcessLimter.class);
// 5:获取注解方法名
String methodNameKey = method.getName();
log.info("获取注解方法名:{}" , methodNameKey);
// 6:获取服务请求的对象
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = requestAttributes.getRequest();
HttpServletResponse response = requestAttributes.getResponse();
String userIp = MyIPUtils.getIpAddr(request);
log.info("用户IP是:.......{}" , userIp);
// 7:通过方法反射获取注解的参数
Integer count = annotation.count();
Integer timeout = annotation.timeout();
/*Object[] args = joinPoint.getArgs();
for (int i = 0; i < args.length; i++) {
log.info("参数id--> " + request.getParameter("userId") + "---" + args[i]);
}*/
String redisKey = userIp;
log.info("当前的key-->" + redisKey);
// 8: 请求lua脚本
Boolean acquired = stringRedisTemplate.execute(ipLimitLua, Lists.newArrayList(redisKey) , count.toString() , timeout.toString());
// 如果超过限流限制
if (!acquired) {
// 抛出异常,然后让全局异常去处理
response.setCharacterEncoding("UTF-8");
response.setContentType("text/html;charset=UTF-8");
try (PrintWriter writer = response.getWriter()) {
writer.print("<h1>操作频繁,请稍后在试</h1>");
} catch (Exception ex) {
throw new RuntimeException("操作频繁,请稍后在试");
}
}
}
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface MyAcessLimter {
/**
* 限流唯一标示
* @author fan
* @date 2022/5/7 1:55
* @return java.lang.String
*/
String key() default "";
/**
* 每timeout限制请求的个数
* @author fan
* @date 2022/5/7 1:54
* @return int
*/
int count() default 5;
/**
* 超时时间,单位默认是秒
* @author fan
* @date 2022/5/7 1:54
* @return int
*/
int timeout() default 10;
/**
* 访问间隔
* @author fan
* @date 2022/5/7 1:54
* @return int
*/
int waits() default 20;
}
配置类
@Configuration
public class MyLuaConfiguration {
/**
* 将IP-lua脚本的内容加载出来放入到DefaultRedisScript
* @author fan
* @date 2022/5/7 9:35
* @return org.springframework.data.redis.core.script.DefaultRedisScript<java.lang.Boolean>
*/
@Bean(name = "ipLimitLua")
public DefaultRedisScript<Boolean> ipLimitLua() {
DefaultRedisScript<Boolean> defaultRedisScript = new DefaultRedisScript<>();
defaultRedisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("myLimit_ip.lua")));
defaultRedisScript.setResultType(Boolean.class);
return defaultRedisScript;
}
/**
* 将秒杀lua脚本的内容加载出来放入到DefaultRedisScript
* @return
*/
@Bean
public DefaultRedisScript<Boolean> seckillLimiterLuaScript() {
DefaultRedisScript<Boolean> defaultRedisScript = new DefaultRedisScript<>();
defaultRedisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("seckill.lua")));
defaultRedisScript.setResultType(Boolean.class);
return defaultRedisScript;
}
@Bean
public DefaultRedisScript<Long> seckillLimiterLuaScript2() {
DefaultRedisScript<Long> defaultRedisScript = new DefaultRedisScript<Long>();
defaultRedisScript.setResultType(Long.class);
defaultRedisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("buyone.lua")));
return defaultRedisScript;
}
}
订单接口略
buyone.lue
--商品库存Key
local product_stock_key = KEYS[1]
--商品购买用户记录Key
local buyersKey = KEYS[2]
--用户ID
local uid = KEYS[3]
--校验用户是否已经购买
local result = redis.call("sadd" , buyersKey , uid )
if(tonumber(result) == 1)
then
--没有购买过,可以购买
local stock = redis.call("lpop" , product_stock_key )
--除了nil和false,其他值都是真(包括0)
if(stock)
then
--有库存
return 1
else
--没有库存
return -1
end
else
--已经购买过
return 2
end
myLimit_ip.lua
-- 为某个接口的请求IP设置计数器,比如:127.0.0.1请求课程接口
-- KEYS[1] = 127.0.0.1 也就是用户的IP
-- ARGV[1] = 过期时间 30m
-- ARGV[2] = 限制的次数
local count = redis.call('incr',KEYS[1]);
if count == 1 then
redis.call("expire",KEYS[1],ARGV[2])
end
-- 如果次数还没有过期,并且还在规定的次数内,说明还在请求同一接口
if count > tonumber(ARGV[1]) then
return false
end
return true
seckill.lua
--商品库存Key product_one_stock_XXX 仅售一件
local product_id = KEYS[1]
--用户ID
local user_id = ARGV[1]
-- 商品库存key
local product_stock_key = 'seckill:{' .. product_id .. '}:stock'
-- 商品秒杀结束标识的key
local end_product_key = 'seckill:{' .. product_id .. '}:end'
-- 存储秒杀成功的用户id的集合的key
local bought_users_key = 'seckill:{' .. product_id .. '}:uids'
--判断该商品是否秒杀结束
local is_end = redis.call('get',product_stock_key)
if is_end and tonumber(is_end) ~=1 then
return -2
end
-- 判断用户是否秒杀过
local is_in = redis.call('sismember',bought_users_key,user_id)
if is_in > 0 then
return 0
end
-- 获取商品当前库存
local stock = redis.call('get',product_stock_key)
-- 如果库存<=0,则返回-1
if not stock or tonumber(stock) <=0
then
redis.call("set",end_product_key,"1")
return -1
end
-- 减库存,并且把用户的id添加进已购买用户set里
redis.call("decr",product_stock_key)
redis.call("sadd",bought_users_key,user_id)
return 1