首页 文章 精选 留言 我的

精选列表

搜索[CEL策略],共10004篇文章
优秀的个人博客,低调大师

Jetpack—LiveData组件的缺陷以及应对策略

一、前言 为了解决Android-App开发以来一直存在的架构设计混乱的问题,谷歌推出了Jetpack-MVVM的全家桶解决方案。作为整个解决方案的核心-LiveData,以其生命周期安全,内存安全等优点,甚至有逐步取代EventBus,RxJava作为Android端状态分发组件的趋势。 官网商城app团队在深度使用LiveData的过程中,也遇到了一些困难,尤其是在LiveData的观察者使用上踩到了不少坑,我们把这些经验在这里做一次总结与分享。 二、Observer到底可以接收多少次回调 2.1 为什么最多收到2个通知 这是一个典型的案例,在调试消息总线的场景时,我们通常会在消息的接收者那里打印一些log日志方便我们定位问题,然而日志的打印有时候也会给我们的问题定位带来一定的迷惑性,可以看下面的例子。 我们首先定义一个极简的ViewModel: public class TestViewModel extends ViewModel { private MutableLiveData<String> currentName; public MutableLiveData<String> getCurrentName() { if (currentName == null) { currentName = new MutableLiveData<String>(); } return currentName; } } 然后看下我们的activity代码; public class JavaTestLiveDataActivity extends AppCompatActivity { private TestViewModel model; private String test="12345"; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_java_test_live_data); model = new ViewModelProvider(this).get(TestViewModel.class); test3(); model.getCurrentName().setValue("3"); } private void test3() { for (int i = 0; i < 10; i++) { model.getCurrentName().observe(this, new Observer<String>() { @Override public void onChanged(String s) { Log.v("ttt", "s:" + s); } }); } } } 大家可以想一下,这段程序运行的结果会是多少?我们创建了一个Livedata,然后对这个Livedata Observe了10次,每次都是new出不同的Observer对象,看上去我们对一个数据源做了10个观察者的绑定。当我们修改这个数据源的时候,我们理应有10条通知。运行一下看看执行结果: 2021-11-21 15:20:07.662 27500-27500/com.smart.myapplication V/ttt: s:3 2021-11-21 15:20:07.662 27500-27500/com.smart.myapplication V/ttt: s:3 奇怪,为什么我明明注册了10个观察者,但是只收到了2个回调通知?换种写法试试? 我们在Log的代码里增加一部分内容比如打印下hashCode再看下执行结果: 2021-11-21 15:22:59.377 27912-27912/com.smart.myapplication V/ttt: s:3 hashCode:217112568 2021-11-21 15:22:59.377 27912-27912/com.smart.myapplication V/ttt: s:3 hashCode:144514257 2021-11-21 15:22:59.377 27912-27912/com.smart.myapplication V/ttt: s:3 hashCode:72557366 2021-11-21 15:22:59.377 27912-27912/com.smart.myapplication V/ttt: s:3 hashCode:233087543 2021-11-21 15:22:59.377 27912-27912/com.smart.myapplication V/ttt: s:3 hashCode:22021028 2021-11-21 15:22:59.377 27912-27912/com.smart.myapplication V/ttt: s:3 hashCode:84260109 2021-11-21 15:22:59.377 27912-27912/com.smart.myapplication V/ttt: s:3 hashCode:94780610 2021-11-21 15:22:59.377 27912-27912/com.smart.myapplication V/ttt: s:3 hashCode:240593619 2021-11-21 15:22:59.377 27912-27912/com.smart.myapplication V/ttt: s:3 hashCode:207336976 2021-11-21 15:22:59.378 27912-27912/com.smart.myapplication V/ttt: s:3 hashCode:82154761 这次结果就正常了,其实对于很多消息总线的调试都有类似的问题。 实际上对于Log系统来说,如果他判定时间戳一致的情况下,后面的Log内容也一致,那么他就不会重复打印内容了。这里一定要注意这个细节,否则在很多时候,会影响我们对问题的判断。再回到我们之前没有添加hashCode的代码,再仔细看看也就明白了:只是Log打印了两条而已,但是通知是收到了10次的,为啥打印两条?因为你的时间戳一致,后续的内容也一致。 2.2 奇怪的编译优化 事情到这还没结束,看下图: 上述的代码跑在android studio里面会变灰,相信很多有代码洁癖的人一看就知道为啥,这不就是Java8的lambda嘛,ide自动给提示给我们让我们优化一下写法呗,而且鼠标一点就自动优化了,贼方便。 灰色没有了,代码变的简洁了,kpi在向我招手了,运行一下试试: 2021-11-21 15:31:50.386 29136-29136/com.smart.myapplication V/ttt: s:3 奇怪,为啥这次只有一个日志了?难道还是Log日志系统的原因?那我加个时间戳试试: 再看下执行结果: 2021-11-21 15:34:33.559 29509-29509/com.smart.myapplication V/ttt: s:3 time:1637480073559 奇怪,为什么还是只打印了一条log?我这里for循环add了10次观察者呀。难道是lambda导致的问题?嗯,我们可以把Observer的数量打出来看看,看看到底是哪里出了问题。看下源码,如下图所示:我们的观察者实际上都是存在这个map里面的,我们取出来这个map的size就可以知道原因了。 反射取一下这个size,注意我们平常使用的LiveData是MutableLiveData,而这个值是在LiveData里,所以是getSuperclass()。 private void hook(LiveData liveData) throws Exception { Field map = liveData.getClass().getSuperclass().getDeclaredField("mObservers"); map.setAccessible(true); SafeIterableMap safeIterableMap = (SafeIterableMap) map.get(liveData); Log.v("ttt", "safeIterableMap size:" + safeIterableMap.size()); } 再看下执行结果: 2021-11-21 15:40:37.010 30043-30043/com.smart.myapplication V/ttt: safeIterableMap size:1 2021-11-21 15:40:37.013 30043-30043/com.smart.myapplication V/ttt: s:3 time:1637480437013 果然这里的map size是1,并不是10,那肯定只能收到1条通知了。那么问题来了,我明明是for循环添加了10个观察者啊,为啥一改成lambda的写法,我的观察者就变成1个了?遇事不决我们反编译(用jadx直接反编译我们的debug app)一下看看。 private void test3() { for (int i = 0; i < 10; i++) { this.model.getCurrentName().observe(this, $$Lambda$JavaTestLiveDataActivity$zcrCJYfWItRTy4AC_xWfANwZkzE.INSTANCE); } } public final /* synthetic */ class $$Lambda$JavaTestLiveDataActivity$zcrCJYfWItRTy4AC_xWfANwZkzE implements Observer { public static final /* synthetic */ $$Lambda$JavaTestLiveDataActivity$zcrCJYfWItRTy4AC_xWfANwZkzE INSTANCE = new $$Lambda$JavaTestLiveDataActivity$zcrCJYfWItRTy4AC_xWfANwZkzE(); private /* synthetic */ $$Lambda$JavaTestLiveDataActivity$zcrCJYfWItRTy4AC_xWfANwZkzE() { } public final void onChanged(Object obj) { Log.v("ttt", "s:" + ((String) obj)); } } 已经很清晰的看出来,这里因为使用了Java8 lambda的写法,所以编译器在编译的过程中自作聪明了一下,自动帮我们优化成都是添加的同一个静态的观察者,并不是10个,这就解释了为什么会出现map size为1的情况了。我们可以再把lambda的写法删除掉,再看看反编译的结果就正常了。 还剩最后一个问题,这个lamda的优化是不分任何场景一直生效的嘛?我们换个写法试试: private String outer = "123456"; private void test3() { for (int i = 0; i < 10; i++) { model.getCurrentName().observe(this, s -> Log.v("ttt", "s:" + s + outer)); } } 注意看,我们这种写法虽然也是用了lambda,但是我们引入了外部变量,和之前的lambda的写法是不一样的,看下这种写法反编译的结果; private void test3() { for (int i = 0; i < 10; i++) { this.model.getCurrentName().observe(this, new Observer() { public final void onChanged(Object obj) { JavaTestLiveDataActivity.this.lambda$test33$0$JavaTestLiveDataActivity((String) obj); } }); } } 看到new关键字就放心了,这种写法就可以绕过Java8 lambda编译的优化了。 1.3 Kotlin的lambda写法会有坑吗 考虑到现在大多数人都会使用Kotlin语言,我们也试试看Kotlin的lamda写法会不会也和Java8的lambda一样会有这种坑? 看下Kotlin中 lambda的写法: fun test2() { val liveData = MutableLiveData<Int>() for (i in 0..9) { liveData.observe(this, { t -> Log.v("ttt", "t:$t") }) } liveData.value = 3 } 再看下反编译的结果: public final void test2() { MutableLiveData liveData = new MutableLiveData(); int i = 0; do { int i2 = i; i++; liveData.observe(this, $$Lambda$KotlinTest$6ZY8yysFE1G_4okj2E0STUBMfmc.INSTANCE); } while (i <= 9); liveData.setValue(3); } public final /* synthetic */ class $$Lambda$KotlinTest$6ZY8yysFE1G_4okj2E0STUBMfmc implements Observer { public static final /* synthetic */ $$Lambda$KotlinTest$6ZY8yysFE1G_4okj2E0STUBMfmc INSTANCE = new $$Lambda$KotlinTest$6ZY8yysFE1G_4okj2E0STUBMfmc(); private /* synthetic */ $$Lambda$KotlinTest$6ZY8yysFE1G_4okj2E0STUBMfmc() { } public final void onChanged(Object obj) { KotlinTest.m1490test2$lambda3((Integer) obj); } } 看来Kotlin的lambda编译和Java8 lambda的编译是一样激进的,都是在for循环的基础上 默认帮你优化成一个对象了。同样的,我们也看看让这个lambda访问外部的变量,看看还有没有这个“负优化”了。 val test="12345" fun test2() { val liveData = MutableLiveData<Int>() for (i in 0..9) { liveData.observe(this, { t -> Log.v("ttt", "t:$t $test") }) } liveData.value = 3 } 看下反编译的结果: public final void test2() { MutableLiveData liveData = new MutableLiveData(); int i = 0; do { int i2 = i; i++; liveData.observe(this, new Observer() { public final void onChanged(Object obj) { KotlinTest.m1490test2$lambda3(KotlinTest.this, (Integer) obj); } }); } while (i <= 9); liveData.setValue(3); } 一切正常了。最后我们再看看 普通Kotlin的非lambda写法 是不是和Java的非lambda写法一样呢? fun test1() { val liveData = MutableLiveData<Int>() for (i in 0..9) { liveData.observe(this, object : Observer<Int> { override fun onChanged(t: Int?) { Log.v("ttt", "t:$t") } }) } liveData.value = 3 } 看下反编译的结果: public final void test11() { MutableLiveData liveData = new MutableLiveData(); int i = 0; do { int i2 = i; i++; liveData.observe(this, new KotlinTest$test11$1()); } while (i <= 9); liveData.setValue(3); } 一切正常,到这里我们就可以下一个结论了。 对于for循环中间使用lambda的场景,当你的lambda中没有使用外部的变量或者函数的时候,那么不管是Java8的编译器还是Kotlin的编译器都会默认帮你优化成使用同一个lambda。 编译器的出发点是好的,for循环中new不同的对象,当然会导致一定程度的性能下降(毕竟new出来的东西最后都是要gc的),但这种优化往往可能不符合我们的预期,甚至有可能在某种场景下造成我们的误判,所以使用的时候一定要小心。 二、LiveData为何会收到Observe之前的消息 2.1 分析源码找原因 我们来看一个例子: fun test1() { val liveData = MutableLiveData<Int>() Log.v("ttt","set live data value") liveData.value = 3 Thread{ Log.v("ttt","wait start") Thread.sleep(3000) runOnUiThread { Log.v("ttt","wait end start observe") liveData.observe(this, { t -> Log.v("ttt", "t:$t") }) } }.start() } 这段代码的意思是我先更新了一个livedata的值为3,然后3s之后我livedata 注册了一个观察者。这里要注意了,我是先更新的livedata的值,过了一段时间以后才注册的观察者,那么此时,理论上我应该是收不到livedata消息的。因为你是先发的消息,我后面才观察的,但程序的执行结果却是: 2021-11-21 16:27:22.306 32275-32275/com.smart.myapplication V/ttt: set live data value 2021-11-21 16:27:22.306 32275-32388/com.smart.myapplication V/ttt: wait start 2021-11-21 16:27:25.311 32275-32275/com.smart.myapplication V/ttt: wait end start observe 2021-11-21 16:27:25.313 32275-32275/com.smart.myapplication V/ttt: t:3 这个就很诡异了,而且不符合一个我们常见的消息总线框架的设计。来看看源码到底是咋回事? 每次observe的时候我们会创建一个wrapper,看下这个wrapper是干啥的。 注意这个wrapper有一个onStateChanged方法,这是整个事件分发的核心,我们暂且记住这个入口,再回到我们之前的observe方法,最后一行是调用了addObserver方法,我们看看这个方法里做了啥。 最终流程会走到这个dispatchEvent方法里,继续跟。 这个mLifeCycleObserver其实就是我们一开始observe那个方法里new出来的LifecycleBoundObserver对象了,也就是那个wrapper的变量。这个onStateChanged方法经过一系列的调用最终会走到如下图所示的considerNotify方法。 而整个considerNotify方法的作用只有一个。 就是判断mLastVersion和mVersion的值,如果mLastVersion的值<mversion的值,那么就会触发observer的onchaged方法了,也就是会回调到我们的观察者方法里面<strong="">。 我们来看看这2个值咋变化的。首先看这个mVersion; 可以看出来这个值默认值就是start_version也就是-1。但是每次setValue的时候这个值都会加1。 而我们observer里面的mLastVersion 它的初始值就是-1。 最后总结一下: Livedata的mVersion初始值是-1。 经过一次setValue以后她的值就变成了0。 后续每次observe的时候会创建一个ObserverWrapper。 Wrapper她里面有一个mLastVersion 这个值是-1,observe的函数调用最终会经过一系列的流程走到considerNotify方法中此时 LiveData的mVersion是0。 0显然是大于observer的mLastVersion-1的,所以此时就一定会触发observer的监听函数了。 2.2 配合ActivityViewModels要小心 Livedata的这种特性,在某些场景下会引发灾难性的后果,比如说,单Activity多Fragment的场景下,在没有Jetpack-mvvm组件之前,要让Activity-Fragment 实现数据同步是很不方便的 ,但是有了Jetpack-mvvm组件之后,要实现这套机制会变的非常容易。可以看下官网上的例子: class SharedViewModel : ViewModel() { val selected = MutableLiveData<Item>() fun select(item: Item) { selected.value = item } } class MasterFragment : Fragment() { private lateinit var itemSelector: Selector private val model: SharedViewModel by activityViewModels() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) itemSelector.setOnClickListener { item -> // Update the UI } } } class DetailFragment : Fragment() { private val model: SharedViewModel by activityViewModels() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) model.selected.observe(viewLifecycleOwner, Observer<Item> { item -> // Update the UI }) } } 只要让2个fragment之间共享这套 ActivityViewModel 即可。使用起来很方便,但是某些场景下却会导致一些严重问题。来看这个场景,我们有一个activity默认显ListFragment,点击了ListFragment以后我们会跳转到DetailFragment,来看下代码: class ListViewModel : ViewModel() { private val _navigateToDetails = MutableLiveData<Boolean>() val navigateToDetails : LiveData<Boolean> get() = _navigateToDetails fun userClicksOnButton() { _navigateToDetails.value = true } } 再看下核心的ListFragment; class ListFragment : Fragment() { private val model: ListViewModel by activityViewModels() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) model.navigateToDetails.observe(viewLifecycleOwner, { t -> if (t) { parentFragmentManager.commit { replace<DetailFragment>(R.id.fragment_container_view) addToBackStack("name") } } }) } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { // Inflate the layout for this fragment return inflater.inflate(R.layout.fragment_list, container, false).apply { findViewById<View>(R.id.to_detail).setOnClickListener { model.userClicksOnButton() } } } } 可以看出来我们的实现机制就是点击了按钮以后我们调用viewModel的userClicksOnButton方法将navigateToDetails这个livedata的值改成true,然后监听这个LiveData值,如果是true的话就跳转到Detail 这个详情的fragment。 这个流程初看是没问题的,点击以后确实能跳转到DetailFragment,但是当我们在DetailFragment页面点击了返回键以后,理论上会回到ListFragment,但实际的执行结果是回到ListFragment以后马上又跳到DetailFragment了。 这是为啥?问题其实就出现在Fragment生命周期这里,当你按了返回键以后,ListFragment的onViewCreated又一次会被执行,然后这次你observe了,Livedata之前的值是true,于是又会触发跳转到DetailFragment的流程。导致你的页面再也回不到列表页了。 2.3 解决方案一:引入中间层 俗话说的好,计算机领域中的所有问题都可以通过引入一个中间层来解决。这里也一样,我们可以尝试“一个消息只被消费一次”的思路来解决上述的问题。例如我们将LiveData的值包一层: class ListViewModel : ViewModel() { private val _navigateToDetails = MutableLiveData<Event<Boolean>>() val navigateToDetails : LiveData<Event<Boolean>> get() = _navigateToDetails fun userClicksOnButton() { _navigateToDetails.value = Event(true) } } open class Event<out T>(private val content: T) { var hasBeenHandled = false private set // 只允许外部读 不允许外部写这个值 /** * 通过这个函数取的value 只能被消费一次 */ fun getContentIfNotHandled(): T? { return if (hasBeenHandled) { null } else { hasBeenHandled = true content } } /** * 如果想消费之前的value 那就直接调用这个方法即可 */ fun peekContent(): T = content } 这样我们在做监听的时候只要调用getContentIfNotHandled()这个方法即可: model.navigateToDetails.observe(viewLifecycleOwner, { t -> t.getContentIfNotHandled()?.let { if (it){ parentFragmentManager.commit { replace<DetailFragment>(R.id.fragment_container_view) addToBackStack("name") } } } }) 2.4 解决方案二:Hook LiveData的observe方法 前文我们分析过,每次observe的时候,mLastVersion的值小于 mVersion的值 是问题产生的根源,那我们利用反射,每次observer的时候将mLastVersion的值设置成与version相等不就行了么。 class SmartLiveData<T> : MutableLiveData<T>() { override fun observe(owner: LifecycleOwner, observer: Observer<in T>) { super.observe(owner, observer) //get livedata version val livedataVersion = javaClass.superclass.superclass.getDeclaredField("mVersion") livedataVersion.isAccessible = true // 获取livedata version的值 val livedataVerionValue = livedataVersion.get(this) // 取 mObservers Filed val mObserversFiled = javaClass.superclass.superclass.getDeclaredField("mObservers") mObserversFiled.isAccessible = true // 取 mObservers 对象 val objectObservers = mObserversFiled.get(this) // 取 mObservers 对象 所属的class SafeIterableMap val objectObserversClass = objectObservers.javaClass val methodGet = objectObserversClass.getDeclaredMethod("get", Any::class.java) methodGet.isAccessible = true //LifecycleBoundObserver val objectWrapper = (methodGet.invoke(objectObservers, observer) as Map.Entry<*, *>).value //ObserverWrapper val mLastVersionField = objectWrapper!!.javaClass.superclass.getDeclaredField("mLastVersion") mLastVersionField.isAccessible = true //将 mVersion的值 赋值给 mLastVersion 使其相等 mLastVersionField.set(objectWrapper, livedataVerionValue) } } 2.5 解决方案三:使用Kotlin-Flow 如果你还在使用Kotlin,那么此问题的解决方案则更加简单,甚至连过程都变的可控。在今年的谷歌I/O大会中,Yigit 在Jetpack的 AMA 中明确指出了 Livedata的存在就是为了照顾Java的使用者,短期内会继续维护(含义是什么大家自己品品),作为Livedata的替代品Flow会在今后渐渐成为主流(毕竟现在Kotlin渐渐成为主流),那如果使用了Flow,上述的情况则可以迎刃而解。 改写viewModel class ListViewModel : ViewModel() { val _navigateToDetails = MutableSharedFlow<Boolean>() fun userClicksOnButton() { viewModelScope.launch { _navigateToDetails.emit(true) } } } 然后改写下监听的方式即可; override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) lifecycleScope.launch { model._navigateToDetails.collect { if (it) { parentFragmentManager.commit { replace<DetailFragment>(R.id.fragment_container_view) addToBackStack("name") } } } } } 我们重点看SharedFlow这个热流的构造函数; 他的实际作用就是:当有新的订阅者collect的时候(可以理解为collect就是Livedata中的observe),发送几个(replay)collect之前已经发送过的数据给它,默认值是0。所以我们上述的代码是不会收到之前的消息的。大家在这里可以试一下 把这个replay改成1,即可复现之前Livedata的问题。相比于前面两种解决方案,这个方案更加优秀,唯一的缺点就是Flow不支持Java,仅支持Kotlin。 三、总结 整体上来说,即使现在有了Kotlin Flow,LiveData也依旧是目前Android客户端架构组件中不可缺少的一环,毕竟它的生命周期安全和内存安全实在是太香,可以有效降低我们平常业务开发中的负担,在使用他的时候我们只要关注3个方面即可避坑: 谨慎使用Android Studio给出的lambda智能提示 多关注是否真的需要Observe 在注册监听之前的消息 Activity与Fragment之间使用ActivityViewModel时要小心处理。 作者:vivo互联网前端团队-Wu Yue

优秀的个人博客,低调大师

架构设计策略之风险驱动设计

风险驱动架构设计 风险驱动设计 风险 识别风险 描述风险 风险指导架构设计 技术选择 设定风险阈值 总结 你今日预咗风险未? 每次与项目成员沟通后,应该都会预感项目有风险吧。 大概都会遇到这些问题:进度风险、技术难点、需求变更、资源分配问题等等。 别和我说,基本没啥风险,项目很稳定。真有这样没风险的项目,那就不需要架构师了。毕竟这样的项目肯定会有成熟的“轮子”,直接借鉴就好了。 实际上,没风险的项目很少,需要开发的软件项目总有风险的,因为实际需求都是随时间变化的。 我们日常生活很多事情都是看风险来做决定的,最明显莫过于股票、基金等投资行为,在自己能接受的风险范围内进行决策。这是因为风险可以反映出真实世界的不确定性,而且是一个很好的决策指示器,它能帮助我们看清障碍,做出合理的选择。 因此,风险是可以指导我们进行优秀的架构设计,也就是说我们可以采用风险驱动模型进行架构设计。 风险驱动设计 风险驱动模型就是以风险为中心,根据合乎逻辑的理由进行决策权衡的模型。 采用风险驱动的话,必须要能回答以下这些问题: 项目的主要失败风险有哪些? 应对失败风险的技术有哪些? 何时结束和恢复架构设计? 为了解答以上问题,主要的对策有: 识别风险、描述风险 选择技术降低风险 设定风险阈值,权衡架构设计 下面就将详细讲解以上的问题和对策。 风险 风险是指一个事件产生我们所不希望的后果的可能性。对于已经发生的,应该称之为事故。 以上对风险的定义无法直观衡量,我们借鉴下在工程领域对风险的定义,即: 风险 = 失败的概率 x 失败带来的影响 但定义中失败的概率和影响大部分的时候是难以精确度量的,除非我们能觉察到风险,也就是说只能在已知的条件下去定义风险。因此,风险的定义就变为: 当前已知条件下的风险 = 觉察到的失败的概率 x 觉察到的影响 此定义是让我们接受现实,尽力去降低觉察到风险,使架构设计达到已知的最优。 有了定义,首先我们要识别风险,然后就可以去描述风险,更深入地理解风险。 识别风险 识别风险的方法主要有: 确定难以实现的需求。例如,复杂难以理解的业务问题,就需要开需求会议来评估风险。 识别不完整或难以理解的质量属性(非功能属性)需求。例如,“保证可靠性”这种不可测试的质量属性,就需要开发质量属性研讨会来识别风险。 借鉴典型风险来识别。例如,web项目必须要关注安全性。 风险分类排序。以利益相关方需求的优先级和开发者觉察到的难度来进行风险分类排序。 描述风险 根据风险的定义,描述风险至少需要三个部分:条件、后果、优先级。条件是当前风险可能发生的实际情况,后果(觉察到的影响)是由条件引发的将来可能出现的不良状况,优先级(觉察到的失败的概率和影响等来确定)是已知条件下风险的重要程度。 然而,这样的描述方式缺乏项目交叉引用的能力,因此为了标识风险以及便于沟通理解,我们还需要加入四个部分:名称、编号、类型、来源。 我们可以采用表格的形式记录风险。例如,可以用表1作为示例来描述风险。 表1 描述风险 风险名称 风险编号 风险类型 条件 后果 优先级 来源 数据处理风险 R1.1 数据风险 业务系统CPU、磁盘和网络带宽已占有80%,数据处理将消耗大量的时间和资源 可能无法顺利完成任务 高 数据需求D1.1:必须1小时内处理完数据 通过描述理解了风险后,我们可以通过以下方式去降低或消除风险: 降低概率。减少业务系统的资源占有。 减少影响。优化数据处理的能力,提升资源的利用率。 减小风险发生的时间窗口。在业务系统空闲时,进行数据处理。 移除条件。增加CPU、磁盘和网络带等资源,或需求来源方沟通,降低质量属性的要求 接受现状,等接近不可接受的程度再着手处理。优先级不高,数据处理超时和资源不够用的情况较少出现,等解决完优先级高的风险后或风险优先级上升后再处理。 风险指导架构设计 一旦能清楚地识别和描述所面临的风险,就可以借助风险来指导架构设计,进而运用合适的技术去降低或消除风。 可以把风险理解为架构设计的GPS,它帮助我们进行架构设计的定位,告知我们目前在何地距离要去的目的地还有多远。 技术选择 根据风险选择技术是最敏捷高效的,这样就不会把时间和资源浪费在那些低效的技术上,也不会忽略那些危及项目的风险,主动地推动我们进行最优的技术选型。 借助风险选择技术的方法主要有: 分析风险的条件、影响、概率、时间窗口,确定可以解决的部分,进而选择解决问题的技术。 借鉴业界成熟解决方案。 研究密切相关的技术,寻找解决风险的技术 为了实现架构设计的可重复性,我们还可以总结经验,做出风险指导架构设计的指南。例如: 面临的风险 解决的技术 时间和成本预计 数据处理无法顺利完成 增加硬件资源、引入数据处理框架 硬件资源每年花费XX元,引入XX框架学习成本是x个人天 设定风险阈值 风险的阈值到底是多少? 我们参考下《恰如其分的软件架构》对风险阈值的定义:风险降低到不再是系统中最大风险源的地步。当然这是主观的判断,但只要能保证所设计的架构能克服所面临的失败即可。 如果架构不再是系统中最大的风险源,就可以从主动设计转为被动设计,否则从被动设计转为主动设计。主动设计是指主动设法降低或消除风险。被动设计是指在需求变更、出现未知的性能问题等需要及时纠正的情况。 需要注意的是,架构还可以随时重新变回重大的风险源,这可能是因为现实环境的变化、重大的需求变更、不可控的项目管理风险等等。当这些情况出现时,肯定要切换回主动设计模型。 总结 运用风险驱动设计的方法,可以识别和描述风险,并选择一套架构和设计技术来降低或消除这些风险,还可以根据风险的重要程度和阈值,把握住架构设计的度,进行最高效敏捷的设计。

优秀的个人博客,低调大师

Mysql的优化策略,提升PHP的运行效率

为了提升PHP的运行效率,程序员不光需要写出逻辑清晰,效率很高的代码,还要能对query语句进行优化。虽然我们对数据库的读取写入速度上却是无能为力,但在一些数据库类扩展像memcache、mongodb、redis这样的数据存储服务器的帮助下,PHP也能达到更快的存取速度,所以了解学习这些扩展也是非常必要。 大型存储方面优化 数据库主从复制和读写分离 1、master将改变记录到二进制日志中,slave将master的二进制拷贝到它的中继日志中,重新将数据返回到它自己的数据中,达到复制主服务器数据的目的。 主从复制可以用作:数据库负载均衡、数据库备份、读写分离等功能。 2、配置主服务器master 修改my.ini/my.conf [mysqld] log-bin=mysql-bin //启用二进制日志 server-id=102 //服务器唯一ID 3、配置从服务器slave log-bin=mysql-bin //启用二进制日志 server-id=226 //服务器唯一ID 4、在主服务器上授权从服务器 GRANT REPLICATION SLAVE ON *.* to 'slavename'@'IP' identified by 'root' 5、在从服务器上使用 change master to master_host="masterip", master_user="masteruser", master_password="masterpasswd"; 6、然后使用start slave命令开始进行主从复制。 不要忘记在每次修改配置后重启服务器,然后可以在主从服务器上用show master/slave status查看主/从状态。 实现数据库的读写分离要依赖MySQL的中间件,如mysql_proxy,atlas等。通过配置这些中间件来对主从服务器进行读写分离,使从服务器承担被读取的责任,从而减轻主服务器的负担。 数据库的sharding 在数据库中数据表中的数据量非常庞大的时候,无论是索引还是缓存等压力都很大,对数据库进行sharding,使之分别以多个数据库服务器或多个表存储,以减轻查询压力。方式有垂直切分、水平切分和联合切分。 垂直切分:在数据表非常多的时候,把数据库中关系紧密(如同一模块,经常连接查询)的表切分出来分别放到不同的主从server上。 水平切分:在表不多,而表里的数据量非常大的时候,为了加快查询,可以用哈希等算法,将一个数据表分为几个,分别放到不同的服务器上,加快查询。水平切分和数据表分区的区别在于其存储介质上的不同。 联合切分:更多的情况是数据表和表中的数据量都非常大,则要进行联合切分,即同时进行垂直和水平分表,将数据库切分为一个分布式的矩阵来存储。 这些数据库的优化方式,每一种拿出来都可以写作一篇文章,可谓是博大精深,了解并记忆了这些方式,可以在有需要的时候进行有目的的选择优化,达到数据库效率的高效。 索引方面优化 在MySQL中,索引属于存储引擎级别的概念,不同存储引擎对索引的实现方式是不同的,下面主要讨论MyISAM和InnoDB两个存储引擎的索引实现方式。 MyISAM索引实现 MyISAM引擎使用B+Tree作为索引结构,叶节点的data域存放的是数据记录的地址。下图是MyISAM索引的原理图: 这里设表一共有三列,假设我们以Col1为主键,则图1是一个MyISAM表的主索引(Primary key)示意。可以看出MyISAM的索引文件仅仅保存数据记录的地址。在MyISAM中,主索引和辅助索引(Secondary key)在结构上没有任何区别,只是主索引要求key是唯一的,而辅助索引的key可以重复。如果我们在Col2上建立一个辅助索引,则此索引的结构如下图所示: 同样也是一颗B+Tree,data域保存数据记录的地址。因此,MyISAM中索引检索的算法为首先按照B+Tree搜索算法搜索索引,如果指定的Key存在,则取出其data域的值,然后以data域的值为地址,读取相应数据记录。 MyISAM的索引方式也叫做“非聚集”的,之所以这么称呼是为了与InnoDB的聚集索引区分。 InnoDB索引实现 虽然InnoDB也使用B+Tree作为索引结构,但具体实现方式却与MyISAM截然不同。 第一个重大区别是InnoDB的数据文件本身就是索引文件。从上文知道,MyISAM索引文件和数据文件是分离的,索引文件仅保存数据记录的地址。而在InnoDB中,表数据文件本身就是按B+Tree组织的一个索引结构,这棵树的叶节点data域保存了完整的数据记录。这个索引的key是数据表的主键,因此InnoDB表数据文件本身就是主索引。 图3 图3是InnoDB主索引(同时也是数据文件)的示意图,可以看到叶节点包含了完整的数据记录。这种索引叫做聚集索引。因为InnoDB的数据文件本身要按主键聚集,所以InnoDB要求表必须有主键(MyISAM可以没有),如果没有显式指定,则MySQL系统会自动选择一个可以唯一标识数据记录的列作为主键,如果不存在这种列,则MySQL自动为InnoDB表生成一个隐含字段作为主键,这个字段长度为6个字节,类型为长整形。 第二个与MyISAM索引的不同是InnoDB的辅助索引data域存储相应记录主键的值而不是地址。换句话说,InnoDB的所有辅助索引都引用主键作为data域。例如,图4为定义在Col3上的一个辅助索引: 图4 这里以英文字符的ASCII码作为比较准则。聚集索引这种实现方式使得按主键的搜索十分高效,但是辅助索引搜索需要检索两遍索引:首先检索辅助索引获得主键,然后用主键到主索引中检索获得记录。 了解不同存储引擎的索引实现方式对于正确使用和优化索引都非常有帮助,例如知道了InnoDB的索引实现后,就很容易明白为什么不建议使用过长的字段作为主键,因为所有辅助索引都引用主索引,过长的主索引会令辅助索引变得过大。 再例如,用非单调的字段作为主键在InnoDB中不是个好主意,因为InnoDB数据文件本身是一颗B+Tree,非单调的主键会造成在插入新记录时数据文件为了维持B+Tree的特性而频繁的分裂调整,十分低效,而使用自增字段作为主键则是一个很好的选择。 数据查询方面优化 在每一个消耗大量时间的查询案例中,都能看到一些不必要的额外操作、某些操作被额外地重复了很多次、某些操作执行得太慢等。优化查询的目的就是减少和消除这些操作所花费的时间。 一、首选要优化数据访问 查询性能底下最基本的原因是访问的数据太多。所以,对于低效的查询,一般通过两个步骤来分析: 确认应用程序是否在检索大量超过需要的数据。这通常意味着访问了太多的行,但有时候也可能是访问了太多的列。确认MySQL服务器层是否在分析大量超过需要的数据行。 1.1、是否向数据库请求了不需要的数据 在访问数据库时,应该只请求需要的行和列,请求多余的行和列会消耗MySQL服务器的CPU和内存资源,并增加网络开销。 1、在处理分页时,应该使用LIMIT限制MySQL只返回需要的数据,而不是向应用程序返回全部数据后,再由应用程序过滤不需要的行。 2、多表关联时,或获取单表数据时,尽量避免不加思考地使用SELECT * 3、当一些数据被多次使用时可以考虑将数据缓存起来,避免每次使用都要到MySQL查询。 1.2、MySQL是否在扫描额外的记录,应该让MySQL使用最合适的方式查询数据 对于MySQL,最简单的衡量查询开销有三个指标:响应时间、扫描的行数和返回的行数。这里主要考虑提高扫描的方式,即查询数据的方式。 查询数据的方式有全表扫描、索引扫描、范围扫描、唯一索引查询、常数引用等。这些查询方式,速度从慢到快,扫描的行数也是从多到少。可以通过EXPLAIN语句中的type列反应查询采用的是哪种方式。 通常可以通过添加合适的索引改善查询数据的方式,使其尽可能减少扫描的数据行,加快查询速度。 例如,当发现查询需要扫描大量的数据行但只返回少数的行,那么可以考虑使用覆盖索引,即把所有需要用到的列都放到索引中。这样存储引擎无须回表获取对应行就可以返回结果了。 二、重构查询的方法 设计查询的时候需要考虑是否需要把一个复杂的查询分成多个简单的查询。在我的印象中,曾经无数次听到一个经验法则:可以在数据库中做的事不要放在应用程序中,数据库比我们想象的要厉害的多。这个经验法则是在华夏基金使用Oracle编写SQL时一位Oracle牛人告诉我的,后来我把它使用到MySQL上,真是吃尽苦头。 当然这其中的原因有Oracle和MySQL原本就不是一样的处理逻辑,并且现在的网络通信、查询解析和优化的代价并没有以前那么高啦。再次说明,经验法则有在某种特定笼子里才有效。 分解复杂的查询: 可以将一个大查询切分成多个小查询执行,每个小查询只完成整个查询任务的一小部分,每次只返回一小部分结果。 删除旧的数据是一个很好的例子。 如果只用一条语句一次性执行一个大的删除操作,则可能需要一次锁住很多数据,占满整个事务日志,耗尽系统资源、阻塞很多小的但重要的查询。将一个大的删除操作分解成多个较小的删除操作可以将服务器上原本一次性的压力分散到多次操作上,尽可能小地影响MySQL性能,减少删除时锁的等待时间,同时也减少了MySQL主从复制的延迟。这个方法我一直在用。 另一个例子是分解关联查询,即对每个要关联的表进行单表查询,然后将结果在应用程序中进行关联。我在之前一家公司和一位在阿里待过很多年的同事一起编码时,他就是这么干的。后来我在心中默默地鄙视着他,因为我心里有这么一个经验法则(可以在数据库中做的事不要放在应用程序中,数据库比我们想象的要厉害的多),并且我在行动上也是保持能用一个SQL解决的事绝对不会用两个SQL。 这么做当然处理经验法则的原因之外还有一个原因是:获取数据的逻辑尽量与业务代码分离,这样以后在切换数据库时也很方便。实际上是这样吗?未必啊。那次的无知让我吃尽苦头啊,后来因为SQL的性能问题再把我写的大部分SQL进行分解。 用分解关联查询的方式重构查询有如下的优势: 让缓存的效率更高。许多应用程序可以方便地缓存单表查询对应的结果对象。将查询分解后,执行单个查询可以减少锁的竞争。在应用层做关联,可以更容易对数据库进行拆分,更容易做到高性能和可扩展。查询本身效率也可能会有所提升。可以减少冗余记录的查询。在应用层做关联查询, 意味着对于某条记录应用只需要查询一次,而在数据库中做关联查询,则可能需要重复地访问一部分数据。从这点看,这样的重构还可能会减少网络和内存的消耗。更进一步,这样做相当于在应用中实现了哈希关联,而不是使用MySQL的嵌套循环关联。某些场景哈希关联的效率要高很多。 数据库设计方面优化 1、数据库设计符合第三范式,为了查询方便可以有一定的数据冗余。 2、选择数据类型优先级 int > date,time > enum,char>varchar > blob,选择数据类型时,可以考虑替换,如ip地址可以用ip2long()函数转换为unsign int型来进行存储。 3、对于char(n)类型,在数据完整的情况下尽量较小的的n值。 4、在建表时用partition命令对单个表分区可以大大提升查询效率,MySQL支持RANGE,LIST,HASH,KEY分区类型,其中以RANGE最为常用,分区方式为: CREATE TABLE tablename{ }ENGINE innodb/myisam CHARSET utf8 //选择数据库引擎和编码 PARTITION BY RANGE/LIST(column),//按范围和预定义列表进行分区 PARTITION partname VALUES LESS THAN /IN(n),//命名分区并详细限定分区的范围 5、选择数据库引擎时要注意innodb 和 myisam的区别。 存储结构:MyISAM在磁盘上存储成三个文件。而InnoDB所有的表都保存在同一个数据文件中,一般为2GB 事务支持:MyISAM不提供事务支持。InnoDB提供事务支持事务。 表锁差异:MyISAM只支持表级锁。InnoDB支持事务和行级锁。 全文索引:MyISAM支持 FULLTEXT类型的全文索引(不适用中文,所以要用sphinx全文索引引擎)。InnoDB不支持。 表的具体行数:MyISAM保存有表的总行数,查询count(*)很快。InnoDB没有保存表的总行数,需要重新计算。 外键:MyISAM不支持。InnoDB支持 几条MySQL小技巧 1、SQL语句中的关键词最好用大写来书写,第一易于区分关键词和操作对象,第二,SQL语句在执行时,MySQL会将其转换为大写,手动写大写能增加查询效率(虽然很小)。 2、如果我们们经对数据库中的数据行进行增删,那么会出现数据ID过大的情况,用ALTER TABLE tablename AUTO_INCREMENT=N,使自增ID从N开始计数。 3、对int类型添加 ZEROFILL 属性可以对数据进行自动补0 4、导入大量数据时最好先删除索引再插入数据,再加入索引,不然,mysql会花费大量时间在更新索引上。 5、创建数据库书写sql语句时 ,我们可以在IDE里创建一个后缀为.sql的文件,IDE会识别sql语法,更易于书写。更重要的是,如果你的数据库丢失了,你还可以找到这个文件,在当前目录下使用/path/mysql -uusername -ppassword databasename < filename.sql来执行整个文件的sql语句(注意-u和-p后紧跟用户名密码,无空格)。 以上内容希望帮助到大家,很多PHPer在进阶的时候总会遇到一些问题和瓶颈,业务代码写多了没有方向感,不知道该从那里入手去提升,对此我整理了一些资料,包括但不限于:分布式架构、高可扩展、高性能、高并发、服务器性能调优、TP6,laravel,Redis,Swoole、Swoft、Kafka、Mysql优化、shell脚本、Docker、微服务、Nginx等多个知识点高级进阶干货需要的可以免费分享给大家,需要戳这里PHP进阶架构师>>>实战视频、大厂面试文档免费获取

优秀的个人博客,低调大师

SAP ABAP Netweaver和Hybris Commerce的部署策略

我们都知道Netweaver经典的三层架构,既能部署在Linux/Unix上也能部署在Windows OS上.https://help.sap.com/doc/1080eced90cf4c7a94858c56e8203257/CURRENT_VERSION/en-US/SystemCopy_70X_win_aj.pdftcode SM51能看到一个逻辑的application server比如AG3后面的物理server instance: 这些物理server instance共享同一个DB.Hybris的部署方式有三种,单instance,多instance和多tenant。下图的cluster mode就对应上图的AG3这种部署方式,而Multi-tenant mode就对应C4C的部署方式,唯一区别就是Hybris里不同客户拥有自己的tenant,数据是通过database table prefix隔离的,而C4C里数据隔离是通过client做的。 对于成都开发团队来说,开发环境肯定采取的是最简单的单instance mode.开发环境里有一个嵌入的tomcat server: 我们直接执行tomcat里这个bat启动tomcat: 这个bat里会首先检测当前os类型,然后执行对应的执行文件: 在我的laptop上,执行这个x86的exe: 本文来自云栖社区合作伙伴“汪子熙”,了解相关信息可以关注微信公众号"汪子熙"。

优秀的个人博客,低调大师

每日一博 | Gitee 存储库体积控制策略

前言 作为全球第二大的代码托管平台,Gitee 拥有350W 用户和 600W 存储库,海量的存储库对 Gitee 的硬件设施提出了更高的要求,以 600W 存储库为例,如果按照平均 1GB 的大小磁盘体积,这些存储库将需要总共 5860 TB 的空间,按照 Gitee 每台存储磁盘 14TB,则需要 419 台存储设备。实际上在 Gitee 中,绝大多数存储库的体积都小于 100 M,超过 100 M 的存储库通常都是未合理使用 git。尽管如此,Gitee 仍然需要投入大量的存储设备来支撑用户的接入。为了能够让更多的人能够免费使用 Gitee,我们迫不得已只能限制大存储库的访问。近期,Gitee 存储库路由架构改造第一阶段已经到了收尾截断,在此期间,我们将陆续将服务端的钩子切换到 GNK (Gitee Native Hook),GNK 基于 C++ 编写,使用了 Git 环境隔离等高级特定,意味着大文件检测和存储库体积检测不会再有漏网之鱼。一些用户的存储库体积已经超过了 Gitee 配额限制,而之前的钩子检测存在缺陷,无法实时拦截大存储库和大文件,当切换到 GNK 后,这些用户修改他们的存储库却无法推送到 Gitee,这让他们产生了困扰,本文就这一困扰解答若干问题。当然如果用户有其他问题也可以在本文下留言。 Gitee 套餐信息 Gitee 分为普通用户,和企业用户,个人的套餐实际和企业免费版一致,最新的套餐信息 如下: 大存储库的产生 Git 是基于文件快照的,但并不是所有时候都存储快照,当对象被打包到 .pack 文件中时,则有可能存储对象的差异,即 OBJ_OFS_DELTA/OBJ_REF_DELTA。这种机制使得在文件大小不变的情况下,每次修改文件都会导致存储库体积按大于文件压缩后体积增长,以一个 Zip 压缩文件为例,每次修改后大小为 50 MB,提交 20次便能够超过 1000 MB。当用户将项目中的构建文件诸如 .exe,.pdb,.so,.jar 或者依赖文件/文件夹 node_modules,packages 以及资源文件 .psd,.raw,.avi,.jpg 添加到版本控制中后,很容易使得存储库体积超出限制。 大文件除了容易让存储库超出限制,而且也会降低用户拉取代码的体验,git 在处理二进制文件时需要花费更多的 CPU。 GNK 的拦截 随着存储服务器陆续切换到 GNK,超出配额的报告也可能会更多。GNK 的拦截是实时的,即用户将代码推送到服务器后,git-receive-pack 将调用 GNK 中的 pre-receive 钩子,这个钩子将统计存储库的 objects 目录,将所有文件和目录所占用的磁盘空间大小累加就获得了存储库的体积,这和 du -sh 机制一致,特别的,我们这里说的存储库体积包含其附带的 wiki 存储库的目录,这也是为了避免个别用户使用 wiki 存储大文件。 GNK 目前会给大存储库推送提供三次机会,如果三次读没有减小存储库体积,则便无法继续修改远程存储库,以 Linux 内核 1.4 G 为例,如下所示: 第三次时如果存储库体积依然超出限制,则会告知用户,已经耗尽所有机会,无法重试。 无法尝试时,输出如下: GNK 还会拦截用户推送的大文件,但已经存在于存储库的大文件,GNK 不会去检测。基于 Git 环境隔离机制实现的 GNK,拦截大文件不会像之前一样反复推送大文件失败导致存储库体积变大,环境隔离目录在 pre-receive 执行失败后会被删除。 存储库的精简方案 在开发的时候,我们可以使用包管理工具管理项目依赖,比如 dotnet core 使用 NuGet,Java 使用 Maven,完全没有必要将依赖二进制纳入版本控制。 如果不慎将大文件纳入了版本控制中,可以去访问 Gitee 帮助:仓库体积过大,如何减小?。在修改本地存储库后,通过 git push -f 的方式推送到 Gitee,然后在项目配置页面运行 Git GC 待 GC 运行后,通常能够发现存储库体积变小。 如果项目使用 PR 机制参与协作开发,强制推送后运行 git gc 可能不会减小存储库的体积(这是因为存储库需要使用内部引用保持 PR 相应的 commit 的可用性,不被 GC),这个时候用户可以升级套餐,获得更大的存储库容量,或者在 Gitee 上新建一个空存储库,将修改历史记录的存储库推送到新的存储库。使用新的存储库即可。 一些大文件无法用版本控制可以将其使用 Git LFS 管理,用户需要使用 Git LFS 可以查看 Gitee LFS。 对于一些用户由于疏忽已经用完重试次数则可以联系官方团队重置重试次数 最后 无论如何,给用户带来困扰,我们很抱歉,但 Gitee 也是需要不断改进,不断的完善。随着 GNK 迁移完成和前端切换,Gitee 的路由架构改造第一阶段也将完成,这使得用户改名,存储库改名,存储库忽略大小写,fork 能够更容易实现或者更加快速。

资源下载

更多资源
腾讯云软件源

腾讯云软件源

为解决软件依赖安装时官方源访问速度慢的问题,腾讯云为一些软件搭建了缓存服务。您可以通过使用腾讯云软件源站来提升依赖包的安装速度。为了方便用户自由搭建服务架构,目前腾讯云软件源站支持公网访问和内网访问。

Nacos

Nacos

Nacos /nɑ:kəʊs/ 是 Dynamic Naming and Configuration Service 的首字母简称,一个易于构建 AI Agent 应用的动态服务发现、配置管理和AI智能体管理平台。Nacos 致力于帮助您发现、配置和管理微服务及AI智能体应用。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据、流量管理。Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。

Spring

Spring

Spring框架(Spring Framework)是由Rod Johnson于2002年提出的开源Java企业级应用框架,旨在通过使用JavaBean替代传统EJB实现方式降低企业级编程开发的复杂性。该框架基于简单性、可测试性和松耦合性设计理念,提供核心容器、应用上下文、数据访问集成等模块,支持整合Hibernate、Struts等第三方框架,其适用范围不仅限于服务器端开发,绝大多数Java应用均可从中受益。

Rocky Linux

Rocky Linux

Rocky Linux(中文名:洛基)是由Gregory Kurtzer于2020年12月发起的企业级Linux发行版,作为CentOS稳定版停止维护后与RHEL(Red Hat Enterprise Linux)完全兼容的开源替代方案,由社区拥有并管理,支持x86_64、aarch64等架构。其通过重新编译RHEL源代码提供长期稳定性,采用模块化包装和SELinux安全架构,默认包含GNOME桌面环境及XFS文件系统,支持十年生命周期更新。

用户登录
用户注册