SpringBoot实现数据库读写分离
SpringBoot实现数据库读写分离
参考博客https://blog.csdn.net/qq_31708899/article/details/121577253
实现原理:翻看AbstractRoutingDataSource源码我们可以看到其中的targetDataSource可以维护一组目标数据源(采用map数据结构),并且做了路由key与目标数据源之间的映射,提供基于key查找数据源的方法。看到了这个,我们就可以想到怎么实现数据源切换了
#### 一 maven依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.4.12-SNAPSHOT</version><relativePath/> <!-- lookup parent from repository --></parent><groupId>com.ReadAndWriteSeparate</groupId><artifactId>demo</artifactId><version>0.0.1-SNAPSHOT</version><name>ReadAndWriteSeparate</name><description>Demo project for Spring Boot</description><properties><java.version>1.8</java.version></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-jdbc</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>2.2.0</version></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>5.1.26</version><scope>runtime</scope></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>org.aspectj</groupId><artifactId>aspectjweaver</artifactId><version>1.9.5</version></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><configuration><excludes><exclude><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></exclude></excludes></configuration></plugin></plugins><resources><resource><directory>src/main/java</directory><includes><include>**/*.xml</include></includes></resource></resources></build></project>
二 数据源配置
- yaml配置
这里我只用一个账号模拟,生产环境下必须要分开只读账号和可读可写账号,因为主从复制中,主机可不会同步从机的数据哟
spring:datasource:master:jdbc-url: jdbc:mysql://localhost:3306/demo?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghaiusername: rootpassword: rootdriver-class-name: com.mysql.jdbc.Driverslave1:jdbc-url: jdbc:mysql://localhost:3306/demo?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghaiusername: rootpassword: rootdriver-class-name: com.mysql.jdbc.Driverslave2:jdbc-url: jdbc:mysql://localhost:3306/demo?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghaiusername: rootpassword: rootdriver-class-name: com.mysql.jdbc.Driver
- 数据源配置
package com.readandwriteseparate.demo.Config;import com.readandwriteseparate.demo.Enum.DbEnum;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;import javax.sql.DataSource;
import java.beans.ConstructorProperties;
import java.util.HashMap;
import java.util.Map;/*** @author OriginalPerson* @date 2021/11/25 20:25* @Email 2568500308@qq.com*/
@Configuration
public class DataSourceConfig {//主数据源,用于写数据,特殊情况下也可用于读@Bean@ConfigurationProperties("spring.datasource.master")public DataSource masterDataSource(){return DataSourceBuilder.create().build();}@Bean@ConfigurationProperties("spring.datasource.slave1")public DataSource slave1DataSource(){return DataSourceBuilder.create().build();}@Bean@ConfigurationProperties("spring.datasource.slave2")public DataSource slave2DataSource(){return DataSourceBuilder.create().build();}@Beanpublic DataSource routingDataSource(@Qualifier("masterDataSource") DataSource masterDataSource,@Qualifier("slave1DataSource") DataSource slave1DataSource,@Qualifier("slave2DataSource") DataSource slave2DataSource){Map<Object,Object> targetDataSource=new HashMap<>();targetDataSource.put(DbEnum.MASTER,masterDataSource);targetDataSource.put(DbEnum.SLAVE1,slave1DataSource);targetDataSource.put(DbEnum.SLAVE2,slave2DataSource);RoutingDataSource routingDataSource=new RoutingDataSource();routingDataSource.setDefaultTargetDataSource(masterDataSource);routingDataSource.setTargetDataSources(targetDataSource);return routingDataSource;}}
这里我们配置了4个数据源,其中前三个数据源都是为了生成第四个路由数据源产生的,路由数据源的key我们使用枚举类型来标注,三个枚举类型分别代表数据库的类型。
package com.readandwriteseparate.demo.Enum;/*** @author OriginalPerson* @date 2021/11/25 20:45* @Email: 2568500308@qq.com*/
public enum DbEnum {MASTER,SLAVE1,SLAVE2;
}
三 数据源切换
这里我们使用ThreadLocal将路由key设置到每个线程的上下文中这里也进行一个简单的负载均衡,轮询两个只读数据源,而访问哪个取决于counter的值,每增加1,切换一下数据源,该值为juc并发包下的原子操作类,保证其线程安全。
- 设置路由键,获取当前数据源的key
package com.readandwriteseparate.demo.Config;import com.readandwriteseparate.demo.Enum.DbEnum;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;import java.util.concurrent.atomic.AtomicInteger;/*** @author OriginalPerson* @date 2021/11/25 20:49* @Email: 2568500308@qq.com*/
public class DBContextHolder {private static final ThreadLocal<DbEnum> contextHolder=new ThreadLocal<>();private static final AtomicInteger counter=new AtomicInteger(-1);public static void set(DbEnum type){contextHolder.set(type);}public static DbEnum get(){return contextHolder.get();}public static void master(){set(DbEnum.MASTER);System.out.println("切换到master数据源");}public static void slave(){//轮询数据源进行读操作int index=counter.getAndIncrement() % 2;if(counter.get()>9999){counter.set(-1);}if(index==0){set(DbEnum.SLAVE1);System.out.println("切换到slave1数据源");}else {set(DbEnum.SLAVE2);System.out.println("切换到slave2数据源");}}
}
- 确定当前数据源
这个比较重要,其继承AbstractRoutingDataSource类,重写了determineCurrentLookupKey方法,该方法决定当前数据源的key,对应于上文配置数据源的map集合中的key,让该方法返回我们定义的ThreadLocal中存储的key,即可实现数据源切换。
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.lang.Nullable;/*** @author OriginalPerson* @date 2021/11/25 20:47* @Email: 2568500308@qq.com*/
public class RoutingDataSource extends AbstractRoutingDataSource {@Nullable@Overrideprotected Object determineCurrentLookupKey() {return DBContextHolder.get();}
}
- mybatis配置三个数据源
package com.readandwriteseparate.demo.Config;import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;import javax.annotation.Resource;
import javax.sql.DataSource;/*** @author OriginalPerson* @date 2021/11/25 22:17* @Email 2568500308@qq.com*/
@EnableTransactionManagement
@Configuration
public class MybatisConfig {@Resource(name = "routingDataSource")private DataSource routingDataSource;@Beanpublic SqlSessionFactory sessionFactory() throws Exception {SqlSessionFactoryBean sqlSessionFactoryBean=new SqlSessionFactoryBean();sqlSessionFactoryBean.setDataSource(routingDataSource);return sqlSessionFactoryBean.getObject();}@Beanpublic PlatformTransactionManager platformTransactionManager(){return new DataSourceTransactionManager(routingDataSource);}
}
四 特殊处理master主库读数据操作、
在某些场景下,我们需要实时读取到更新过的值,例如某个业务逻辑,在插入一条数据后,需要立即查询据,因为读写分离我们用的是主从复制架构,它是异步操作,串行复制数据,所以必然存在主从延迟问题,对于刚插入的数据,如果要马上取出,读从库是没有数据的,因此需要直接读主库,这里我们通过一个Master注解来实现,被该注解标注的方法将直接在主库数据
- 注解
package com.readandwriteseparate.demo.annotation;/*** @author OriginalPerson* @date 2021/11/26 13:28* @Email 2568500308@qq.com*/public @interface Master {
}
- APO切面处理
package com.readandwriteseparate.demo.Aspect;import com.readandwriteseparate.demo.Config.DBContextHolder;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;/*** @author OriginalPerson* @date 2021/11/26 13:23* @Email 2568500308@qq.com*/@Aspect
@Component
public class DataSourceAop {// 非master注解且有关读的方法操作从库@Pointcut("!@annotation(com.readandwriteseparate.demo.annotation.Master)" +" && (execution(* com.readandwriteseparate.demo.Service..*.select*(..)))" +" || execution(* com.readandwriteseparate.demo.Service..*.get*(..)))")public void readPointcut(){}// 有master注解或者有关处理数据的操作主库 @Pointcut("@annotation(com.readandwriteseparate.demo.annotation.Master) " +"|| execution(* com.readandwriteseparate.demo.Service..*.insert*(..)) " +"|| execution(* com.readandwriteseparate.demo.Service..*.add*(..)) " +"|| execution(* com.readandwriteseparate.demo.Service..*.update*(..)) " +"|| execution(* com.readandwriteseparate.demo.Service..*.edit*(..)) " +"|| execution(* com.readandwriteseparate.demo.Service..*.delete*(..)) " +"|| execution(* com.readandwriteseparate.demo.Service..*.remove*(..))")public void writePointcut() {}@Before("readPointcut()")public void read(){DBContextHolder.slave();}@Before("writePointcut()")public void write(){DBContextHolder.master();}
}
五 读写分离案例使用
- 实体类
package com.readandwriteseparate.demo.Domain;import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;import java.io.Serializable;/*** @author OriginalPerson* @date 2021/11/26 23:15* @Email 2568500308@qq.com*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User implements Serializable {private Integer id;private String name;private String sex;
}
- Dao层
package com.readandwriteseparate.demo.Dao;import com.readandwriteseparate.demo.Domain.User;
import org.apache.ibatis.annotations.Param;import java.util.List;/*** @author OriginalPerson* @date 2021/11/26 23:16* @Email 2568500308@qq.com*/
public interface UserMapper {public List<User> selectAllUser();public Integer insertUser(@Param("user") User user);public User selectOneById(@Param("id") Integer id);
}
xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.readandwriteseparate.demo.Dao.UserMapper"><resultMap id="user" type="com.readandwriteseparate.demo.Domain.User"><id property="id" column="id"></id><result property="name" column="name"></result><result property="sex" column="sex"></result></resultMap><select resultMap="user" id="selectAllUser" resultType="com.readandwriteseparate.demo.Domain.User">select * from user</select><insert id="insertUser" parameterType="com.readandwriteseparate.demo.Domain.User">insert into user(name,sex) values(#{user.name},#{user.sex})</insert><select id="selectOneById" parameterType="java.lang.Integer" resultMap="user">select * from user where id=#{id}</select>
</mapper>
service
package com.readandwriteseparate.demo.Service;import com.readandwriteseparate.demo.Dao.UserMapper;
import com.readandwriteseparate.demo.Domain.User;
import com.readandwriteseparate.demo.annotation.Master;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;import java.util.List;/*** @author OriginalPerson* @date 2021/11/27 0:07* @Email 2568500308@qq.com*/@Service
public class UserService {@Autowiredprivate UserMapper userMapper;public List<User> getAllUser(){return userMapper.selectAllUser();}public Integer addUser(User user){return userMapper.insertUser(user);}/** 特殊情况下,需要从主库查询时* 例如某些业务更新数据后需要马上查询,因为主从复制有延迟,所以需要从主库查询* 添加@Master注解即可从主库查询** 该注解实现比较简单,在aop切入表达式中进行判断即可* */@Masterpublic User selectOneById(Integer id){return userMapper.selectOneById(id);}
}
单元测试代码
package com.readandwriteseparate.demo;import com.readandwriteseparate.demo.Dao.UserMapper;
import com.readandwriteseparate.demo.Domain.User;
import com.readandwriteseparate.demo.Service.UserService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;@SpringBootTest
class ReadAndWriteSeparateApplicationTests {@Autowiredprivate UserService userService;@Testvoid contextLoads() throws InterruptedException {User user=new User();user.setName("赵六");user.setSex("男");System.out.println("插入一条数据");userService.addUser(user);for (int i = 0; i <4 ; i++) {System.out.println("开始查询数据");System.out.println("第"+(i+1)+"次查询");userService.getAllUser();System.out.println("-------------------------分割线------------------------");}System.out.println("强制查询主库");userService.selectOneById(1);}}
查询结果:
相关文章:

SpringBoot实现数据库读写分离
SpringBoot实现数据库读写分离 参考博客https://blog.csdn.net/qq_31708899/article/details/121577253 实现原理:翻看AbstractRoutingDataSource源码我们可以看到其中的targetDataSource可以维护一组目标数据源(采用map数据结构),并且做了路由key与目标…...

Linux(四)--包软件管理器与Linux上环境部署示例
一.包软件管理器【yum和apt】 1.先来学习使用yum命令。yum:RPM包软件管理器,用于自动化安装配置Linux软件,并可以自动解决依赖问题。通过yum命令我们可以轻松实现软件的下载,查找,卸载与更新等管理软件的操作。 最常用…...

自监督去噪:Recorrupted-to-Recorrupted原理分析与总结
文章目录 1. 方法原理1.1 相关研究1.2 研究思路1.3 小结 2. 实验结果3. 总结 文章地址: https://ieeexplore.ieee.org/document/9577798/footnotes#footnotes 参考博客: https://github.com/PangTongyao/Recorrupted-to-Recorrupted-Unsupervised-Deep-Learning-for-Image-Den…...

【css】css实现水平和垂直居中
通过 justify-content 和 align-items设置水平和垂直居中, justify-content 设置水平方向,align-items设置垂直方向。 代码: <style> .center {display: flex;justify-content: center;align-items: center;height: 200px;border: 3px…...

常见Charles在Windows10抓包乱码问题
废话不多说 直接开整 最近反复安装证书还是乱码 网上各种百度还是不行 首先计算机查看安装好的证书 certmgr.msc 找到并删除掉 重新安装证书 具体解决方法: 第一步:点击 【工具栏–>Proxy–>SSL Proxying Settings…】 第二步:配置…...

汽车维修保养记录查询API:实现车辆健康状况一手掌握
在当今的数字化世界中,汽车维修保养记录的查询和管理变得前所未有地简单和便捷。通过API,我们可以轻松地获取车辆的维修和保养记录,从而实现对手中车辆健康状况的实时掌握。 API(应用程序接口)是进行数据交换和通信的标…...

正则表达式学习记录(Python)
正则表达式学习记录(Python) 一、特殊符号和字符 多个正则表达式匹配 ( | ) 用来分隔不同的匹配模式,相当于逻辑或,可以符合其中任何一个正则表达式 at | home # 表示匹配at或者home bat | bet | bit # 表示匹配bat或…...

Ubuntu20.04操作系统安装Docker
1、添加Docker仓库 curl -fsSL https://mirrors.ustc.edu.cn/docker-ce/linux/ubuntu/gpg | sudo apt-key add -sudo add-apt-repository \"deb [archamd64] https://mirrors.ustc.edu.cn/docker-ce/linux/ubuntu/ \$(lsb_release -cs) \stable"2、安装Docker sudo…...

python制作小程序制作流程,用python编写一个小程序
这篇文章主要介绍了python制作小程序代码宠物运输,具有一定借鉴价值,需要的朋友可以参考下。希望大家阅读完这篇文章后大有收获,下面让小编带着大家一起了解一下。 1 importtkinter2 importtkinter.messagebox3 importmath4 classJSQ:5 6 7 d…...

Github 创建自己的博客网站
参考pku大佬视频制作,附上B站视频:【GitHub Pages 个人网站构建与发布】 同时还参考了:【Python版宝藏级静态站点生成器Material for MkDocs】 GitHub Pages 介绍 内容参考:GitHub Pages - 杨希杰的个人网站 (yang-xijie.githu…...

Windows上安装 jdk 环境并配置环境变量 (超详细教程)
👨🎓博主简介 🏅云计算领域优质创作者 🏅华为云开发者社区专家博主 🏅阿里云开发者社区专家博主 💊交流社区:运维交流社区 欢迎大家的加入! 🐋 希望大家多多支…...

高效构建 vivo 企业级网络流量分析系统
作者:vivo 互联网服务器团队- Ming Yujia 随着网络规模的快速发展,网络状况的良好与否已经直接关系到了企业的日常收益,故障中的每一秒都会导致大量的用户流失与经济亏损。因此,如何快速发现网络问题与定位异常流量已经成为大型企…...

认识awk
awk 认识awk awk是一种编程语言,用于在linux/unix下对文本和数据进行处理。数据可以来自标准输入(stdin)、一个或多个文件,或其它命令的输出。它支持用户自定义函数和动态正则表达式等先进功能,是linux/unix下的一个强大编程工具。它在命令行…...

【C#学习笔记】数组和索引器
文章目录 数组单维数组多维数组交错数组 索引器类上的索引器方法1方法2 接口中的索引器 数组 数组具有以下属性: 数组可以是一维、多维或交错的。创建数组实例时,将建立纬度数量和每个纬度的长度。 这些值在实例的生存期内无法更改。数值数组元素的默认…...

常见距离计算的Python实现
常见的距离有曼哈顿距离、欧式距离、切比雪夫距离、闵可夫斯基距离、汉明距离、余弦距离等,用Python实现计算的方式有多种,可以直接构造公式计算,也可以利用内置线性代数函数计算,还可以利用scipy库计算。 1.曼哈顿距离 也叫城市…...

开发运营监控
DevOps 监控使管理员能够实时了解生产环境中的元素,并有助于确保应用程序平稳运行,同时提供最高的业务价值,对于采用 DevOps 文化和方法的公司来说,这一点至关重要。 什么是开发运营监控 DevOps 通过持续开发、集成、测试、监控…...

食品小程序的制作教程
在今天的互联网时代,小程序已经成为了各行业推广和销售的重要途径。特别是对于食品行业来说,拥有一个专属的小程序商城可以带来更多的用户和销售机会。那么,如何制作一个完美的食品小程序呢?下面就跟随我来一步步教你,…...

Kubernetes(K8s)从入门到精通系列之十三:软件负载平衡选项
Kubernetes K8s从入门到精通系列之十三:软件负载平衡选项 一、软件负载平衡选项二、keepalived and haproxy三、keepalived配置四、haproxy配置五、选项 1:在操作系统上运行服务六、选项 2:将服务作为静态 Pod 运行 一、软件负载平衡选项 当…...

数据特征选择 | Matlab实现具有深度度量学习的时频特征嵌入
文章目录 效果一览文章概述源码设计参考资料效果一览 文章概述 数据特征选择 | Matlab实现具有深度度量学习的时频特征嵌入。 深度度量学习尝试学习非线性特征嵌入或编码器,它可以减少来自同一类的示例之间的距离(度量)并增加来自不同类的示例之间的距离。 以这种方式工作的…...

浅谈webpack
文章目录 Webpackwebpack的工作原理webpack的构建流程Webpack的基本功能有哪些Webpack常用配置 Webpack Webpack是一个现代的JavaScript应用程序静态模块打包工具。它是一个用于构建和打包前端资源的工具,可以将多个模块和资源(如JavaScript、CSS、图片…...

【 stable diffusion LORA模型训练最全最详细教程】
个人网站:https://tianfeng.space/ 文章目录 一、前言二、朱尼酱的赛博丹炉1.介绍2.解压配置3.使用训练准备首页设置上传素材查看进度 三、秋叶的lora训练器1.下载2.预处理3.参数调配 一、前言 其实想写LORA模型训练很久了,一直没时间,总结…...

蓝桥杯上岸每日N题 第八期 (全球变暖)!!!
蓝桥杯上岸每日N题第八期(全球变暖)!!! 同步收录 👇 蓝桥杯上岸必背!!!(第五期BFS) 大家好 我是寸铁💪 冲刺蓝桥杯省一模板大全来啦 🔥 蓝桥杯4月8号就要开始了 &am…...

CSS基础介绍笔记1
官方文档 CSS指的是层叠样式(Cascading Style Sheets)地址:CSS 教程离线文档:放大放小:ctrl鼠标滚动为什么需要css:简化修改HTML元素的样式;将html页面的内容与样式分离提高web开发的工作效率&…...

https请求异常引发(Received fatal alert: unrecognized_name):如何快速解决项目中问题?
总结思考:如何做一个出色的开发者? 首先我们要承认我们大部分程序员是应用开发,不是操作系统、协议、框架开发等这类底层开发者。 其一:是否能快速定位问题。如找到出现问题的代码,bug出现在哪一行,哪个应…...

小程序 view下拉滑动导致scrollview滑动事件失效
小程序页面需要滑动功能 下拉时滑动,展示整个会员卡内容, 下拉view里包含了最近播放:有scrollview,加了下拉功能后,scrollview滑动失败了。 <view class"cover-section" catchtouchstart"handletou…...

《ROS2》教程
参考资料: 古月居 B站视频: https://www.bilibili.com/video/BV16B4y1Q7jQ/?spm_id_from333.999.0.0 对应资料:https://book.guyuehome.com/ ROS之前最好有点ROS1的基础,跳转门:ROS-https://www.bilibili.com/video/B…...

抖音seo源码开发源代码搭建分享
抖音SEO源码开发涉及到以下几个方面: 前端开发:包括抖音SEO页面的设计与布局,以及需要使用到的前端技术,如HTML、CSS、JavaScript等。 后端开发:包括抖音SEO页面的数据获取和处理,以及需要使用到的后端技术…...

MATLAB——使用建立好的神经网络进行分类程序
学习目标:使用建立好的神经网络(训练好并保存,下次直接调用该神经网络)进行分类 clear all; close all; P[-0.4 -0.4 0.5 -0.2 -0.7;-0.6 0.6 -0.4 0.3 0.8]; %输入向量 T[1 1 0 0 1]; …...

Spring5.2.x 源码使用Gradle成功构建
一 前置准备 1 Spring5.2.x下载 1.1 Spring5.2.x Git下载地址 https://gitcode.net/mirrors/spring-projects/spring-framework.git 1.2 Spring5.2.x zip源码包下载,解压后倒入idea https://gitcode.net/mirrors/spring-projects/spring-framework/-/…...

iOS永久签名工具 - 轻松签使用教程
轻松签是一款IOS端免费的IPA签名和安装工具,最新版可以不用依赖证书对ipa永久签名,虽然现在用上了巨魔(TrollStore)- 是国外iOS开发人员opa334dev发布的一款工具,可以在不越狱的情况下,安装任何一款APP。 …...