10. Micro service items

Pull hook education PC Station - micro service version

- Lao sun

1. Review microservices

1. General

Let's first look at how micro services are described in Martin Fowler's paper

Microservice is an architecture mode or architecture style. It advocates dividing a single application into a group of small services. Each service runs in its own process - Services coordinate and configure with each other to provide final value to users;

Popular points:

In the feudal dynasty, there were many states and counties. Each state and county was a relative of the emperor. There were also generals who made outstanding contributions, Marquis of Zhennan, King Pingxi and other senior officials of the frontier. Each of them was a symbol of the highest power in their own jurisdiction.

Their own states and counties operate independently (single structure)

However, due to different local conditions and customs, the ruling strategy should also meet different needs. (the South has cultural heritage, and Confucius can educate it. The folk custom in the northwest is fierce, so thunder must be used)

The unification of different states and counties, large and small, is a Supreme Court (general structure)

Summary:

Split the traditional one-stop application into an independent service and completely decouple it. Each service provides a service with a single business function. One service does one thing, establishes an independent process, can start and destroy by itself, and even has an independent database.

2. Advantages

  1. Each service is cohesive, small enough, simple to develop and efficient. One service does one thing;

  2. Microservices are loosely coupled and independent in both development and deployment phases;

  3. Microservices can be developed in different languages;

4) Microservices are just business logic code and will not be mixed with HTML, CSS or other page components;

  1. Each service has its own storage capacity. It can have its own database. Of course, it can also have unified data;

3. Disadvantages

  1. Developers need to deal with the complexity of distributed systems;

  2. With the increase of services, the difficulty of operation and maintenance becomes greater;

  3. System deployment dependency

  4. Increased communication costs

  5. Data consistency is difficult

  6. System integration test trouble

  7. Performance monitoring is not easy

  8. . . . .

4. Microservices and microservice architecture

4.1 microservices

  • It emphasizes the size of a service. * * focuses on a point * *, which can solve an existing application, similar to a project / module in the project;

  • Separate dental hospital, Eye Hospital;

  • Mobile phones, computers, sofas, mattresses and sportswear are all micro services;

  • **Focus on individuals, and each individual completes a specific task or function**

4.2 microservice architecture

  • An architecture model, which advocates that a single application is divided into a group of small services, which coordinate and cooperate with each other to provide users with ultimate value;

  • Lightweight communication mechanism is adopted between services (RESTfull of HTTP protocol)

  • Each service is built around specific business and can be independently deployed to the production environment;

  • Try to avoid a unified and centralized service management mechanism

  • Not a single clinic. All our clinics are integrated to form a comprehensive hospital

  • Xiaomi ecological chain, toothbrush, rice cooker, mobile phone and router are all Xiaomi's.

4.3 what is the difference between springcloud and SpringBoot?

  • SpringBoot focuses on developing individual services quickly and easily;
  • SpringCloud focuses on the coordination and arrangement of global microservices. It integrates individual microservices developed by SpringBoot;
  • SpringBoot can be used and developed independently, but SpringCloud is inseparable from SpringBoot and belongs to dependency relationship;
  • SpringBoot belongs to a department, and SpringCloud is a general hospital;

4.4 spring cloud vs. Dubbo

**Dubbo ****SpringCloud **
Service registryZookeeperString Cloud Netflix Eureka
Service invocation modeRPCREST API
Service monitoringDubbo-monitorSpring Boot Admin
Circuit breakerimperfectSpring Cloud Netflix Hystrix
Service gatewaynothingSpring Cloud Netflix Zuul
Distributed configurationnothingSpring Cloud Config
Service trackingnothingSpring Cloud Sleuth
Message busnothingSpring Cloud Bus
data streamnothingSpring Cloud Stream
Batch tasknothingSpring Cloud Task

Brand machine and assembly machine

2. Microservice architecture project

  • Edu Lagou: parent project

  • Edu API: common sub module

  • Edu Eureka boot: Service Center: 7001

  • Edu user boot: user micro service: 8001

  • Edu course boot: course microservice: 8002

  • Edu order boot: order micro service: 8003

  • Edu pay boot: payment micro service: 8004

  • Edu comment boot: Message micro service: 8005

3. Construction project

3.1 parent project



3.2 create a service center

  • Create a new Module in the parent project

  • Open the service manager to make debugging easier

@SpringBootApplication
@EnableEurekaServer //Open Eureka service
public class EduEurekaBootApplication {
    public static void main(String[] args) {
        SpringApplication.run(EduEurekaBootApplication.class, args);
    }
}
  • Modify the suffix of the configuration file to yml
server:
  # Configure service port
  port: 7001
eureka:
  client:
    service-url:
      # Configure eureka server address
      defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
    #Whether you need to register yourself with the registry (the registry cluster needs to be set to true)
    register-with-eureka: false
    #Do you need to search service information? Because you are a registry, it is false
    fetch-registry: false
  • Start project

3.3 creating micro services


@SpringBootApplication
@EnableEurekaClient // Clients registered to the hub
public class EduUserBootApplication {
    public static void main(String[] args) {
        SpringApplication.run(EduUserBootApplication.class, args);
    }
}
  • Modify configuration file suffix yml
server:
  # Service port number
  port: 8001
spring:
  application:
    # Service name - the name used to communicate between services
    name: edu-user-boot
eureka:
  client:
    service-url:
      # Fill in the address of the registry server
      defaultZone: http://localhost:7001/eureka
    # Do you need to register yourself with the registry
    register-with-eureka: true
    # Need to search for service information
    fetch-registry: true
  instance:
    # Register with the registry using an ip address
    prefer-ip-address: true
    # Status parameters displayed in the registry list
    instance-id: ${spring.cloud.client.ip-address}:${server.port}
package com.lagou.eduuserboot.com.lagou.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @BelongsProject: edu-lagou
 * @Author: GuoAn.Sun
 * @CreateTime: 2020-10-15 15:54
 * @Description:
 */
@RestController
public class TestAction {
    @GetMapping("hello1")
    public String hello1(){
        System.out.println("Hello, sun!");
        return "Hi,Lao sun!";
    }
}

  • Start service

  • Final effect:

3.3.1 user micro service

<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.3.2</version>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
</dependency>
<!--pojo Persistent use-->
<dependency>
    <groupId>javax.persistence</groupId>
    <artifactId>javax.persistence-api</artifactId>
    <version>2.2</version>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.12</version>
    <scope>provided</scope>
</dependency>
  • Configure database parameters
server:
  # Service port number
  port: 8001
spring:
  application:
    # Service name - the name used to communicate between services
    name: edu-user-boot
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://192.168.204.141:3306/edu?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC
    username: root
    password: 123123
  • Entity class
import lombok.Data;

import javax.persistence.Id;
import javax.persistence.Table;
import java.io.Serializable;
import java.util.Date;

@Data
@Table(name = "user")
public class User implements Serializable {
    private static final long serialVersionUID = -89788707895046947L;
    /**
     * User id
     */
    @Id
    private Integer id;
  • mapper
package com.lagou.eduuserboot.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.lagou.eduuserboot.entity.User;

/**
 * @BelongsProject: edu-lagou
 * @Author: GuoAn.Sun
 * @CreateTime: 2020-10-15 17:38
 * @Description:
 */
public interface UserMapper extends BaseMapper<User> {
}
  • Interface
public interface UserService {
    User getOne(Integer id);
}
@Service
public class UserServiceImpl  implements UserService {

    @Autowired
    private UserMapper userMapper;

    @Override
    public User getOne(Integer id) {
        return userMapper.selectById(id);
    }
}
  • Control layer
@RestController
public class TestAction {

    @Autowired
    private UserService userService;
    @GetMapping("hello1/{id}")
    public User hello1(@PathVariable("id") Integer id){
        System.out.println("Hello, sun!");
        return userService.getOne(id);
    }
}
  • Startup class
@SpringBootApplication
@EnableEurekaClient // Clients registered to the hub
@MapperScan("com.lagou.eduuserboot.mapper")  // Scan mapper package
public class EduUserBootApplication {
    public static void main(String[] args) {
        SpringApplication.run(EduUserBootApplication.class, args);
    }
}
  • Transform with the original business
public interface UserService {

    /**
     * @param phone cell-phone number
     * @param password password
     * @return User object
     */
    User login(String phone,  String password);

    /**Check whether the mobile phone number has been registered
     *
     * @param phone cell-phone number
     * @return 0: Not registered, 1: registered
     */
    Integer checkPhone(String phone);

    /**
     * User registration
     *
     * @param phone    cell-phone number
     * @param password password
     * @param nickname nickname
     * @param headimg head portrait
     * @return Number of rows affected
     */
    Integer register( String phone, String password,String nickname,String headimg);
}
@Service
public class UserServiceImpl implements UserService {

    @Autowired
    private UserMapper userMapper;

    @Override
    public Integer checkPhone(String phone) {
        //Create condition constructor
        QueryWrapper<User> userQueryWrapper = new QueryWrapper<>();
        // The first parameter is the name of the field in the database. Remember, it is the field in the database
        // The second parameter is the content to query
        userQueryWrapper.eq("phone",phone);
        // selectOne() query is one. If more than one query meets the criteria, an error will be reported
        User user = userMapper.selectOne(userQueryWrapper );
        if(user == null){
            return 0;
        }
        return 1;
    }

    public User login(String phone, String password) {
        QueryWrapper<User> userQueryWrapper = new QueryWrapper<>();
        userQueryWrapper.eq("phone",phone);
        userQueryWrapper.eq("password",password);
        User user = userMapper.selectOne(userQueryWrapper );
        return user;
    }

    public Integer register(String phone, String password,String nickname,String headimg) {
        User user = new User();
        user.setPhone(phone);
        user.setPassword(password);
        user.setName(nickname);
        user.setPortrait(headimg);
        int insert = userMapper.insert(user);
        return insert;
    }
}
  • Start vue project test login
@RestController
@RequestMapping("user")
@CrossOrigin // Cross domain
public class UserController {
}

3.3.2 course microservices

  • Mybatis plus does not support complex multi table Association queries. If you encounter complex multi table queries, you can still use the mybatis+xml configuration file. The usage is the same as before
  • Modify the yml configuration file and tell the program where to find mapper xml
mybatis-plus:
  mapper-locations: classpath:mybatis/mapper/*.xml  #Create mybatis/mapper under resources
  • Put the previously written coursedao Copy the XML and modify it:
<!--Modify package path: mapper Complete interface path-->
<mapper namespace="com.lagou.educourseboot.mapper.CourseMapper"> 
    <resultMap type="com.lagou.educourseboot.entity.Course" id="CourseMap">
    ...ellipsis
  • Add our custom methods to the mapper interface
public interface CourseMapper extends BaseMapper<Course> {
    /**
     * Query all course information
     * @return
     */
    List<Course> getAllCourse();

    /**
     * Query all course information purchased by logged in users
     * @return
     */
    List<Course> getCourseByUserId(@Param("userId") String userId);

    /**
     * Query the details of a course
     * @param courseid Course number
     * @return
     */
    Course getCourseById(@Param("courseid") Integer courseid);
}
  • service
public interface CourseService {
    /**
     * Query all course information
     * @return
     */
    List<Course> getAllCourse();

    /**
     * Query all course information purchased by logged in users
     * @return
     */
    List<Course> getCourseByUserId(String userId);

    /**
     * Query the details of a course
     * @param courseid Course number
     * @return
     */
    Course getCourseById(Integer courseid);
}
  • controller
@RestController
@RequestMapping("course")
@CrossOrigin //Cross domain
public class CourseController {

    @Autowired
    private CourseService courseService;

    @GetMapping("getAllCourse")
    public List<Course> getAllCourse() {
        List<Course> list = courseService.getAllCourse();
        return list;
    }

    @GetMapping("getCourseByUserId/{userid}")
    public List<Course> getCourseByUserId( @PathVariable("userid") String userid ) {
        List<Course> list = courseService.getCourseByUserId(userid);
        return list;
    }

    @GetMapping("getCourseById/{courseid}")
    public Course getCourseById(@PathVariable("courseid")Integer courseid) {
        Course course = courseService.getCourseById(courseid);
        return course;
    }
}

3.3.3 message micro service

  • yml
datasource:
  driver-class-name: com.mysql.jdbc.Driver
  url: jdbc:mysql://192.168.204.141:3306/edu?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC
  username: root
  password: 123123
  • serviceImpl implementation
public interface CommentService {
    /**
     * Save message
     * @param comment Message content object
     * @return Number of rows affected
     */
    Integer saveComment(CourseComment comment);

    /**
     * All messages of a course (pagination)
     * @param courseid Course number
     * @param offset Data offset
     * @param pageSize Entries per page
     * @return Message collection
     */
    List<CourseComment> getCommentsByCourseId(Integer courseid, Integer offset, Integer pageSize);
    /**
     * give the thumbs-up
     * @param comment_id Message number
     * @param userid User number
     * @return 0: Saving failed, 1: saving succeeded
     */
    Integer saveFavorite(Integer comment_id,Integer userid);

    /**
     * Cancel like
     * @param comment_id Message number
     * @param userid User number
     * @return 0: Saving failed, 1: saving succeeded
     */
    Integer cancelFavorite(Integer comment_id,Integer userid);
}
@Service
public class CommentServiceImpl implements CommentService {

    @Autowired
    private CourseCommentDao courseCommentDao;
    @Autowired
    private CourseCommentFavoriteRecordDao courseCommentFavoriteRecordDao;

    //Save message
    public Integer saveComment(CourseComment comment) {
        return courseCommentDao.insert(comment);
    }

    public List<CourseComment> getCommentsByCourseId(Integer courseid, Integer offset, Integer pageSize) {
        return courseCommentDao.getCommentsByCourseId(courseid, offset, pageSize);
    }

    /**
     *give the thumbs-up:
     * First check whether the current user likes this message,
     * If you click too: modify is_del status, cancel like
     * If not: save a like message
     *
     * Finally, update the number of likes
      */
    public Integer saveFavorite(Integer comment_id, Integer userid) {
        QueryWrapper<CourseCommentFavoriteRecord> q1 = new QueryWrapper();
        q1.eq("comment_id", comment_id);
        q1.eq("user_id", userid);
        Integer i = courseCommentFavoriteRecordDao.selectCount(q1);

        int i1 = 0;
        int i2 = 0;
        if(i == 0){ //I didn't like it
            //Save like
            CourseCommentFavoriteRecord favoriteRecord = new CourseCommentFavoriteRecord();
            favoriteRecord.setCommentId(comment_id);
            favoriteRecord.setUserId(userid);
            favoriteRecord.setIsDel(0);
            favoriteRecord.setCreateTime(new Date());
            favoriteRecord.setUpdateTime(new Date());
            i1 = courseCommentFavoriteRecordDao.insert(favoriteRecord);
        }else{
            //Change the like status of this message to 0, indicating that the like has been cancelled
            CourseCommentFavoriteRecord favoriteRecord = new CourseCommentFavoriteRecord();
            favoriteRecord.setIsDel(0);

            QueryWrapper<CourseCommentFavoriteRecord> q2 = new QueryWrapper();
            q2.eq("comment_id", comment_id);
            q2.eq("user_id", userid);

            i1 = courseCommentFavoriteRecordDao.update(favoriteRecord,q2);
        }
        i2 = courseCommentDao.updateLikeCount(1,comment_id);

        if(i1==0 || i2==0){
            throw  new RuntimeException("Like failed!");
        }
        return comment_id;
    }

    /**
     * Delete likes (is_del = 1)
     * Update the number of comment likes - 1
     * @param comment_id Message number
     * @param userid User number
     * @return 0:Failure, 1: success
     */
    public Integer cancelFavorite(Integer comment_id, Integer userid) {
        CourseCommentFavoriteRecord favoriteRecord = new CourseCommentFavoriteRecord();
        favoriteRecord.setIsDel(1);

        QueryWrapper<CourseCommentFavoriteRecord> q1 = new QueryWrapper();
        q1.eq("comment_id", comment_id);
        q1.eq("user_id", userid);

        Integer i1 = courseCommentFavoriteRecordDao.update(favoriteRecord,q1);

        Integer i2 = courseCommentDao.updateLikeCount(-1,comment_id);

        if(i1==0 || i2==0){
            throw  new RuntimeException("Failed to cancel like!");
        }
        return i2;
    }
}
  • mapper
@Service
public interface CourseCommentDao extends BaseMapper<CourseComment> {

    /**
     * All messages of a course (pagination)
     * @param courseid Course number
     * @param offset Data offset
     * @param pageSize Entries per page
     * @return Message collection
     */
    @Select({"select\n" +
            "        cc.id cc_id,`course_id`,`section_id`,`lesson_id`,cc.user_id cc_user_id,`user_name`,`parent_id`,`is_top`,`comment`,`like_count`,`is_reply`,`type`,`status`,cc.create_time cc_create_time ,cc.update_time cc_update_time ,cc.is_del cc_is_del,`last_operator`,`is_notify`,`mark_belong`,`replied` ,\n" +
            "        ccfr.id ccfr_id,ccfr.user_id ccfr_user_id,comment_id,ccfr.is_del ccfr_is_del,ccfr.create_time ccfr_create_time,ccfr.update_time ccfr_update_time\n" +
            "        from course_comment cc left join (select * from course_comment_favorite_record where is_del = 0) ccfr\n" +
            "        on cc.id = ccfr.comment_id\n" +
            "        where cc.is_del = 0\n" +
            "        and course_id = #{courseid}\n" +
            "        order by is_top desc , like_count desc , cc.create_time desc\n" +
            "        limit #{offset}, #{pageSize}"})
    @Results({
            @Result(column = "cc_id",property = "id"),
            @Result(column = "cc_user_id",property = "userId"),
            @Result(column = "cc_create_time",property = "createTime"),
            @Result(column = "cc_update_time",property = "updateTime"),
            @Result(column = "cc_is_del",property = "isDel"),
            @Result(column = "comment_id", property = "favoriteRecords",many = @Many(select = "com.lagou.educommentboot.mapper.CourseCommentFavoriteRecordDao.findByCommentid"))
    })
    List<CourseComment> getCommentsByCourseId(@Param("courseid") Integer courseid, @Param("offset") Integer offset, @Param("pageSize") Integer pageSize);

    /**
     * Update the number of likes
     * @param x +1 If yes, the number of likes increases, and if - 1, the number of likes decreases
     * @param comment_id Number of a message
     * @return 0: Saving failed, 1: saving succeeded
     */
    @Update({"update course_comment set like_count = like_count + #{x} where id = #{comment_id}"})
    Integer updateLikeCount(@Param("x") Integer x, @Param("comment_id") Integer comment_id);
}
public interface CourseCommentFavoriteRecordDao extends BaseMapper<CourseCommentFavoriteRecord> {
    @Select({"select * from course_comment_favorite_record where comment_id = #{comment_id}"})
    List<CourseCommentFavoriteRecord> findByCommentid(Integer comment_id);
}
  • Startup class
@SpringBootApplication
@EnableEurekaClient // Clients registered to the hub
@MapperScan("com.lagou.educommentboot.mapper")
public class EduCommentBootApplication {
    public static void main(String[] args) {
        SpringApplication.run(EduCommentBootApplication.class, args);
    }
}

http://localhost:8005/course/comment/getCourseCommentList/7/1/20

3.3.4 payment micro service

  • Compared with the previous, there is no change
<dependency>
    <groupId>com.github.wxpay</groupId>
    <artifactId>wxpay-sdk</artifactId>
    <version>0.0.3</version>
</dependency>

<dependency>
    <groupId>com.jfinal</groupId>
    <artifactId>jfinal</artifactId>
    <version>3.5</version>
</dependency>
@RestController
@RequestMapping("order")
@CrossOrigin
public class WxPayController {

    @GetMapping("createCode")
    public Object createCode(String courseid,String coursename,String price) throws Exception {
        coursename = new String(coursename.getBytes("ISO-8859-1"),"UTF-8");
        Map<String,String> mm = new HashMap();
        mm.put("appid", PayConfig.appid);  // Public account ID
        mm.put("mch_id",PayConfig.partner);// Merchant number
        mm.put("nonce_str", WXPayUtil.generateNonceStr());//Random string
        mm.put("body",coursename); //Brief description of goods
        mm.put("out_trade_no",WXPayUtil.generateNonceStr()); //Randomly generated merchant order number
        mm.put("total_fee",price); // Order amount, total order amount, in minutes, can only be an integer
        mm.put("spbill_create_ip","127.0.0.1"); // Terminal IP
        mm.put("notify_url",PayConfig.notifyurl); //Notification address
        mm.put("trade_type","NATIVE"); //Transaction type
        //System.out.println("merchant information:" + mm);

        //2. Generate digital signature and convert merchant information into xml format
        String xml = WXPayUtil.generateSignedXml(mm, PayConfig.partnerKey);
        //System.out.println("merchant's xml information:" + xml);

        //3. Send xml data to wechat payment platform to generate orders
        String url = "https://api.mch.weixin.qq.com/pay/unifiedorder";
        // Send the request and return a string in xml format
        String result = HttpKit.post(url, xml);

        //4. Wechat payment platform returns xml format data, converts it into map format and returns it to the front end
        Map<String, String> resultMap = WXPayUtil.xmlToMap(result);
        resultMap.put("orderId",mm.get("out_trade_no"));
        return resultMap;
    }


    @GetMapping("checkOrderStatus")
    public Object checkOrderStatus(String orderId) throws Exception {
        //1. Prepare merchant information
        Map<String,String> mm = new HashMap();
        mm.put("appid", PayConfig.appid);  // Public account ID
        mm.put("mch_id",PayConfig.partner);// Merchant number
        mm.put("out_trade_no", orderId);//Merchant order number
        mm.put("nonce_str",WXPayUtil.generateNonceStr()); //Random string
        //2. Generate digital signature
        String xml = WXPayUtil.generateSignedXml(mm, PayConfig.partnerKey);
        //3. Send query request to wechat payment platform
        String url = "https://api.mch.weixin.qq.com/pay/orderquery";

        // Start time of querying order status
        long beginTime = System.currentTimeMillis();
        // Keep going to the wechat payment platform to ask whether the payment is successful
        while(true) {
            //4. Process the query results returned by wechat payment platform
            String result = HttpKit.post(url, xml);
            Map<String, String> resultMap = WXPayUtil.xmlToMap(result);

            //Payment has been made successfully. Don't ask again
            if(resultMap.get("trade_state").equalsIgnoreCase("SUCCESS")){
                return resultMap;
            }

            //If you fail to pay for more than 30 seconds, stop asking
            if( System.currentTimeMillis()- beginTime > 30000 ){
                return resultMap;
            }
            Thread.sleep(3000); //Ask the wechat payment platform every 3 seconds
        }
    }
}

3.3.5 order micro service

  • Change the @ Reference in the controller to @ Autowired

  • dao interface

@Service
public interface OrderDao extends BaseMapper<UserCourseOrder> {
}
  • service implementation class (mybatis plus transformation)
@Service
public class OrderServiceImpl implements OrderService {

    @Autowired
    private OrderDao orderDao;
	// Save order
    public void saveOrder(String orderNo, String user_id, String course_id, String activity_course_id, String source_type) {
        UserCourseOrder order = new UserCourseOrder();

        order.setOrderNo(orderNo);
        order.setUserId(user_id);
        order.setCourseId(course_id);
        order.setActivityCourseId( Integer.parseInt(activity_course_id));
        order.setSourceType(source_type);
        order.setIsDel(0);
        order.setStatus(0);
        order.setCreateTime(new Date());
        order.setUpdateTime(new Date());

        orderDao.insert(order);
    }
    // Update order status
    public Integer updateOrder(String orderNo, int status) {
        UserCourseOrder order = new UserCourseOrder();
        order.setStatus(status);

        QueryWrapper<UserCourseOrder> qw = new QueryWrapper();
        qw.eq("order_no", orderNo);
        return orderDao.update(order,qw);
    }
	// Delete order
    public Integer deleteOrder(String orderNo) {
        QueryWrapper<UserCourseOrder> qw = new QueryWrapper();
        qw.eq("order_no", orderNo);
        // delete from user_course_order where order_no = orderNo
        return orderDao.delete(qw);
    }
	// Query all orders of a user
    public List<UserCourseOrder> getOrdersByUserId(String userId) {
        QueryWrapper<UserCourseOrder> qw = new QueryWrapper();
        qw.eq("user_id", userId);
        //select * from user_course_order where user_id = userId
        return orderDao.selectList(qw);
    }

}

3.4 sub warehouse reconstruction

  • As we said before, microservices can have their own independent database. Now we use sub database to realize it

  • Re plan the ownership of databases and tables

    • User micro service: edu_user library - > User
    • Course micro service: edu_course library - > course, course_section,activity_course,course_lesson,course_media,teacher
    • Message micro service: edu_comment Library - > Course_ comment,course_comment_favorite_record
    • Order micro service: edu_order Library - > 10 tables from order: user_course_order_0,user_course_order_1,user_course_order_2 …
  • Modify the database parameters in the yml configuration of each microservice

  • The transformation of courses and orders after sub warehouse is as follows:

    1. Through the logged in user id, go to the order micro service to query the course id that the user has successfully purchased
  1. Send all the purchased course IDs to the course micro service to query the course details

    Use axios to send a cross domain request, followed by the parameters, which cannot be in the format of array!

    For example: xxx?ids=[1,2,3], this format is not allowed. It should be changed to XXX? 0 = 1 & 1 = 2 & 2 = 3 (012 is the subscript of the array)

    Solution: use qs object instead of JSON object for conversion

data() {
    return {
       myCourseIds:[], // List of courses I have purchased
    };
},
created() {
    this.getCourseList(); //When the component is created, the method of obtaining all courses will be called

    this.user = JSON.parse( localStorage.getItem("user") );
    if( this.user != null ){
        this.isLogin = true; //Logged in
        this.getMyCourseIds(); //1. Get the course number of the current user who has purchased (status=20)
    }
},
getMyCourseList(){
  //2. Query course information according to course number
  return this.axios
  .get("http://localhost:8002/course/getCourseByCourseId",{
      Headers:{
         'Content-Type':'text/plain'
      },
      params:{
         ids:qs.stringify(this.myCourseIds); // npm install qs, import qs from 'qs' is introduced on this page;
      }
   }).then((result) => {
      console.log(result);
      this.myCourseList = result.data;
  }).catch( (error)=>{
      this.$message.error("Failed to obtain the purchased course information!");
  });
},
getMyCourseIds(){
  return this.axios
  .get("http://localhost:8003/order/getOKOrderIdsByUserId/"+this.user.content.id)
.then((result) => {
      this.myCourseIds = result.data;
    console.log("curriculum id: "+this.myCourseIds);
      this.getMyCourseList();// To get the purchased courses
  }).catch( (error)=>{
      this.$message.error("Get purchased courses ID Failed!");
  });
}
  • Order micro service
// Query the course ID that the user has purchased
@GetMapping("getOKOrderIdsByUserId/{userid}")
public List<Object> getOKOrderIdsByUserId(@PathVariable("userid")String userid ) {
    System.out.println("Whose course number to query:"+userid);
  List<UserCourseOrder> list = orderService.getOKOrderIdsByUserId(userid);
    List<Object> list2 = new ArrayList<>();
    for(UserCourseOrder order : list){
        list2.add(order.getCourseId());
  }
    System.out.println(list2);
    return list2;
}
List<UserCourseOrder> getOKOrderIdsByUserId(String userId);
  public List<UserCourseOrder> getOKOrderIdsByUserId(String userId) {
    QueryWrapper<UserCourseOrder> qw = new QueryWrapper();
      qw.select("course_id"); // Query the specified column: select count_id from table
      qw.eq("user_id", userId);
      qw.eq("status", 20);
      return orderDao.selectList(qw);
  }
  • Course micro service
// Passed on successfully purchased course ID (multiple courses) 

@GetMapping("getCourseByCourseId")
public List<Course> getCourseByCourseId(  String ids ) {
    System.out.println(ids); // The format is id = 1 & id = 2 & id = 3
    List<String> idList = new ArrayList<>();
    // Extract all id values in the string
    while(ids.indexOf("=")>0){
        String id;
        if(ids.indexOf("&")>0){
             id = ids.substring(ids.indexOf("=")+1,ids.indexOf("&"));
            System.out.println(id);
            ids = ids.substring(ids.indexOf("&")+1);

        }else{
             id = ids.substring(ids.indexOf("=")+1);
            System.out.println(id);
          ids = ids.substring(ids.indexOf("=")+1);
        }
        idList.add(id);
    }
    System.out.println(idList);
    List<Course> list = courseService.getCourseByCourseId(idList);
    return list;
}
/**
 * Query all course information purchased by logged in users
 * @return
 */
List<Course> getCourseByCourseId(List<String> idList);
@Override
public List<Course> getCourseByCourseId(List<String> idList) {
    return courseMapper.getCourseByCourseId(idList);
}
/**
 * Query all course information purchased by logged in users
 * @return
 */
List<Course> getCourseByCourseId(@Param("idList") List<String> idList);
<select id="getCourseByCourseId" resultMap="CourseMap">
    <include refid="courseInfo"/>
    where  c.id in
    <foreach collection="idList" open="(" separator="," close=")" item="cid">
        #{cid}
    </foreach>
    order by amount desc , c_id , ac_create_time desc
</select>

3.5 application of order sub table

  • Sharding JDBC provides services in the form of jar package, so maven dependency should be introduced first
<dependency>
    <groupId>org.apache.shardingsphere</groupId>
    <artifactId>sharding-jdbc-spring-boot-starter</artifactId>
    <version>4.0.0-RC1</version>
</dependency>
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>1.1.10</version>
</dependency>
  • Create sub database and sub table configuration class
package com.example.demo.config;

import com.alibaba.druid.pool.DruidDataSource;
import org.apache.shardingsphere.api.config.sharding.KeyGeneratorConfiguration;
import org.apache.shardingsphere.api.config.sharding.ShardingRuleConfiguration;
import org.apache.shardingsphere.api.config.sharding.TableRuleConfiguration;
import org.apache.shardingsphere.api.config.sharding.strategy.InlineShardingStrategyConfiguration;
import org.apache.shardingsphere.shardingjdbc.api.ShardingDataSourceFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.sql.DataSource;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;

/**
 * @BelongsProject: demo
 * @Author: GuoAn.Sun
 * @Description: Configure fragmentation rules
 */
@Configuration
public class ShardingJdbcConfig {
    // Define data source
    Map<String, DataSource> createDataSourceMap() {
        DruidDataSource ds = new DruidDataSource();
        ds.setDriverClassName("com.mysql.jdbc.Driver");
        ds.setUrl("jdbc:mysql://192.168.204.141:3306/edu_order?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC");
        ds.setUsername("root");
        ds.setPassword("123123");
        Map<String, DataSource> result = new HashMap<>();
        result.put("m1", ds);
        return result;
    }

    // Define primary key generation policy
    private static KeyGeneratorConfiguration getKeyGeneratorConfiguration() {
        KeyGeneratorConfiguration result = new KeyGeneratorConfiguration("SNOWFLAKE", "id");
        return result;
    }

    // Definition t_ Sharding strategy of order table
    TableRuleConfiguration getOrderTableRuleConfiguration() {
        TableRuleConfiguration result = new TableRuleConfiguration("user_course_order", "m1.user_course_order_$->{0..9}");
        result.setTableShardingStrategyConfig(new InlineShardingStrategyConfiguration("id", "user_course_order_$->{id % 2 + 1}"));
        result.setKeyGeneratorConfig(getKeyGeneratorConfiguration());
        return result;
    }

    // Defining sharding JDBC data sources
    @Bean
    DataSource getShardingDataSource() throws SQLException {
        ShardingRuleConfiguration shardingRuleConfig = new ShardingRuleConfiguration();
        shardingRuleConfig.getTableRuleConfigs().add(getOrderTableRuleConfiguration());
        //spring.shardingsphere.props.sql.show = true
        Properties properties = new Properties();
        properties.put("sql.show", "true");
        return ShardingDataSourceFactory.createDataSource(createDataSourceMap(), shardingRuleConfig, properties);
    }
}
  • yml
server:
  port: 8003
spring:
  application:
    name: edu-order-boot
eureka:
  client:
    service-url:
      defaultZone: http://localhost:7001/eureka
    register-with-eureka: true
    fetch-registry: true
  instance:
    prefer-ip-address: true
    instance-id: ${spring.cloud.client.ip-address}:${server.port}
  • Startup class
import org.apache.shardingsphere.shardingjdbc.spring.boot.SpringBootConfiguration;

@SpringBootApplication(exclude = {SpringBootConfiguration.class}) // Shield the use of spring in the startup class Class of shardingsphere configuration item
@EnableEurekaClient // Clients registered to the hub
@MapperScan("com.lagou.eduorderboot.mapper")
public class EduOrderBootApplication {
    public static void main(String[] args) {
        SpringApplication.run(EduOrderBootApplication.class, args);
    }
}

3.6 modification of playback components

  • The < video > component tag of html we used in the front-end project can be played. Although it can be played, it has a single function and can only be played, which can not ensure the security of video resources

  • We adopt Alibaba cloud video on demand

  • Alibaba official website: https://www.alibabacloud.com/help/zh/doc-detail/51236.htm?spm=a2c63.p38356.b99.2.28213799QTbeE3

  • High end extra services are charged according to traffic

  • Transform the front-end project and add Alibaba playback components

  1. Index. In the public directory of vue project Introducing css and js into HTML files
<link rel="stylesheet" href="https://g.alicdn.com/de/prismplayer/2.8.2/skins/default/aliplayer-min.css" />

<script charset="utf-8" type="text/javascript" src="https://g.alicdn.com/de/prismplayer/2.8.2/aliplayer-min.js"></script>
  1. Introduce in your own vue component
<div class="prism-player" id="J_prismPlayer" ></div>
  1. vue binding
var player = new Aliplayer({
  id: 'J_prismPlayer',
  width: '100%',
  height: '900px',
  autoplay: true,
  //This playback address supports the highest playback priority
  source : 'https://video.pearvideo.com/mp4/short/20200914/cont-1697119-15382138-hd.mp4',
});

3.6.1 switching video using player

<template>
    <div>
        <div id="video-box">
            <div class="prism-player" id="J_prismPlayer" ></div>
        </div>
    
        <button @click="playLesson('https://xxxx. Mp4 ') "> Video 1 < / button >
        <button @click="playLesson('https://yyyy. Mp4 ') "> Video 2 < / button >
    </div>
</template>
<script>
	mounted(){
        this.player = new Aliplayer({
            id: 'J_prismPlayer',
            width: '100%',
            height: '900px',
            autoplay: true,
            //Support playback address playback, which has the highest playback priority
            source : '',
        });
    },
    methods:{
        playLesson(mp4_url){
            // Recreate the player every time (otherwise the video will be superimposed)
            document.getElementById("J_prismPlayer").remove();
            var pdiv = document.createElement("div");
            pdiv.setAttribute("class","prism-player");
            pdiv.setAttribute("id","J_prismPlayer");
			document.getElementById("video-box").appendChild(pdiv);
            
            this.player = new Aliplayer({
                id: 'J_prismPlayer',
                width: '100%',
                height: '900px',
                autoplay: true,
                //Support playback address playback, which has the highest playback priority
                source : mp4_url,
            });
        }
    }
</script>

3.7 gateway

edu-gateway-boot

<!--GateWay gateway-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!--introduce webflux-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
@SpringBootApplication
@EnableEurekaClient
public class LagouCloudGatewayApplication {
    public static void main(String[] args) {
        SpringApplication.run(LagouCloudGatewayApplication.class, args);
    }
}
server:
  port: 9000
eureka:
  client:
    service-url:
      defaultZone: http://localhost:7001/eureka
    register-with-eureka: true
    fetch-registry: true
  instance:
    prefer-ip-address: true
    instance-id: ${spring.cloud.client.ip-address}:${server.port}
spring:
  application:
    name: edu-gateway-boot
  cloud:
    gateway:
      routes:
        - id: edu-routes-course # Route name
          uri: lb://Edu course boot # go to the registry to find the name of the micro service
          predicates: # When the assertion is successful, forwarding is used when it is handed over to a microservice for processing
            - Path=/course/**
          filters:
            - StripPrefix=1   # Remove the first part of the uri
        - id: edu-routes-comment
          uri: lb://edu-comment-boot
          predicates:
            - Path=/comment/**
          filters:
            - StripPrefix=1
        - id: edu-routes-order
          uri: lb://edu-order-boot
          predicates:
            - Path=/order/**
          filters:
            - StripPrefix=1
        - id: edu-routes-pay
          uri: lb://edu-pay-boot
          predicates:
            - Path=/pay/**
          filters:
            - StripPrefix=1
        - id: edu-routes-user
          uri: lb://edu-user-boot
          predicates:
            - Path=/user/**
          filters:
            - StripPrefix=1

Test 1: http://localhost:9000/course/course/getAllCourse

Test 2: http://localhost:9000/comment/course/comment/getCourseCommentList/9/1/20

3.8 high concurrency redis to help you carry

  • Introduce dependency
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
  • Modify yml
server:
  port: 8002
spring:
  application:
    name: edu-course-boot
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://192.168.204.141:3306/edu_course?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC
    username: root
    password: 123123
  redis:
    host: 192.168.204.141
    port: 6379
  • redis operation should be placed in controller? Or service?
@Service
public class CourseServiceImpl implements CourseService {
    @Autowired
    private CourseMapper courseMapper;

    @Autowired
    private RedisTemplate<Object,Object> redisTemplate;

    @Override
    public List<Course> getAllCourse() {
        //Rename the serialized collection name in redis memory with String (increase readability)
        RedisSerializer rs = new StringRedisSerializer();
        redisTemplate.setKeySerializer(rs);

        System.out.println("query redis");
        List<Course> list = (List<Course>)redisTemplate.opsForValue().get("allCourses");
        if(list == null){
            //Go to database
            System.out.println("====MySql database====");
            list = courseMapper.getAllCourse();
            // Put the collection queried from the database into redis memory (key,value, expired seconds, tool class of seconds)
            redisTemplate.opsForValue().set("allCourses", list,10, TimeUnit.SECONDS);
        }
        return list;
    }
  • If an error occurs:

  • redis is currently read-only and cannot write data, because the current identity role is to become the master for the reason of

3.8.1 cache penetration of high parallel delivery

  • Because, let's assume that 1000 people enter the method execution at the same time, and 1000 people find the set from the cache, but they don't find it. Then, if we enter the next step, 1000 people will query the database at the same time. In this way, we query the database 1000 times, which is inefficient, and redis is not used. The reason for this is that after the first redis cache query, subsequent queries are not blocked, which is "cache penetration"

  • Simulate 20 threads with high concurrency

    @GetMapping("getAllCourse")
    public List<Course> getAllCourse() {
        // Simulate multithreading: create a thread pool with a capacity of 20
        ExecutorService es = Executors.newFixedThreadPool(20);
        // Simulate 20 threads to query at the same time
        for (int i = 1; i <= 20; i++) {
            es.submit(new Runnable() {
                @Override
                public void run() {
                    courseService.getAllCourse();
                }
            });
        }
        return courseService.getAllCourse();
    }
    
  • Solution:

  • 1. The simplest and crudest solution -- synchronization method lock

    public synchronized List<Course> getAllCourse() {
        xxxx
    }
    
  • 2. A slightly more efficient scheme -- synchronous code block (double-layer detection lock)

    @Autowired
    private RedisTemplate<Object,Object> redisTemplate;
    
    @Override
    public List<Course> getAllCourse() {
        RedisSerializer rs = new StringRedisSerializer();
        redisTemplate.setKeySerializer(rs);
    
        System.out.println("query redis");
        List<Course> list = (List<Course>)redisTemplate.opsForValue().get("allCourses");
        if(list == null){
            //Queue up, let the first person in and go through the process (the people behind will go through the cache)
            synchronized (this){
                list = (List<Course>)redisTemplate.opsForValue().get("allCourses");
                if(list == null){
                    //Go to database
                    System.out.println("====MySql database====");
                    list = courseMapper.getAllCourse();
                    // Put the set queried from the database into redis memory
                     redisTemplate.opsForValue().set("allCourses", list,10, TimeUnit.SECONDS);
                }
            }
        }
        return list;
    }
    

3.8.2 how to ensure that the data in redis is up-to-date

  • If the course content changes, we will delete the relevant sets in redis when modifying the course content.
  • Then save the latest data to the database
  • When querying data, because the data in redis has been deleted, it will query the database at the first time to ensure that the data is up-to-date.

4. IDEA integration Docker deployment microservices

4.1 review docker

  • I want to build a house, so I move bricks, cut wood, draw drawings, and cement. After a fierce operation, the house was finally built.

  • After living for some time, I wanted to move back to my hometown in the northeast on a whim. At this time, according to the previous method, I can only go back to the northeast and move bricks, cut wood, draw drawings, cement and build a house again.

  • Suddenly, a fairy sister came and taught me a spell. This spell can make a copy of my house into a "mirror image" and put it in a treasure chest

  • Holding the treasure chest, I went back to the northeast. I used this "mirror image" to copy a house, reproduce it perfectly, and check in with my bag

  • Isn't it amazing? Corresponding to our project, the house is the project itself, the image is the copy of the project, and the treasure chest is the image warehouse

  • If you want to dynamically expand the capacity, take the project image from the warehouse and copy it casually

  • There is no need to pay attention to issues such as version, compatibility and deployment, which completely solves the embarrassment of "perfect development, online collapse, and non-stop troubleshooting environment"

4.2 installing docker

# Install docker on 192.168.204.141
[root@A ~]# yum -y install docker

# Start docker
[root@A ~]# systemctl start docker

# View the running status of docker
[root@A ~]# systemctl status docker

4.3 enable remote access

  • Docker does not allow remote access by default
# Modify profile
[root@A ~]# vim /lib/systemd/system/docker.service

ExecStart=/usr/bin/dockerd -H tcp://0.0.0.0:2375 -H unix://var/run/docker.sock

Version 19.03.5
ExecStart=/usr/bin/dockerd -H tcp://0.0.0.0:5678 -H unix://var/run/docker.sock -H fd:// --containerd=/run/containerd/containerd.sock --graph /data/docker

Docker common ports
2375 port: unencrypted docker socket; Remote login root account without password to access the host (not enabled by default)
Port 2376: TLS encrypted socket. It is likely that the CI 4243 port of the server is modified as https 443 port
Port 2377: cluster mode socket, applicable to cluster manager, not docker client
Port 5000: docker registration service
4789 and 7946 ports: overlay network

# Reload profile
[root@A ~]# systemctl daemon-reload

# Restart docker
[root@A ~]# service docker restart

# Check whether the port is on
[root@A ~]# netstat -nlpt

# Verify that the port is valid
[root@A ~]# curl http://113.31.144.141:2375/info
[root@A ~]# curl http://106.75.253.40:2375/info

4.4 IDEA integration plug-in

  • Search for Docker in Plugins and install it

https://owi3yzzk.mirror.aliyuncs.com

4.5 Docker Maven plug-in

  • In the traditional process, we have to go through the steps of packaging, deployment, uploading to linux, writing Dockerfile, building image, creating container and so on
  • Docker made plugin is to help us automatically generate images and push them to the warehouse during development
<plugin>
    <groupId>com.spotify</groupId>
    <artifactId>docker-maven-plugin</artifactId>
    <version>1.0.0</version>
    <configuration>
        <!--Image name laosun/test-docker-demo-->
        <imageName>laosun/${project.artifactId}</imageName>
        <!--Label version-->
        <imageTags>
            <imageTag>latest</imageTag>
        </imageTags>
        <!--Basic image, equivalent to Dockerfile Inside from-->
        <baseImage>java</baseImage>
        <!--Label version-->
        <maintainer>laosun angiersun@lagou.com</maintainer>
        <!--Entry point, project.build.finalName namely project Under label build Under label filename Label content, test-docker-demo-->
        <!--It is equivalent to automatic execution after starting the container java -jar/test-docker-demo.jar-->
        <entryPoint>["java", "-jar", "/${project.build.finalName}.jar"]</entryPoint>
        <!--docker address-->
        <dockerHost>http://192.168.204.141:2375</dockerHost>

        <!-- Here is copy jar Package to docker Container specified directory configuration -->
        <resources>
            <resource>
                <targetPath>/</targetPath>
                <!--The root directory of the copy, target-->
                <directory>${project.build.directory}</directory>
                <!--Which file to upload to docker,amount to Dockerfile Inside add test-docker-demo.jar /-->
                <include>${project.build.finalName}.jar</include>
            </resource>
        </resources>
    </configuration>
</plugin>

4.6 execute command

  • Package the project and build the image to docker
  • For the first time, wait a little longer to pull the java environment and other operations
mvn clean package docker:build
  • Tips
    Run Commands using IDE Press Ctrl+Enter to run the highlighted action using the relevant IDE feature instead of the terminal. Press Ctrl+Shift+Enter for debug. Press Enter to run the command in the terminal as usual. You can turn this behavior on/off in Settings | Tools | Terminal. Got it!

  • After the command is executed, the jar package image will be automatically pushed to docker

  • In the docker interface of idea, you can create a container according to the image.

  • "One mirror can create N containers"

  • If the following error occurs, please execute the corresponding command, which is the reason why the executable file cannot be found

[root@A ~]# cd /usr/libexec/docker/
[root@A docker]# ln -s docker-proxy-current docker-proxy
  • The command that docker is running cannot be found
[root@A docker]# cd /usr/libexec/docker/
[root@A docker]# ln -s docker-runc-current docker-runc
  • visit: http://192.168.204.141:9001

  • error
    The reason why docker+tomcat starts very slowly is JRE /dev/random blocking
    1 enter the container
    2 vim /etc/java-8-openjdk/security/java.security
    3 find securerandom source=file:/dev/random
    4. Change to securerandom source=file:/dev/./ urandom
    5 restart the container

  • docker container cross host access

  1. Download and install weave
[root@localhost node1]# curl -L git.io/weave -o /usr/bin/weave
[root@localhost node1]# chmod a+x /usr/bin/weave 
[root@localhost node1]# weave version
weave script 2.3.0
weave 2.3.0
  1. Start weave
      after running, a running container, two data only containers and three images will be generated. Among them, these data of weave are saved on the container named weavedb allocated on each machine. It is a data volume container, which is only responsible for data persistence
      a virtual network device named weave will also be generated
      a custom network using weave bridge will also be generated in docker.
[root@node1 ~]# weave launch
[root@node1 ~]# docker images |grep weave
[root@node1 ~]# ifconfig weave
[root@node1 ~]# docker network ls
  1. First use docker run to start the container, and then use the weave attach command to bind the IP address to the container
[root@linux-node1 ~]# weave attach 111.111.1.1/24 weave-test1  #Bind the weave-test1 container with an ip of 111.111.1.1
111.111.1.1
## Enter container
[root@linux-node1 ~]# docker exec -it weave-test1 /bin/bash
[root@be8cc008d9f1 /]# ifconfig   ##yum install -y net-tools
[root@linux-node2 ~]# weave attach 111.111.1.2/24 weave-test2  #Bind the weave-test2 container with an ip of 111.111.1.2
111.111.1.2
## Enter container
[root@linux-node2 ~]# docker exec -it weave-test2 /bin/bash
[root@be8cc008d9f2 /]# ifconfig   ##yum install -y net-tools
## Container interconnection
## By default, the above two containers created on node1 and node2 hosts cannot ping each other.
## You need to use the weave connect command to establish a connection between two weave routers.
[root@linux-node1 ~]# weave connect 118.190.201.12   ##The ip of the host is connected. Note that "weave forget ip" z means to disconnect the connection
[root@linux-node2 ~]# weave connect 118.190.201.11

## Then enter the container for mutual ping
[root@be8cc008d9f1 /]# ping 111.111.1.2
[root@be8cc008d9f2 /]# ping 111.111.1.1

Keywords: Java

Added by webpals on Sat, 19 Feb 2022 09:11:21 +0200