思路:对于普通商品进行正常下单(直接减去数据库中的库存然后生成订单,对于热点数据加一层redis缓存层,商品放在redis中,用分布式锁加锁,然后可以放入消息队列中排队下单,下单时先检查Redis中库存量是否足够,成功了才减去数据库库存)
在这里还使用了feign用来远程调用服务和网关拦截请求。
下面是代码:/
配置文件和通用文件
@Configuration
public class RedisConfig {
/***
* 模板操作对象序列化设置
* @param redissonConnectionFactory
* @return
*/
@Bean("redisTemplate")
public RedisTemplate getRedisTemplate(RedisConnectionFactory redissonConnectionFactory) {
RedisTemplate<Object, Object> redisTemplate = new RedisTemplate();
redisTemplate.setConnectionFactory(redissonConnectionFactory);
redisTemplate.setValueSerializer(valueSerializer());
redisTemplate.setKeySerializer(keySerializer());
redisTemplate.setHashKeySerializer(keySerializer());
redisTemplate.setHashValueSerializer(valueSerializer());
return redisTemplate;
}
/****
* 序列化设置
* @return
*/
@Bean
public StringRedisSerializer keySerializer() {
return new StringRedisSerializer();
}
/****
* 序列化设置
* @return
*/
@Bean
public RedisSerializer valueSerializer() {
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
return jackson2JsonRedisSerializer;
}
}
通用结果类型:
@ApiModel(description = "Result",value = "Result")
public class Result<T> {
@ApiModelProperty(value="执行是否成功,true:成功,false:失败",required = true)
private boolean flag;//是否成功
@ApiModelProperty(value="返回状态码,20000:成功,20001:失败,20002:用户名或密码错误,20003:权限不足,20004:远程调用失败,20005:重复操作,20006:没有对应的数据",required = true)
private Integer code;//返回码
@ApiModelProperty(value="提示信息",required = true)
private String message;//返回消息
@ApiModelProperty(value="逻辑数据",required = true)
private T data;//返回数据
public Result(boolean flag, Integer code, String message, Object data) {
this.flag = flag;
this.code = code;
this.message = message;
this.data = (T) data;
}
public Result(boolean flag, Integer code, String message) {
this.flag = flag;
this.code = code;
this.message = message;
}
public Result() {
this.flag = true;
this.code = StatusCode.OK;
this.message = "操作成功!";
}
返回码:
/**
* 返回码
*/
public class StatusCode {
public static final int OK = 20000;//成功
public static final int ERROR = 20001;//失败
public static final int LOGINERROR = 20002;//用户名或密码错误
public static final int ACCESSERROR = 20003;//权限不足
public static final int REMOTEERROR = 20004;//远程调用失败
public static final int REPERROR = 20005;//重复操作
public static final int NOTFOUNDERROR = 20006;//没有对应的抢购数据
//库存递减状态码
public static final int DECOUNT_1=1; //递减成功
public static final int DECOUNT_NUM=405;//库存不足
public static final int DECOUNT_HOT=205;//商品是热卖商品
public static final int DECOUNT_OK=200;//库存递减成功
public static final int ORDER_QUEUE=202;//抢购商品正在排队
public static final int ORDER_OK=200;//抢单成功
//令牌无效
public static final int TOKEN_ERROR=401;
}
统一异常处理:
@ControllerAdvice //所有请求路径,都将被该类处理->过滤器/(拦截器)
public class BaseExceptionHandler {
private static Logger logger = LoggerFactory.getLogger(BaseExceptionHandler.class);
/***
* 异常处理
* 当前请求发生了指定异常,则执行该方法处理异常
*/
@ExceptionHandler(Exception.class)
@ResponseBody
public Result error(Exception ex){
StringWriter stringWriter = new StringWriter();
PrintWriter writer = new PrintWriter(stringWriter);
ex.printStackTrace(writer);
ex.printStackTrace();
logger.error(stringWriter.toString());
return new Result(false, StatusCode.ERROR,ex.getMessage(),stringWriter.toString());
}
}
网关
server:
port: 8001
spring:
application:
name: gateway-web
cloud:
nacos:
config:
file-extension: yaml
server-addr: nacos-server:8848
discovery:
#Nacos的注册地址
server-addr: nacos-server:8848
gateway:
routes:
#商品
- id: goods_route
uri: lb://seckill-goods
predicates:
- Path=/api/skuAct/**,/api/activity/**,/api/brand/**,/api/category/**,/api/seckillTime/**,/api/sku/**
filters:
- StripPrefix=1
#订单
- id: order_route
uri: lb://seckill-order
predicates:
- Path=/api/order/**
filters:
- StripPrefix=1
#搜索
- id: search_route
uri: lb://seckill-search
predicates:
- Path=/api/search/**
filters:
- StripPrefix=1
#用户
- id: user_route
uri: lb://seckill-user
predicates:
- Path=/api/user/**
filters:
- StripPrefix=1
#管理员
- id: manager_route
uri: lb://seckill-manager
predicates:
- Path=/api/admin/**
filters:
- StripPrefix=1
#静态页
- id: page_route
uri: lb://seckill-page
predicates:
- Path=/api/page/**
filters:
- StripPrefix=1
网关拦截设置:
@Component
public class AuthorizeFilter implements GlobalFilter,Ordered {
//令牌头名字
private static final String AUTHORIZE_TOKEN = "Authorization";
private static final String ADMINAUTHORIZE_TOKEN = "Admin-Token-Itheima";
/***
* 过滤拦截
* @param exchange
* @param chain
* @return
*/
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//获取request和response
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
//获取用户请求的地址
String path = request.getURI().getPath();
// /api/user/login放行
///api/order/add 测试放行
if(path.equals("/api/user/login") || path.equals("/api/admin/login") || path.equals("/api/search") || path.equals("/api/activity/times")){
//放行
return chain.filter(exchange);
}
HttpMethod method = request.getMethod();
System.out.println(method.name());
// /sku/xxx GET方式允许通过
if(path.startsWith("/api/sku/") && request.getMethod().name().equalsIgnoreCase("GET")){
//放行
return chain.filter(exchange);
}
//获取用户请求头中的令牌
String token = request.getHeaders().getFirst(AUTHORIZE_TOKEN); //获取请求头中第1个Authorization参数
//如果请求头中没有令牌,则有可能用的是参数传入的
if(StringUtils.isEmpty(token)){
token = request.getQueryParams().getFirst(AUTHORIZE_TOKEN);//获取请求参数中第1个Authorization
}
//如果请求头和参数中都没有令牌,则直接拒绝用户访问各大微服务
if(StringUtils.isEmpty(token)){
//从Cookie中获取令牌数据
HttpCookie cookie = request.getCookies().getFirst(AUTHORIZE_TOKEN);
HttpCookie adminCookie = request.getCookies().getFirst(ADMINAUTHORIZE_TOKEN);
if(cookie==null && adminCookie==null){
//状态吗 401
response.setStatusCode(HttpStatus.UNAUTHORIZED);
//结束当前请求
return response.setComplete();
}
//获取令牌
if(cookie!=null){
token = cookie.getValue();
}else{
token = adminCookie.getValue();
}
//将令牌封装到请求头中
request.mutate().header(AUTHORIZE_TOKEN,"bearer "+token);
}
return chain.filter(exchange);
}
/***
* 排序
* @return
*/
@Override
public int getOrder() {
return 0;
}
@Bean
public CorsWebFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
// cookie跨域
config.setAllowCredentials(Boolean.TRUE); //允许Cookie跨域
config.addAllowedMethod("*"); //所有提交方法都允许跨域
config.addAllowedOrigin("*"); //所有的域名都允许跨域
config.addAllowedHeader("*");
//跨域解析器
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(new PathPatternParser());
source.registerCorsConfiguration("/**", config); //所有请求路径都支持跨域
return new CorsWebFilter(source);
}
}
feign服务
下面是库存的接口:
@FeignClient(value = "seckill-goods")
public interface SkuFeign {
/****
* 库存递减
*/
@PutMapping(value = "/sku/dcount/{id}/{count}")
Result<Sku> dcount(@PathVariable(value = "id")String id,@PathVariable(value = "count")Integer count);
/***
* 热点商品隔离
*/
@PostMapping(value = "/sku/hot/isolation")
Result hotIsolation(@RequestParam List<String> ids);
/****
* 分页查询-查询总数量
*/
@GetMapping(value = "/sku/count")
Integer count();
/****
* 分页查询集合列表
*/
@GetMapping(value = "/sku/list/{page}/{size}")
List<Sku> list(@PathVariable(value = "page")Integer page,@PathVariable(value = "size")Integer size);
/***
* 根据ID查询Sku数据
* @param id
* @return
*/
@GetMapping("/sku/{id}")
Result<Sku> findById(@PathVariable String id);
/***
* 商品数据归0
*/
@GetMapping(value = "/sku/zero/{id}")
Result zero(@PathVariable(value = "id") String id);
}
消息通知的接口:
@FeignClient(value = "seckill-message")
public interface MessageFeign {
/****
* 发送消息
*/
@GetMapping(value = "/message/{userid}")
String send(@PathVariable(value = "userid")String userid,@RequestParam(value = "msg") String msg) throws IOException;
}
redission配置
多个用户实现加锁操作,只允许有一个用户可以获取到对应锁
@Component
public class RedissonDistributedLocker implements DistributedLocker {
@Autowired
private RedissonClient redissonClient;
/***
* 加锁,会一直循环加锁,直到拿到锁
* @param lockkey
* @return
*/
@Override
public RLock lock(String lockkey) {
RLock lock = redissonClient.getLock(lockkey);
lock.lock();
return lock;
}
/***
* 加锁,在指定时间内拿不到锁就会放弃
* @param lockkey
* @return
*/
@Override
public RLock lock(String lockkey, long timeout) {
RLock lock = redissonClient.getLock(lockkey);
lock.lock(timeout,TimeUnit.SECONDS);
return lock;
}
/***
* 加锁,在指定时间内拿不到锁就会放弃
* @param lockkey
* @return
*/
@Override
public RLock lock(String lockkey, long timeout, TimeUnit unit) {
return null;
}
/***
* 加锁,在指定时间内拿不到锁就会放弃,如果拿到锁,锁最终有效时间为leasetime
* @param lockkey
* @return
*/
@Override
public boolean tryLock(String lockkey, long timeout, long leasetime, TimeUnit unit) {
return false;
}
/****
* 解锁
* @param lockkey
*/
@Override
public void unLock(String lockkey) {
RLock lock = redissonClient.getLock(lockkey);
lock.unlock();
}
/***
* 解锁
* @param lock
*/
@Override
public void unLocke(RLock lock) {
lock.unlock();
}
}
controller
商品SkuController
@RestController
@RequestMapping("/sku")
public class SkuController {
@Autowired
private SkuService skuService;
/****
* 库存递减
*/
@PutMapping(value = "/dcount/{id}/{count}")
public Result<Sku> dcount(@PathVariable(value = "id")String id,@PathVariable(value = "count")Integer count){
//1.调用业务层实现递减
int code = skuService.dcount(id, count);
String message="";
Sku sku = null;
switch (code){
case StatusCode.DECOUNT_OK:
sku = skuService.findById(id);
message="库存递减成功!";
break;
case StatusCode.DECOUNT_NUM:
message="库存不足!";
break;
case StatusCode.DECOUNT_HOT:
message="商品是热点商品!";
break;
default:
}
//3.根据状态码,响应不同的提示信息
return new Result<Sku>(true,code,message,sku);
}
/***
* 热点商品隔离
*/
@PostMapping(value = "/hot/isolation")
public Result hotIsolation(@RequestParam List<String> ids){
for (String id : ids) {
skuService.hotIsolation(id);
}
return new Result(true,StatusCode.OK,"热点数据隔离成功!");
}
订单OrderController:
@RestController
@RequestMapping("/order")
//@CrossOrigin
public class OrderController {
@Autowired
private OrderService orderService;
@Autowired
private IdWorker idWorker;
/****
* 添加订单
*/
@PostMapping(value = "/add/{id}")
public Result add(@PathVariable(value = "id") String id, @RequestHeader(value = "Authorization") String authorization) {
String username = null;
try {
//解析令牌
Map<String, Object> tokenMap = JwtTokenUtil.parseToken(authorization);
username =tokenMap.get("username").toString();
} catch (Exception e) {
return new Result(false, StatusCode.TOKEN_ERROR, "令牌无效!");
}
//封装Order
Order order = new Order();
order.setId("No"+idWorker.nextId());
order.setSkuId(id);
order.setCreateTime(new Date());
order.setUpdateTime(order.getCreateTime());
order.setUsername(username);
order.setTotalNum(1);
//添加订单
int code = orderService.add(order);
switch (code) {
case StatusCode.ORDER_OK:
return new Result(true, StatusCode.ORDER_OK, order.getId());
case StatusCode.DECOUNT_NUM:
return new Result(false, StatusCode.DECOUNT_NUM, "库存不足!");
case StatusCode.ORDER_QUEUE:
return new Result(true, StatusCode.ORDER_QUEUE, "排队抢购中!");
default:
return new Result(false, StatusCode.ERROR, "抢单发生异常!");
}
}
/***
* Order分页条件搜索实现
* @param page
* @param size
* @return
*/
@PostMapping(value = "/search/{page}/{size}")
public Result<PageInfo> findPage(@RequestBody(required = false) OrderVo orderVo, @PathVariable int page, @PathVariable int size) {
//调用OrderService实现分页条件查询Order
Order order = new Order();
BeanUtils.copyProperties(orderVo,order);
PageInfo<Order> pageInfo = orderService.findPage(order, page, size);
return new Result(true, StatusCode.OK, "查询成功", pageInfo);
}
/***
* 用户订单
* @param page
* @param size
* @return
*/
@GetMapping(value = "/user/{page}/{size}")
public Result<PageInfo> userOrders(@PathVariable int page,
@PathVariable int size,
@RequestParam(value = "type",defaultValue = "0")Integer type,
@RequestHeader("Authorization")String authorization) {
Map<String, Object> userMap = JwtTokenUtil.parseToken(authorization);
//调用OrderService实现分页条件查询Order
Order order = new Order();
order.setUsername(userMap.get("username").toString());
switch (type){
case 1:
order.setPayStatus("0");
break;
case 3:
order.setPayStatus("1");
break;
}
PageInfo<Order> pageInfo = orderService.findPage(order, page, size);
return new Result(true, StatusCode.OK, "查询成功", pageInfo);
}
}
service:热点商品和普通商品分开下单
这里用了druid工具进行热点数据监控:
@Component
public class MonitorItemsAccess {
@Value("${druidurl}")
private String druidurl;
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private DruidDataSource dataSource;
/******
* 定义热点数据标准:
* 1.某一件商品访问量>N
* 2.最近N小时
*/
public List<String> loadData() throws Exception{
//获取连接对象
//Connection connection = (AvaticaConnection) DriverManager.getConnection(druidurl);
Connection connection =dataSource.getConnection();
//Statement
Statement statement = connection.createStatement();
//执行查询
ResultSet resultSet = statement.executeQuery(druidSQL());
//解析结果集
List<String> ids = new ArrayList<String>();
while (resultSet.next()){
String uri = resultSet.getString("uri");
uri=uri.replace("/web/items/","").replace(".html","");
ids.add(uri);
}
//关闭资源
resultSet.close();
statement.close();
connection.close();
return ids;
}
/***
* SQL组装
* @return
*/
public String druidSQL(){
//SQL语句
String prefix="SELECT COUNT(*) AS \"viewCount\",uri FROM logsitems WHERE __time>=CURRENT_TIMESTAMP - INTERVAL '1' HOUR";
//后部分
String suffix=" GROUP BY uri HAVING viewCount>2";
//SQL中间部分 AND uri NOT IN ('/web/items/S1235433012716498944.html')
//SKU_S1235433012716498944
String sql = "";
//基于Redis中存的热点商品的key来过滤排除要查询的数据
Set<String> keys = redisTemplate.keys("SKU_*");//所有以SKU_开始的key全部查询出来
if(keys!=null && keys.size()>0){
sql=" AND uri NOT IN (";
for (String key : keys) {
sql+="'/web/items/"+key.substring(4)+".html',";
}
sql=sql.substring(0,sql.length()-1);
sql+=")";
}
return prefix+sql+suffix;
}
}
然后用定时任务组件来执行上面的监控方法,设置每隔多久执行一次,这里用的是elasticjob第三方工具,实际当中可以用其他的定时任务框架
import com.dangdang.ddframe.job.api.ShardingContext;
import com.dangdang.ddframe.job.api.simple.SimpleJob;
import com.dangdang.elasticjob.lite.annotation.ElasticSimpleJob;
import com.seckill.goods.feign.SkuFeign;
import lombok.SneakyThrows;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.List;
@Component
@ElasticSimpleJob(//这是elasticjob工具的定时任务注解
cron = "1/5 * * * * ?",
jobName = "monitortask",
shardingTotalCount = 1
)
public class MonitorTask implements SimpleJob{
@Autowired
private MonitorItemsAccess monitorItemsAccess;
@Autowired
private SkuFeign skuFeign;
/***
* 执行业务逻辑
* @param shardingContext
*/
@SneakyThrows
@Override
public void execute(ShardingContext shardingContext) {
List<String> ids = monitorItemsAccess.loadData();
for (String id : ids) {
System.out.println("热点商品ID:"+id);
}
//热点数据隔离
skuFeign.hotIsolation(ids);
}
}
订单Service
@Service
public class OrderServiceImpl implements OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private SkuFeign skuFeign;
@Autowired
private IdWorker idWorker;
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private KafkaTemplate kafkaTemplate;
@Autowired
private RedissonDistributedLocker redissonDistributedLocker;
@Autowired
private MessageFeign messageFeign;
/****
* 热点商品下单
* @param orderMap
* @return
*/
@Override
public void hotAdd(Map<String, String> orderMap) throws IOException {
//消息封装对象
Map<String,Object> messageMap = new HashMap<String,Object>();
String username = orderMap.get("username");
String id = orderMap.get("id");
//Redis中对应的key
String key="SKU_"+id;
String lockkey="LOCKSKU_"+id;
String userKey="USER"+username+"ID"+id;
//如果key在redis缓存,则表示商品信息在Redis中进行操作
boolean bo =true;
// redissonDistributedLocker.tryLock(lockkey, 10, 10, TimeUnit.MINUTES);
if(bo){
if(redisTemplate.hasKey(key)){
//获取商品数量
Integer num = Integer.parseInt(redisTemplate.boundHashOps(key).get("num").toString());
if(num<=0){
//商品售罄通知
messageMap.put("code",20001);
messageMap.put("message","商品已售罄");
messageFeign.send(username,JSON.toJSONString(messageMap));
return;
}
Result<Sku> skuResult =skuFeign.findById(id);
Sku sku = skuResult.getData();
//1.创建Order
Order order = new Order();
order.setTotalNum(1);
order.setCreateTime(new Date());
order.setUpdateTime(order.getCreateTime());
order.setId("No"+idWorker.nextId());
order.setOrderStatus("0");
order.setPayStatus("0");
order.setConsignStatus("0");
order.setSkuId(id);
order.setName(sku.getName());
order.setPrice(sku.getSeckillPrice()*order.getTotalNum());
orderMapper.insertSelective(order);
//2.Redis中对应的num递减
num--;
if(num==0){
skuFeign.zero(id);
}
//2.清理用户排队信息
Map<String,Object> allMap = new HashMap<String,Object>();
allMap.put(userKey,0);
allMap.put("num",num);
redisTemplate.boundHashOps(key).putAll(allMap);
//3.记录用户购买过该商品,24小时后过期
redisTemplate.boundValueOps(userKey).set("");
redisTemplate.expire(userKey,1,TimeUnit.MINUTES);
//抢单成功通知
messageMap.put("code",200);
messageMap.put("message","抢单成功!");
messageFeign.send(username,JSON.toJSONString(messageMap));
}
//释放锁
//redissonDistributedLocker.unLock(lockkey);
return;
}
//抢单失败通知
messageMap.put("code",20001);
messageMap.put("message","抢单失败!");
messageFeign.send(username,JSON.toJSONString(messageMap));
}
/***
* 添加订单
* @param order
* @return
*/
@GlobalTransactional
@Override
public int add(Order order) {
String userKey="USER"+order.getUsername()+"ID"+order.getSkuId();
//1.递减库存
Result<Sku> dcount = skuFeign.dcount(order.getSkuId(), order.getTotalNum());
//2.递减成功->下单->记录当前用户抢单的时间点,24小时内不能抢购该商品
if(dcount.getCode()== StatusCode.DECOUNT_OK){
//int q=10/0;
Sku sku = dcount.getData();
//下单
//order.setId("No"+idWorker.nextId());
order.setOrderStatus("0");
order.setPayStatus("0");
order.setConsignStatus("0");
order.setSkuId(sku.getId());
order.setName(sku.getName());
order.setPrice(sku.getSeckillPrice()*order.getTotalNum());
orderMapper.insertSelective(order);
//记录当前用户抢单的时间点,24小时内不能抢购该商品
redisTemplate.boundValueOps(userKey).set("");
redisTemplate.boundValueOps(userKey).expire(1, TimeUnit.MINUTES);
return StatusCode.ORDER_OK;
}else{
//3.递减失败
//405库存不足
if(dcount.getCode()==StatusCode.DECOUNT_NUM){
return StatusCode.DECOUNT_NUM;
}else if(dcount.getCode()==StatusCode.DECOUNT_HOT){
//205商品热点
String key = "SKU_"+order.getSkuId();
Long increment = redisTemplate.boundHashOps(key).increment(userKey, 1);
if(increment==1){
//执行排队
Map<String,String> queueMap = new HashMap<String,String>();
queueMap.put("username",order.getUsername());
queueMap.put("id",order.getSkuId());
kafkaTemplate.send("neworder", JSON.toJSONString(queueMap));
}
return StatusCode.ORDER_QUEUE;
}
//0
return dcount.getCode();
}
}
消息队列监听订单
@Component
public class RabbitOrderListener {
@Autowired
private OrderService orderService;
/***
* 订单消费
* @param message
*/
@RabbitListener(queues = PayOrderMchNotifyMQ.MQ_NAME)
public void getMessage(String message) throws IOException {
//下单信息
Map<String,String> orderMap = JSON.parseObject(message,Map.class);
//热点商品下单
orderService.hotAdd(orderMap);
}
}