Scene Description: Alipay transfer balance treasure
Distributed transactions must meet the following conditions:
1, remote RPC calls, Alipay and balance treasure interface calls
2, Alipay and balance treasure use different databases.
As shown in the picture:
2. Distributed transaction solution
1. Two phase commit based on database XA protocol
Xa protocol is a protocol supported by database. The core of XA protocol is a transaction manager used to manage two distributed databases, as shown in the figure
The transaction manager is responsible for dealing with the Alipay database and the balance treasure database. Once a database connection fails, the operation of another database will not be carried out. A database operation failure will lead to another database rollback. Only the transactions of the two databases will be submitted.
The two-stage and three-stage submission based on XA protocol is a strict security confirmation mechanism, and its security is very high, but the premise to ensure security is to sacrifice performance, which is the CAP theory in the distributed system, and the premise to do any architecture needs to have a choice. Therefore, the concurrency of distributed transactions based on XA protocol is not high, which is not suitable for high concurrency scenarios.
2. Solution based on activemq
As shown in the picture:
1, when Alipay deducts money successfully, inserts the message to the message form.
2. The message table has message_id (flow ID), status (confirm, unconfirm)
3. timer scans unconfirm status record of message table to insert message into activemq
4. When yu'e Bao receives the message consumption message, first query the message table. If there is a record, it will not be processed. If there is no record, it will be added to the database
5, if the balance of treasure database operation successfully insert the message to the balance treasure message table, the table field is consistent with Alipay message.
6, if the 5 operation is successful, callback the Alipay interface to modify the state of the message table and convert the unconfirm state to the confirm state.
Problem Description:
1, the purpose of Alipay's message table design
If Alipay inserts messages to activemq and the consumption message of the balance treasure is abnormal, it may be that the consumption message is successful and the transaction operation is abnormal. It may be network uncertainty and so on. If there is an exception and activemq receives the confirmation signal, the message in activemq is deleted and the message is lost. Setting the message table means that there is a message stub, and the message in the message table is still missing in activemq. Solve the problem of active MQ message loss
2. The purpose of message table designed by yu'ebao
When the balance treasure consumption is successful and the database operation is successful, callback the message confirmation interface of Alipay. If the callback interface fails, the Alipay state changes or the unconfirm state will fail. At this time, it will be scanned by timer, and will be inserted into the activemq message, and will be consumed by the balance treasure. However, the message has been successfully consumed only by the failure of the callback. Yes, so you need to have a message table like this. When yu'e Bao consumes, insert the message table first. If message can query records according to message [ID], it means that the message is no longer consumed before. Just need to call back successfully. If the message is not queried, consume the message to continue the database operation. If the database operation is successful, insert the message into the message table. This solves the problem of repeated message consumption, which is also the idempotent operation of the consumer side.
The distributed transaction based on message middleware is the most ideal solution for distributed transaction, which takes security and concurrency into account!
Next, post the code:
Alipay Code:
@Controller @RequestMapping("/order") public class OrderController { /** * @Description TODO * @param @return parameter * @return String Return type * @throws * * userID: User ID of transfer * amount: How much is it? */ @Autowired @Qualifier("activemq") OrderService orderService; @RequestMapping("/transfer") public @ResponseBody String transferAmount(String userId,String messageId, int amount) { try { orderService.updateAmount(amount,messageId, userId); } catch (Exception e) { e.printStackTrace(); return "===============================transferAmount failed==================="; } return "===============================transferAmount successfull==================="; } @RequestMapping("/callback") public String callback(String param) { JSONObject parse = JSONObject.parseObject(param); String respCode = parse.getString("respCode"); if(!"OK".equalsIgnoreCase(respCode)) { return null; } try { orderService.updateMessage(param); }catch (Exception e) { e.printStackTrace(); return "fail"; } return "ok"; } }
public interface OrderService { public void updateAmount(int amount, String userId,String messageId); public void updateMessage(String param); }
@Service("activemq") @Transactional(rollbackFor = Exception.class) public class OrderServiceActivemqImpl implements OrderService { Logger logger = LoggerFactory.getLogger(getClass()); @Autowired JdbcTemplate jdbcTemplate; @Autowired JmsTemplate jmsTemplate; @Override public void updateAmount(final int amount, final String messageId, final String userId) { String sql = "update account set amount = amount - ?,update_time=now() where user_id = ?"; int count = jdbcTemplate.update(sql, new Object[]{amount, userId}); if (count == 1) { //Insert into message log table sql = "insert into message(user_id,message_id,amount,status) values (?,?,?,?)"; int row = jdbcTemplate.update(sql,new Object[]{userId,messageId,amount,"unconfirm"}); if(row == 1) { //Insert message into activemq jmsTemplate.send("zg.jack.queue", new MessageCreator() { @Override public Message createMessage(Session session) throws JMSException { com.zhuguang.jack.bean.Message message = new com.zhuguang.jack.bean.Message(); message.setAmount(Integer.valueOf(amount)); message.setStatus("unconfirm"); message.setUserId(userId); message.setMessageId(messageId); return session.createObjectMessage(message); } }); } } } @Override public void updateMessage(String param) { JSONObject parse = JSONObject.parseObject(param); String messageId = parse.getString("messageId"); String sql = "update message set status = ? where message_id = ?"; int count = jdbcTemplate.update(sql,new Object[]{"confirm",messageId}); if(count == 1) { logger.info(messageId + " callback successfull"); } } }activemq.xml
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:amq="http://activemq.apache.org/schema/core" xmlns:jms="http://www.springframework.org/schema/jms" xmlns:context="http://www.springframework.org/schema/context" xmlns:mvc="http://www.springframework.org/schema/mvc" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.1.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.1.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.1.xsd http://www.springframework.org/schema/jms http://www.springframework.org/schema/jms/spring-jms-4.1.xsd http://activemq.apache.org/schema/core http://activemq.apache.org/schema/core/activemq-core-5.12.1.xsd" > <context:component-scan base-package="com.zhuguang.jack" /> <mvc:annotation-driven /> <amq:connectionFactory id="amqConnectionFactory" brokerURL="tcp://192.168.88.131:61616" userName="system" password="manager" /> <!-- To configure JMS Connecting foreman --> <bean id="connectionFactory" class="org.springframework.jms.connection.CachingConnectionFactory"> <constructor-arg ref="amqConnectionFactory" /> <property name="sessionCacheSize" value="100" /> </bean> <!-- Defining message queues( Queue) --> <bean id="demoQueueDestination" class="org.apache.activemq.command.ActiveMQQueue"> <!-- Set the name of the message queue --> <constructor-arg> <value>zg.jack.queue</value> </constructor-arg> </bean> <!-- To configure JMS Template ( Queue),Spring Provided JMS Tool class, which sends and receives messages. --> <bean id="jmsTemplate" class="org.springframework.jms.core.JmsTemplate"> <property name="connectionFactory" ref="connectionFactory" /> <property name="defaultDestination" ref="demoQueueDestination" /> <property name="receiveTimeout" value="10000" /> <!-- trueyes topic,falseyes queue,The default isfalse,Write out herefalse --> <property name="pubSubDomain" value="false" /> </bean> </beans>spring-dispatcher.xml
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p" xmlns:context="http://www.springframework.org/schema/context" xmlns:task="http://www.springframework.org/schema/task" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:util="http://www.springframework.org/schema/util" xmlns:mvc="http://www.springframework.org/schema/mvc" xsi:schemaLocation=" http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-3.2.xsd http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.2.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.2.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-3.2.xsd http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task-3.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.0.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd "> <!-- Imported under the same folder redis Property profile --> <!-- Solve springMVC Response data scrambling text/plain Is to return data as is when responding--> <import resource="../activemq/activemq.xml"/> <!--<context:property-placeholder ignore-unresolvable="true" location="classpath:config/core/core.properties,classpath:config/redis/redis-config.properties" />--> <bean id="propertyConfigurerForProject1" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer"> <property name="order" value="1" /> <property name="ignoreUnresolvablePlaceholders" value="true" /> <property name="location"> <value>classpath:config/core/core.properties</value> </property> </bean> <mvc:annotation-driven> <mvc:message-converters register-defaults="true"> <bean class="org.springframework.http.converter.StringHttpMessageConverter"> <property name="supportedMediaTypes" value = "text/plain;charset=UTF-8" /> </bean> </mvc:message-converters> </mvc:annotation-driven> <!-- avoid IE implement AJAX Time,Return JSON Download file appears --> <bean id="mappingJacksonHttpMessageConverter" class="org.springframework.http.converter.json.MappingJacksonHttpMessageConverter"> <property name="supportedMediaTypes"> <list> <value>text/html;charset=UTF-8</value> </list> </property> </bean> <!-- open controller Annotation support --> <!-- Note: if base-package=com.avicit Annotation transaction does not work TODO Read source code --> <context:component-scan base-package="com.zhuguang"> </context:component-scan> <mvc:view-controller path="/" view-name="redirect:/index" /> <bean class="org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping" /> <bean id="handlerAdapter" class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter"> </bean> <bean class="org.springframework.web.servlet.view.ContentNegotiatingViewResolver"> <property name="mediaTypes"> <map> <entry key="json" value="application/json" /> <entry key="xml" value="application/xml" /> <entry key="html" value="text/html" /> </map> </property> <property name="viewResolvers"> <list> <bean class="org.springframework.web.servlet.view.BeanNameViewResolver" /> <bean class="org.springframework.web.servlet.view.UrlBasedViewResolver"> <property name="viewClass" value="org.springframework.web.servlet.view.JstlView" /> <property name="prefix" value="/" /> <property name="suffix" value=".jsp" /> </bean> </list> </property> </bean> <!-- Support to upload files --> <!-- Controller exception handling --> <bean id="exceptionResolver" class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver"> <property name="exceptionMappings"> <props> <prop key="java.lang.Exception"> error </prop> </props> </property> </bean> <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource" destroy-method="close"> <property name="driverClass"> <value>${jdbc.driverClassName}</value> </property> <property name="jdbcUrl"> <value>${jdbc.url}</value> </property> <property name="user"> <value>${jdbc.username}</value> </property> <property name="password"> <value>${jdbc.password}</value> </property> <property name="minPoolSize" value="10" /> <property name="maxPoolSize" value="100" /> <property name="maxIdleTime" value="1800" /> <property name="acquireIncrement" value="3" /> <property name="maxStatements" value="1000" /> <property name="initialPoolSize" value="10" /> <property name="idleConnectionTestPeriod" value="60" /> <property name="acquireRetryAttempts" value="30" /> <property name="breakAfterAcquireFailure" value="false" /> <property name="testConnectionOnCheckout" value="false" /> <property name="acquireRetryDelay"> <value>100</value> </property> </bean> <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate"> <property name="dataSource" ref="dataSource"></property> </bean> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"/> </bean> <tx:annotation-driven transaction-manager="transactionManager" proxy-target-class="true" /> <aop:aspectj-autoproxy expose-proxy="true"/> </beans>logback.xml
<?xml version="1.0" encoding="UTF-8"?> <!-- scan: When this property is set totrueIf the configuration file changes, it will be reloaded. The default value istrue. scanPeriod: Set whether the monitoring profile has a modified time interval. If no time unit is given, the default unit is ms when scan bytrueThis property takes effect. The default time interval is1Minute. debug: When this property is set totrueWill be printed out logback Internal log information, real-time viewing logback Operation status. The default value isfalse. --> <configuration scan="false" scanPeriod="60 seconds" debug="false"> <!-- Define log root --> <!-- <property name="LOG_HOME" value="/app/log" /> --> <!-- Define log file name --> <property name="appName" value="netty"></property> <!-- ch.qos.logback.core.ConsoleAppender Represents console output --> <appender name="stdout" class="ch.qos.logback.core.ConsoleAppender"> <Encoding>UTF-8</Encoding> <!-- Log output format:%d Represents date time,%thread Represents the thread name,%-5level: Level left5Character width %logger{50} Express logger Name is the longest.50Characters, otherwise separated by periods. %msg: Log messages,%n Newline character --> <encoder> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern> </encoder> </appender> <!-- Scroll to record the file, first record the log to the specified file, and when a certain condition is met, record the log to other files --> <appender name="appLogAppender" class="ch.qos.logback.core.rolling.RollingFileAppender"> <Encoding>UTF-8</Encoding> <!-- Specify the name of the log file --> <file>${appName}.log</file> <!-- When scrolling occurs, decide RollingFileAppender , involving file movement and renaming TimeBasedRollingPolicy: The most commonly used rolling strategy, which makes rolling strategy according to time, is responsible for rolling as well as starting rolling. --> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <!-- Storage location and file name of files generated during scrolling %d{yyyy-MM-dd}: Scroll logs by day %i: When the file size exceeds maxFileSize In accordance with i Scrolling files --> <fileNamePattern>${appName}-%d{yyyy-MM-dd}-%i.log</fileNamePattern> <!-- Optional node, which controls the maximum number of archive files to be retained. If the number exceeds, the old files will be deleted. Suppose you set scrolling every day, And maxHistory yes365,Save only recent365Days of files, delete previous old files. Note that deleting old files is, Directories created for archiving are also deleted. --> <MaxHistory>365</MaxHistory> <!-- When the log file exceeds maxFileSize The specified size is based on the above mentioned%i Scroll the log file note the configuration here SizeBasedTriggeringPolicy Scrolling by file size is not possible and must be configured timeBasedFileNamingAndTriggeringPolicy --> <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP"> <maxFileSize>100MB</maxFileSize> </timeBasedFileNamingAndTriggeringPolicy> </rollingPolicy> <!-- Log output format:%d Represents date time,%thread Represents the thread name,%-5level: Level left5Character width %logger{50} Express logger Name is the longest.50Characters, otherwise separated by periods. %msg: Log messages,%n Newline character --> <encoder> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [ %thread ] - [ %-5level ] [ %logger{50} : %line ] - %msg%n</pattern> </encoder> </appender> <!-- logger It is mainly used to store log objects and define log types and levels name: For matching logger The type prefix, which is the first half of the package level: The level of logging to log, including TRACE < DEBUG < INFO < WARN < ERROR additivity: The role lies in children-logger Whether to use rootLogger Configured appender Output,false: Indicates only the current logger Of appender-ref,true: Present logger Of appender-ref and rootLogger Of appender-ref All effective --> <!-- <logger name="edu.hyh" level="info" additivity="true"> <appender-ref ref="appLogAppender" /> </logger> --> <!-- root And logger It is a parent-child relationship. If there is no special definition, it defaults to root,Any class can only be associated with one logger Corresponding, Or defined logger,Either root,The key to judgment is to find this logger,And then judge this logger Of appender and level. --> <root level="debug"> <appender-ref ref="stdout" /> <appender-ref ref="appLogAppender" /> </root> </configuration>
2,Yu'ebao code
package com.zhuguang.jack.controller; import com.alibaba.fastjson.JSONObject; import com.zhuguang.jack.service.OrderService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; @Controller @RequestMapping("/order") public class OrderController { /** * @Description TODO * @param @return parameter * @return String Return type * @throws * * Simulated bank transfer * userID: User ID of transfer * amount: How much is it? */ @Autowired OrderService orderService; @RequestMapping("/transfer") public @ResponseBody String transferAmount(String userId, String amount) { try { orderService.updateAmount(Integer.valueOf(amount), userId); } catch (Exception e) { e.printStackTrace(); return "===============================transferAmount failed==================="; } return "===============================transferAmount successfull==================="; } }Message listener
package com.zhuguang.jack.listener; import com.alibaba.fastjson.JSONObject; import com.zhuguang.jack.service.OrderService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.client.SimpleClientHttpRequestFactory; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.client.RestTemplate; import javax.jms.JMSException; import javax.jms.Message; import javax.jms.MessageListener; import javax.jms.ObjectMessage; @Service("queueMessageListener") public class QueueMessageListener implements MessageListener { private Logger logger = LoggerFactory.getLogger(getClass()); @Autowired OrderService orderService; @Transactional(rollbackFor = Exception.class) @Override public void onMessage(Message message) { if (message instanceof ObjectMessage) { ObjectMessage objectMessage = (ObjectMessage) message; try { com.zhuguang.jack.bean.Message message1 = (com.zhuguang.jack.bean.Message) objectMessage.getObject(); String userId = message1.getUserId(); int count = orderService.queryMessageCountByUserId(userId); if (count == 0) { orderService.updateAmount(message1.getAmount(), message1.getUserId()); orderService.insertMessage(message1.getUserId(), message1.getMessageId(), message1.getAmount(), "ok"); } else { logger.info("Abnormal transfer"); } RestTemplate restTemplate = createRestTemplate(); JSONObject jo = new JSONObject(); jo.put("messageId", message1.getMessageId()); jo.put("respCode", "OK"); String url = "http://jack.bank_a.com:8080/alipay/order/callback?param=" + jo.toJSONString(); restTemplate.getForObject(url,null); } catch (JMSException e) { e.printStackTrace(); throw new RuntimeException("abnormal"); } } } public RestTemplate createRestTemplate() { SimpleClientHttpRequestFactory simpleClientHttpRequestFactory = new SimpleClientHttpRequestFactory(); simpleClientHttpRequestFactory.setConnectTimeout(3000); simpleClientHttpRequestFactory.setReadTimeout(2000); return new RestTemplate(simpleClientHttpRequestFactory); } }
package com.zhuguang.jack.service; public interface OrderService { public void updateAmount(int amount, String userId); public int queryMessageCountByUserId(String userId); public int insertMessage(String userId,String messageId,int amount,String status); }
package com.zhuguang.jack.service; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.client.SimpleClientHttpRequestFactory; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.client.RestTemplate; @Service @Transactional(rollbackFor = Exception.class) public class OrderServiceImpl implements OrderService { private Logger logger = LoggerFactory.getLogger(getClass()); @Autowired JdbcTemplate jdbcTemplate; /* * Update the database table and subtract the amount from the account balance */ @Override public void updateAmount(int amount, String userId) { //1. Agricultural Bank of China transfers 3000, that is to say, the Bank of agriculture's jack account will be reduced by 3000 String sql = "update account set amount = amount + ?,update_time=now() where user_id = ?"; int count = jdbcTemplate.update(sql, new Object[] {amount, userId}); if (count != 1) { throw new RuntimeException("Order creation failed, Agricultural Bank of China transfer failed!"); } } public RestTemplate createRestTemplate() { SimpleClientHttpRequestFactory simpleClientHttpRequestFactory = new SimpleClientHttpRequestFactory(); simpleClientHttpRequestFactory.setConnectTimeout(3000); simpleClientHttpRequestFactory.setReadTimeout(2000); return new RestTemplate(simpleClientHttpRequestFactory); } @Override public int queryMessageCountByUserId(String messageId) { String sql = "select count(*) from message where message_id = ?"; int count = jdbcTemplate.queryForInt(sql, new Object[]{messageId}); return count; } @Override public int insertMessage(String userId, String message_id,int amount, String status) { String sql = "insert into message(user_id,message_id,amount,status) values(?,?,?)"; int count = jdbcTemplate.update(sql, new Object[]{userId, message_id,amount, status}); if(count == 1) { logger.info("Ok"); } return count; } }activemq.xml
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:amq="http://activemq.apache.org/schema/core" xmlns:jms="http://www.springframework.org/schema/jms" xmlns:context="http://www.springframework.org/schema/context" xmlns:mvc="http://www.springframework.org/schema/mvc" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.1.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.1.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.1.xsd http://www.springframework.org/schema/jms http://www.springframework.org/schema/jms/spring-jms-4.1.xsd http://activemq.apache.org/schema/core http://activemq.apache.org/schema/core/activemq-core-5.12.1.xsd" > <context:component-scan base-package="com.zhuguang.jack" /> <mvc:annotation-driven /> <amq:connectionFactory id="amqConnectionFactory" brokerURL="tcp://192.168.88.131:61616" userName="system" password="manager" /> <!-- To configure JMS Connecting foreman --> <bean id="connectionFactory" class="org.springframework.jms.connection.CachingConnectionFactory"> <constructor-arg ref="amqConnectionFactory" /> <property name="sessionCacheSize" value="100" /> </bean> <!-- Defining message queues( Queue) --> <bean id="demoQueueDestination" class="org.apache.activemq.command.ActiveMQQueue"> <!-- Set the name of the message queue --> <constructor-arg> <value>zg.jack.queue</value> </constructor-arg> </bean> <!-- Display injection message listening container( Queue),Configure the connection factory to monitor demoQueueDestination,A listener is a listener defined above --> <bean id="queueListenerContainer" class="org.springframework.jms.listener.DefaultMessageListenerContainer"> <property name="connectionFactory" ref="connectionFactory" /> <property name="destination" ref="demoQueueDestination" /> <property name="messageListener" ref="queueMessageListener" /> </bean> <!-- To configure JMS Template ( Queue),Spring Provided JMS Tool class, which sends and receives messages. --> <bean id="jmsTemplate" class="org.springframework.jms.core.JmsTemplate"> <property name="connectionFactory" ref="connectionFactory" /> <property name="defaultDestination" ref="demoQueueDestination" /> <property name="receiveTimeout" value="10000" /> <!-- trueyes topic,falseyes queue,The default isfalse,Write out herefalse --> <property name="pubSubDomain" value="false" /> </bean> </beans>OK~~~~~~~~~~~~be accomplished!!! , If you are satisfied and interested in technology, please add: 171239762, Pure technology exchange group, if you are the one.
<li class="tool-item tool-active is-like "><a href="javascript:;"><svg class="icon" aria-hidden="true"> <use xlink:href="#csdnc-thumbsup"></use> </svg><span class="name">Give the thumbs-up</span> <span class="count">6</span> </a></li> <li class="tool-item tool-active is-collection "><a href="javascript:;" data-report-click="{"mod":"popu_824"}"><svg class="icon" aria-hidden="true"> <use xlink:href="#icon-csdnc-Collection-G"></use> </svg><span class="name">Collection</span></a></li> <li class="tool-item tool-active is-share"><a href="javascript:;"><svg class="icon" aria-hidden="true"> <use xlink:href="#icon-csdnc-fenxiang"></use> </svg>share</a></li> <!--Reward begins--> <!--End of reward--> <li class="tool-item tool-more"> <a> <svg t="1575545411852" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5717" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M179.176 499.222m-113.245 0a113.245 113.245 0 1 0 226.49 0 113.245 113.245 0 1 0-226.49 0Z" p-id="5718"></path><path d="M509.684 499.222m-113.245 0a113.245 113.245 0 1 0 226.49 0 113.245 113.245 0 1 0-226.49 0Z" p-id="5719"></path><path d="M846.175 499.222m-113.245 0a113.245 113.245 0 1 0 226.49 0 113.245 113.245 0 1 0-226.49 0Z" p-id="5720"></path></svg> </a> <ul class="more-box"> <li class="item"><a class="article-report">Article report</a></li> </ul> </li> </ul> </div> </div> <div class="person-messagebox"> <div class="left-message"><a href="https://blog.csdn.net/luoyang_java"> <img src="https://profile.csdnimg.cn/2/F/E/3_luoyang_java" class="avatar_pic" username="luoyang_java"> <img src="https://g.csdnimg.cn/static/user-reg-year/1x/4.png" class="user-years"> </a></div> <div class="middle-message"> <div class="title"><span class="tit"><a href="https://Blog. CSDN. Net / luoyang_java "data report Click =" {& quot; mod & quot;: & quot; pop_379 & quot;} "target =" _blank "> Huangshan technological ape</a></span> </div> <div class="text"><span>Published 106 original articles</span> · <span>Praise 34</span> · <span>40000 visitors+</span></div> </div> <div class="right-message"> <a href="https://Im. CSDN. Net / im / main. HTML? Username = Luoyang? Java "target =" [blank "class =" BTN BTN SM BTN red hold BT button personal letter "> private message </a> <a class="btn btn-sm bt-button personal-watch" data-report-click="{"mod":"popu_379"}">follow</a> </div> </div> </div> </article>