微服务秒杀项目整合网关+feign+redis分离热点商品分别下单示例
生活随笔
收集整理的這篇文章主要介紹了
微服务秒杀项目整合网关+feign+redis分离热点商品分别下单示例
小編覺得挺不錯的,現在分享給大家,幫大家做個參考.
文章目錄
- 配置文件和通用文件
- 通用結果類型:
- 返回碼:
- 統一異常處理:
- 網關
- feign服務
- redission配置
- controller
- service:熱點商品和普通商品分開下單
- 消息隊列監聽訂單
思路:對于普通商品進行正常下單(直接減去數據庫中的庫存然后生成訂單,對于熱點數據加一層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*/@Beanpublic StringRedisSerializer keySerializer() {return new StringRedisSerializer();}/***** 序列化設置* @return*/@Beanpublic 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)@ResponseBodypublic 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-webcloud:nacos:config:file-extension: yamlserver-addr: nacos-server:8848discovery:#Nacos的注冊地址server-addr: nacos-server:8848gateway:routes:#商品- id: goods_routeuri: lb://seckill-goodspredicates:- Path=/api/skuAct/**,/api/activity/**,/api/brand/**,/api/category/**,/api/seckillTime/**,/api/sku/**filters:- StripPrefix=1#訂單- id: order_routeuri: lb://seckill-orderpredicates:- Path=/api/order/**filters:- StripPrefix=1#搜索- id: search_routeuri: lb://seckill-searchpredicates:- Path=/api/search/**filters:- StripPrefix=1#用戶- id: user_routeuri: lb://seckill-userpredicates:- Path=/api/user/**filters:- StripPrefix=1#管理員- id: manager_routeuri: lb://seckill-managerpredicates:- Path=/api/admin/**filters:- StripPrefix=1#靜態頁- id: page_routeuri: lb://seckill-pagepredicates:- 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*/@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {//獲取request和responseServerHttpRequest 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){//狀態嗎 401response.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*/@Overridepublic int getOrder() {return 0;}@Beanpublic 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 {@Autowiredprivate RedissonClient redissonClient;/**** 加鎖,會一直循環加鎖,直到拿到鎖* @param lockkey* @return*/@Overridepublic RLock lock(String lockkey) {RLock lock = redissonClient.getLock(lockkey);lock.lock();return lock;}/**** 加鎖,在指定時間內拿不到鎖就會放棄* @param lockkey* @return*/@Overridepublic RLock lock(String lockkey, long timeout) {RLock lock = redissonClient.getLock(lockkey);lock.lock(timeout,TimeUnit.SECONDS);return lock;}/**** 加鎖,在指定時間內拿不到鎖就會放棄* @param lockkey* @return*/@Overridepublic RLock lock(String lockkey, long timeout, TimeUnit unit) {return null;}/**** 加鎖,在指定時間內拿不到鎖就會放棄,如果拿到鎖,鎖最終有效時間為leasetime* @param lockkey* @return*/@Overridepublic boolean tryLock(String lockkey, long timeout, long leasetime, TimeUnit unit) {return false;}/***** 解鎖* @param lockkey*/@Overridepublic void unLock(String lockkey) {RLock lock = redissonClient.getLock(lockkey);lock.unlock();}/**** 解鎖* @param lock*/@Overridepublic void unLocke(RLock lock) {lock.unlock();} }controller
商品SkuController
@RestController @RequestMapping("/sku")public class SkuController {@Autowiredprivate 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 {@Autowiredprivate OrderService orderService;@Autowiredprivate 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, "令牌無效!");}//封裝OrderOrder 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實現分頁條件查詢OrderOrder 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實現分頁條件查詢OrderOrder 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;@Autowiredprivate RedisTemplate redisTemplate;@Autowiredprivate DruidDataSource dataSource;/******* 定義熱點數據標準:* 1.某一件商品訪問量>N* 2.最近N小時*/public List<String> loadData() throws Exception{//獲取連接對象//Connection connection = (AvaticaConnection) DriverManager.getConnection(druidurl);Connection connection =dataSource.getConnection();//StatementStatement 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_S1235433012716498944String 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{@Autowiredprivate MonitorItemsAccess monitorItemsAccess;@Autowiredprivate SkuFeign skuFeign;/**** 執行業務邏輯* @param shardingContext*/@SneakyThrows@Overridepublic 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 {@Autowiredprivate OrderMapper orderMapper;@Autowiredprivate SkuFeign skuFeign;@Autowiredprivate IdWorker idWorker;@Autowiredprivate RedisTemplate redisTemplate;@Autowiredprivate KafkaTemplate kafkaTemplate;@Autowiredprivate RedissonDistributedLocker redissonDistributedLocker;@Autowiredprivate MessageFeign messageFeign;/***** 熱點商品下單* @param orderMap* @return*/@Overridepublic 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中對應的keyString 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.創建OrderOrder 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@Overridepublic 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;}//0return dcount.getCode();} }消息隊列監聽訂單
@Component public class RabbitOrderListener {@Autowiredprivate 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);} }總結
以上是生活随笔為你收集整理的微服务秒杀项目整合网关+feign+redis分离热点商品分别下单示例的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: mysql 共享锁和排他锁 意向锁 记录
- 下一篇: 类加载器源码、双亲委派、自定义类加载器详