解读JVM级别本地缓存Caffeine青出于蓝的要诀3

news2024/10/7 16:20:42

Caffeine的异步淘汰清理机制

在惰性删除实现机制这边,Caffeine做了一些改进优化以提升在并发场景下的性能表现。我们可以和Guava Cache的基于容量大小的淘汰处理做个对比。

当限制了Guava Cache最大容量之后,有新的记录写入超过了总大小,会理解触发数据淘汰策略,然后腾出空间给新的记录写入。比如下面这段逻辑:

public static void main(String[] args) {
    Cache<String, String> cache = CacheBuilder.newBuilder()
            .maximumSize(1)
            .removalListener(notification -> System.out.println(notification.getKey() + "被移除,原因:" + notification.getCause()))
            .build();
    cache.put("key1", "value1");
    System.out.println("key1写入后,当前缓存内的keys:" + cache.asMap().keySet());
    cache.put("key2", "value1");
    System.out.println("key2写入后,当前缓存内的keys:" + cache.asMap().keySet());
}

其运行后的结果显示如下,可以很明显的看出,超出容量之后继续写入,会在写入前先执行缓存移除操作。

key1写入后,当前缓存内的keys:[key1]
key1被移除,原因:SIZE
key2写入后,当前缓存内的keys:[key2]

同样地,我们看下使用Caffeine实现一个限制容量大小的缓存对象的处理表现,代码如下:

public static void main(String[] args) {
    Cache<String, String> cache = Caffeine.newBuilder()
            .maximumSize(1)
            .removalListener((key, value, cause) -> System.out.println(key + "被移除,原因:" + cause))
            .build();
    cache.put("key1", "value1");
    System.out.println("key1写入后,当前缓存内的keys:" + cache.asMap().keySet());
    cache.put("key2", "value1");
    System.out.println("key2写入后,当前缓存内的keys:" + cache.asMap().keySet());
}

运行这段代码,会发现Caffeine的容量限制功能似乎“失灵”了!从输出结果看并没有限制住

key1写入后,当前缓存内的keys:[key1]
key2写入后,当前缓存内的keys:[key1, key2]

什么原因呢?

Caffeine为了提升读写操作的并发效率而将数据淘汰清理操作改为了异步处理,而异步处理时会有微小的延时,由此导致了上述看到的容量控制“失灵”现象。为了证实这一点,我们对上述的测试代码稍作修改,打印下调用线程与数据淘汰清理线程的线程ID,并且最后添加一个sleep等待操作:

public static void main(String[] args) throws Exception {
    System.out.println("当前主线程:" + Thread.currentThread().getId());
    Cache<String, String> cache = Caffeine.newBuilder()
            .maximumSize(1)
            .removalListener((key, value, cause) ->
                    System.out.println("数据淘汰执行线程:" + Thread.currentThread().getId()
                            + "," + key + "被移除,原因:" + cause))
            .build();
    cache.put("key1", "value1");
    System.out.println("key1写入后,当前缓存内的keys:" + cache.asMap().keySe());
    cache.put("key2", "value1");
    Thread.sleep(1000L); // 等待一段时间时间,等待异步清理操作完成
    System.out.println("key2写入后,当前缓存内的keys:" + cache.asMap().keySet());
}

再次执行上述测试代码,发现结果变的符合预期了,也可以看出Caffeine的确是另起了独立线程去执行数据淘汰操作的。

当前主线程:1
key1写入后,当前缓存内的keys:[key1]
数据淘汰执行线程:13,key1被移除,原因:SIZE
key2写入后,当前缓存内的keys:[key2]

深扒一下源码的实现,可以发现Caffeine在读写操作时会使用独立线程池执行对应的清理任务,如下图中的调用链执行链路 —— 这也证实了上面我们的分析。

所以,严格意义来说,Caffeine的大小容量限制并不能够保证完全精准的小于设定的值,会存在短暂的误差,但是作为一个以高并发吞吐量为优先考量点的组件而言,这一点点的误差也是可以接受的。关于这一点,如果阅读源码仔细点的小伙伴其实也可以发现在很多场景的注释中,Caffeine也都会有明确的说明。比如看下面这段从源码中摘抄的描述,就清晰的写着“如果有同步执行的插入或者移除操作,实际的元素数量可能会出现差异”。

public interface Cache<K, V> {
    /**
     * Returns the approximate number of entries in this cache. The value returned is an estimate; the
     * actual count may differ if there are concurrent insertions or removals, or if some entries are
     * pending removal due to expiration or weak/soft reference collection. In the case of stale
     * entries this inaccuracy can be mitigated by performing a {@link #cleanUp()} first.
     *
     * @return the estimated number of mappings
     */
    @NonNegative
    long estimatedSize();

  // 省略其余内容...
}

同样道理,不管是基于大小、还是基于过期时间或基于引用的数据淘汰策略,由于数据淘汰处理是异步进行的,都会存在短暂不够精确的情况。

Caffeine多种数据驱逐机制

上面提到并演示了Caffeine基于整体容量进行的数据驱逐策略。除了基于容量大小之外,Caffeine还支持基于时间与基于引用等方式来进行数据驱逐处理。

基于时间

Caffine支持基于时间进行数据的淘汰驱逐处理。这部分的能力与Guava Cache相同,支持根据记录创建时间以及访问时间两个维度进行处理。

数据的过期时间在创建缓存对象的时候进行指定,Caffeine在创建缓存对象的时候提供了3种设定过期策略的方法。

方式具体说明
expireAfterWrite基于创建时间进行过期处理
expireAfterAccess基于最后访问时间进行过期处理
expireAfter基于个性化定制的逻辑来实现过期处理(可以定制基于新增读取更新等场景的过期策略,甚至支持为不同记录指定不同过期时间

下面逐个看下。

expireAfterWrite

expireAfterWrite用于指定数据创建之后多久会过期,使用方式举例如下:

Cache<String, User> userCache = 
    Caffeine.newBuilder()
        .expireAfterWrite(1, TimeUnit.SECONDS)
        .build();
userCache.put("123", new User("123", "张三"));
复制代码

当记录被写入缓存之后达到指定的时间之后,就会被过期淘汰(惰性删除,并不会立即从内存中移除,而是在下一次操作的时候触发清理操作)。

expireAfterAccess

expireAfterAccess用于指定缓存记录多久没有被访问之后就会过期。使用方式与expireAfterWrite类似:

Cache<String, User> userCache = 
    Caffeine.newBuilder()
        .expireAfterAccess(1, TimeUnit.SECONDS)
        .build();
    userCache.get("123", s -> userDao.getUser(s));
复制代码

这种是基于最后一次访问时间来计算数据是否过期,如果一个数据一直被访问,则其就不会过期。比较适用于热点数据的存储场景,可以保证较高的缓存命中率。同样地,数据过期时也不会被立即从内存中移除,而是基于惰性删除机制进行处理。

expireAfter

上面两种设定过期时间的策略与Guava Cache是相似的。为了提供更为灵活的过期时间设定能力,Caffeine提供了一种全新的的过期时间设定方式,也即这里要介绍的expireAfter方法。其支持传入一个自定义的Expiry对象,自行实现数据的过期策略,甚至是针对不同的记录来定制不同的过期时间。

先看下Expiry接口中需要实现的三个方法:

方法名称含义说明
expireAfterCreate指定一个过期时间,从记录创建的时候开始计时,超过指定的时间之后就过期淘汰,效果类似expireAfterWrite,但是支持更灵活的定制逻辑。
expireAfterUpdate指定一个过期时间,从记录最后一次被更新的时候开始计时,超过指定的时间之后就过期。每次执行更新操作之后,都会重新计算过期时间。
expireAfterRead指定一个过期时间,从记录最后一次被访问的时候开始计时,超过指定时间之后就过期。效果类似expireAfterAccess,但是支持更高级的定制逻辑。

比如下面的代码中,定制了expireAfterCreate方法的逻辑,根据缓存key来决定过期时间,如果key以字母A开头则设定1s过期,否则设定2s过期:

public static void main(String[] args) {
    try {
        LoadingCache<String, User> userCache = Caffeine.newBuilder()
                .removalListener((key, value, cause) -> {
                    System.out.println(key + "移除,原因:" + cause);
                })
                .expireAfter(new Expiry<String, User>() {
                    @Override
                    public long expireAfterCreate(@NonNull String key, @NonNullUser value, long currentTime) {
                        if (key.startsWith("A")) {
                            return TimeUnit.SECONDS.toNanos(1);
                        } else {
                            return TimeUnit.SECONDS.toNanos(2);
                        }
                    }
                    @Override
                    public long expireAfterUpdate(@NonNull String key, @NonNullUser value, long currentTime,
                                                  @NonNegative longcurrentDuration) {
                        return Long.MAX_VALUE;
                    }
                    @Override
                    public long expireAfterRead(@NonNull String key, @NonNull Uservalue, long currentTime,
                                                @NonNegative long currentDuration){
                        return Long.MAX_VALUE;
                    }
                })
                .build(key -> userDao.getUser(key));
        userCache.put("123", new User("123", "123"));
        userCache.put("A123", new User("A123", "A123"));
        Thread.sleep(1100L);
        System.out.println(userCache.get("123"));
        System.out.println(userCache.get("A123"));
    } catch (Exception e) {
        e.printStackTrace();
    }
}

执行代码进行测试,可以发现,不同的key拥有了不同的过期时间

User(userName=123, userId=123, departmentId=null)
A123移除,原因:EXPIRED
User(userName=A123, userId=A123, departmentId=null)

除了根据key来定制不同的过期时间,也可以根据value的内容来指定不同的过期时间策略。也可以同时定制上述三个方法,搭配来实现更复杂的过期策略。

按照这种方式来定时过期时间的时候需要注意一点,如果不需要设定某一维度的过期策略的时候,需要将对应实现方法的返回值设置为一个非常大的数值,比如可以像上述示例代码中一样,指定为Long.MAX_VALUE值。

基于大小

除了前面提到的基于访问时间或者创建时间来执行数据过期淘汰的方式之外,Caffeine还支持针对缓存总体容量大小进行限制,如果容量满的时候,基于W-TinyLFU算法,淘汰最不常被使用的数据,腾出空间给新的记录写入。

maximumSize

在创建Caffeine缓存对象的时候,可以通过maximumSize来指定允许缓存的最大条数。

比如下面这段代码:

Cache<Integer, String> cache = Caffeine.newBuilder()
        .maximumSize(1000L) // 限制最大缓存条数
        .build();
复制代码

maximumWeight

在创建Caffeine缓存对象的时候,可以通过maximumWeightweighter组合的方式,指定按照权重进行限制缓存总容量。比如一个字符串value值的缓存场景下,我们可以根据字符串的长度来计算权重值,最后根据总权重大小来限制容量。

代码示意如下:

Cache<Integer, String> cache = Caffeine.newBuilder()
        .maximumWeight(1000L) // 限制最大权重值
        .weigher((key, value) -> (String.valueOf(value).length() / 1000) + 1)
        .build();
复制代码

使用注意点

需要注意一点:如果创建的时候指定了weighter,则必须同时指定maximumWeight值,如果不指定、或者指定了maximumSize,会报错(这一点与Guava Cache一致):

java.lang.IllegalStateException: weigher requires maximumWeight
	at com.github.benmanes.caffeine.cache.Caffeine.requireState(Caffeine.java:201)
	at com.github.benmanes.caffeine.cache.Caffeine.requireWeightWithWeigher(Caffeine.java:1215)
	at com.github.benmanes.caffeine.cache.Caffeine.build(Caffeine.java:1099)
	at com.veezean.skills.cache.caffeine.CaffeineCacheService.main(CaffeineCacheService.java:254)
复制代码

基于引用

基于引用回收的策略,核心是利用JVM虚拟机的GC机制来达到数据清理的目的。当一个对象不再被引用的时候,JVM会选择在适当的时候将其回收。Caffeine支持三种不同的基于引用的回收方法:

方法具体说明
weakKeys采用弱引用方式存储key值内容,当key对象不再被引用的时候,由GC进行回收
weakValues采用弱引用方式存储value值内容,当value对象不再被引用的时候,由GC进行回收
softValues采用软引用方式存储value值内容,当内存容量满时基于LRU策略进行回收

下面逐个介绍下。

weakKeys

默认情况下,我们创建出一个Caffeine缓存对象并写入key-value映射数据时,key和value都是以强引用的方式存储的。而使用weakKeys可以指定将缓存中的key值以弱引用(WeakReference)的方式进行存储,这样一来,如果程序运行时没有其它地方使用或者依赖此缓存值的时候,该条记录就可能会被GC回收掉。

 LoadingCache<String,  User> loadingCache = Caffeine.newBuilder()
                .weakKeys()
                .build(key -> userDao.getUser(key));
复制代码

小伙伴们应该都有个基本的认知,就是两个对象进行比较是否相等的时候,要使用equals方法而非==。而且很多时候我们会主动去覆写hashCode方法与equals方法来指定两个对象的相等判断逻辑。但是基于引用的数据淘汰策略,关注的是引用地址值而非实际内容值,也即一旦使用weakKeys指定了基于引用方式回收,那么查询的时候将只能是使用同一个key对象(内存地址相同)才能够查询到数据,因为这种情况下查询的时候,使用的是==判断是否为同一个key。

看下面的例子:

public static void main(String[] args) {
    Cache<String, String> cache = Caffeine.newBuilder()
            .weakKeys()
            .build();
    String key1 = "123";
    cache.put(key1, "value1");
    System.out.println(cache.getIfPresent(key1));
    String key2 = new String("123");
    System.out.println("key1.equals(key2) : " + key1.equals(key2));
    System.out.println("key1==key2 : " + (key1==key2));
    System.out.println(cache.getIfPresent(key2));
}
复制代码

执行之后,会发现使用存入时的key1进行查询的时候是可以查询到数据的,而使用key2去查询的时候并没有查询到记录,虽然key1与key2的值都是字符串123!

value1
key1.equals(key2) : true
key1==key2 : false
null
复制代码

在实际使用的时候,这一点务必需要注意,对于新手而言,很容易踩进坑里

weakValues

与weakKeys类似,我们可以在创建缓存对象的时候使用weakValues指定将value值以弱引用的方式存储到缓存中。这样当这条缓存记录的对象不再被引用依赖的时候,就会被JVM在适当的时候回收释放掉。

 LoadingCache<String,  User> loadingCache = Caffeine.newBuilder()
                .weakValues()
                .build(key -> userDao.getUser(key));

实际使用的时候需要注意weakValues不支持AsyncLoadingCache中使用。比如下面的代码:

public static void main(String[] args) {
    AsyncLoadingCache<String, User> cache = Caffeine.newBuilder()
            .weakValues()
            .buildAsync(key -> userDao.getUser(key));
}

启动运行的时候,就会报错:

Exception in thread "main" java.lang.IllegalStateException: Weak or soft values cannot be combined with AsyncLoadingCache
	at com.github.benmanes.caffeine.cache.Caffeine.requireState(Caffeine.java:201)
	at com.github.benmanes.caffeine.cache.Caffeine.buildAsync(Caffeine.java:1192)
	at com.github.benmanes.caffeine.cache.Caffeine.buildAsync(Caffeine.java:1167)
	at com.veezean.skills.cache.caffeine.CaffeineCacheService.main(CaffeineCacheService.java:297)

当然咯,很多时候也可以将weakKeysweakValues组合起来使用,这样可以获得到两种能力的综合加成。

 LoadingCache<String,  User> loadingCache = Caffeine.newBuilder()
                .weakKeys()
                .weakValues()
                .build(key -> userDao.getUser(key));

softValues

softValues是指将缓存内容值以软引用的方式存储在缓存容器中,当内存容量满的时候Caffeine会以LRU(least-recently-used,最近最少使用)顺序进行数据淘汰回收。对比下其与weakValues的差异:

方式具体描述
weakValues弱引用方式存储,一旦不再被引用,则会被GC回收
softValues软引用方式存储,不会被GC回收,但是在内存容量满的时候,会基于LRU策略数据回收

具体使用的时候,可以在创建缓存对象的时候进行指定基于软引用方式数据淘汰:

 LoadingCache<String,  User> loadingCache = Caffeine.newBuilder()
                .softValues()
                .build(key -> userDao.getUser(key));

与weakValues一样,需要注意softValues不支持AsyncLoadingCache中使用。此外,还需要注意softValuesweakValues两者也不可以一起使用。

public static void main(String[] args) {
    LoadingCache<String, User> cache = Caffeine.newBuilder()
            .weakKeys()
            .weakValues()
            .softValues()
            .build(key -> userDao.getUser(key));
}

启动运行的时候,也会报错:

Exception in thread "main" java.lang.IllegalStateException: Value strength was already set to WEAK
	at com.github.benmanes.caffeine.cache.Caffeine.requireState(Caffeine.java:201)
	at com.github.benmanes.caffeine.cache.Caffeine.softValues(Caffeine.java:572)
	at com.veezean.skills.cache.caffeine.CaffeineCacheService.main(CaffeineCacheService.java:297)

小结回顾

好啦,关于Caffeine Cache数据淘汰驱逐策略的实现原理与使用方式的阐述,就介绍到这里了。至此呢,关于Caffeine相关的内容就全部结束了,通过与Caffeine相关的这三篇文章,我们介绍完了Caffeine的整体情况、与Guava Cache相比的改进点、Caffeine的项目中使用,以及Caffeine在数据回源、数据驱逐等方面的展开探讨。关于Caffeine Cache,你是否有自己的一些想法与见解呢?欢迎评论区一起交流下,期待和各位小伙伴们一起切磋、共同成长。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/13832.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

HTML---表单详解

目录 一、表单介绍 二、表单元素 1&#xff1a;input输入表单元素 &#xff08;1&#xff09; text和password &#xff08;2&#xff09;radio和checkbox &#xff08;3&#xff09;button 2&#xff1a;label标签 3&#xff1a;select下拉表单元素 4&#xff1a;textar…

喜报 | 强强联手,这件大事终于有着落了

近日&#xff0c;擎创科技与国产数据库领先品牌——达梦数据库达成战略合作伙伴关系&#xff0c;双方将在品牌、产品、技术、渠道等方面进行全方位合作&#xff0c;共同完成产品整合、资源共享、联合方案等事宜&#xff1b;共同赋能于金融、能源、交通等行业的数字化转型&#…

2019年1+X 证书 Web 前端开发中级理论考试题目原题+答案——第一套

&#x1f4da;文章目录 &#x1f3af;关于1X标准 &#x1f3af;关于中级考点 ⏩&#x1f4bb;答案速查 一、单选题&#xff08;每小题2分&#xff0c;共30小题&#xff0c;共60分&#xff09; 二、多选题&#xff08;每小题2分&#xff0c;共15小题&#xff0c;共30分&…

Java入门-Java语言概述

1、Java语言基本概述 1.1、什么是计算机编程语言 人与人之间是如何沟通的呢&#xff1f;从小父母就教会我们说话&#xff0c;在长期的熏陶和自我学习中&#xff0c;我们就学会了普通话&#xff0c;学会了表达自己的需求&#xff0c;父母收到我们的信号或者听到我们的要求也会尽…

C# 自定义控件

一 自定义控件 1 自定义控件的三种方式&#xff1a; 1&#xff09;复合控件&#xff1a;将标准控件组合起来 class YourControl:UserControl{}2) 扩展控件&#xff1a;继承于标准控件 class YourControl:Button{}3) 自定义控件&#xff1a;完全地自定义一个控件 class You…

CSSStyleSheet 对象(css 样式表)- 你不知道的有趣功能

1.概念 CSSStyleSheet 对象 代表着&#xff0c;css文件被浏览器解析后生成的css样式表。 CSS 样式表由 CSS 规则组成&#xff0c;可以通过 CSSRule 对象操作每条规则。CSSStyleSheet 对象允许您查询、插入和删除样式表规则。 例如&#xff1a;好玩儿的尝试&#xff08;改变页…

基于经验论述:实现k-NN算法

以兰普威尔小镇为例,那里的人们为他们的两支球队——兰普威尔红队和兰普威尔蓝队——而疯狂。红队已经存在很长时间了,人们很喜欢这支队伍。 但是后来,一些外地来的富翁买下了红队的最佳射手,成立了一支新的球队——蓝队。令多数红队球迷不满的是,这位最佳射手将继续带领蓝…

数据结构与算法_BST树_BST树的定义及删除操作

先写BST树的定义及特点&#xff0c;然后记录BST数的删除操作。 1 BST定义及特点 BST数是一棵特殊的二叉树&#xff0c;如何能得到一颗二叉搜索树呢&#xff1f;下面一个有序序列&#xff0c;经过二分搜索&#xff0c;得到的就是一颗BST树。根节点就是当前一轮要搜索的中间节点…

Cell子刊:器官再生,这种病可以让受损的肝脏再生

麻风病是由麻风杆菌引起的一种慢性传染病&#xff0c;主要病变在皮肤和周围神经。它是世界上最古老和最顽固的疾病之一&#xff0c;但引起它的细菌也可能具有令人惊讶的生长和再生重要器官的能力。器官再生是治疗疾病、损伤或衰老功能障碍的理想策略&#xff0c;尽管体外模型、…

linux-adnroid 键值枚举与注释

一、概述 没有找到直接的linux 键值枚举的注释。在android 官网linux-android健码网页里找到了这个映射关系。因为android列的比较多。去掉冗余摘取主要对比和注释进表格。 二、术语摘抄解释 HID 用法、Linux 键码和 Android 键码 系统根据抽象层使用几个不同的标识符来引用…

【服务器数据恢复】5节点Lustre分布式文件系统RAID5数据恢复案例

服务器数据恢复环境&#xff1a; 5台服务器节点&#xff0c;每台服务器节点配置一组RAID5&#xff0c;每组6块硬盘&#xff0c;其中1块设置为热备盘。 系统环境为Lustre分布式文件系统&#xff0c;5台服务器共同存储全部的数据文件。 服务器故障&检测&#xff1a; 机房漏水…

你被裁了需要知道的一些细节

前言 就在上周&#xff0c;得知自己要被裁了&#xff0c;理由是公司在疫情下的经济性裁员&#xff0c;在这里总结一下关于裁员过程中需要注意的一些细节。 关于工作交接 要在收到书面通知之后&#xff0c;再进行工作交接&#xff0c;否则可能面临的风险是公司卸磨杀驴。 关…

C++初阶 List的模拟实现

作者&#xff1a;小萌新 专栏&#xff1a;C初阶 作者介绍&#xff1a;大二学生 希望能和大家一起进步 本篇博客简介&#xff1a;模拟STL中List的实现 List的模拟实现本篇博客目标节点类的实现构造函数迭代器类的实现迭代器类模拟实现的意义迭代器类的三个参数说明构造函数运算…

zabbix自定义模板,邮件报警,代理服务器,自动发现与自动添加及snmp

内容预知 1.自定义监控内容 监控案例1&#xff1a;登录人数检测 具体步骤 步骤一&#xff1a;在客户端创建自定义 key 步骤二&#xff1a;在 Web 页面创建自定义监控项模板 &#xff08;1&#xff09;创建模板 &#xff08;2&#xff09;创建应用集&#xff08;用于管理…

《数据结构》顺序表ArrayList

《数据结构》顺序表ArrayList 文章目录《数据结构》顺序表ArrayList什么是顺序表:模拟实现顺序表(以int类型为例)详解ArrayList:ArrayList实现的接口:ArrayList的构造方法:对源码的解析:ArrayList的方法ArrayList的扩容机制ArrayList的缺陷什么是顺序表: 顺序表是用一段物理地…

【Hack The Box】windows练习-- acute

HTB 学习笔记 【Hack The Box】windows练习-- acute &#x1f525;系列专栏&#xff1a;Hack The Box &#x1f389;欢迎关注&#x1f50e;点赞&#x1f44d;收藏⭐️留言&#x1f4dd; &#x1f4c6;首发时间&#xff1a;&#x1f334;2022年11月17日&#x1f334; &#x1f…

并发入门组件AQS源码解析(未完善)

必要掌握技术 阻塞&#xff0c;唤醒与中断 阻塞与唤醒 LockSupport的park使用的是Parker->park() synchronized的wait&#xff0c;synchronized的monitorenter使用的是ParkEvent->park()&#xff0c; 而LockSupport的unpark&#xff0c;Parker->unpark() synchroni…

员工离职后,账号权限怎么自动化回收?

最近一则离职员工报复前东家的新闻引人注目。起因是该员工被公司辞退后怀恨在心&#xff0c;于是用自己的笔记本电脑入侵了前公司的服务器&#xff0c;进入了该公司的法国站、英国站、德国站三个亚马逊店铺&#xff0c;进行了大幅调低商品价格、主动向客户发起退款、调高广告预…

1525_AURIX TC275 BootROM上

全部学习汇总&#xff1a; GitHub - GreyZhang/g_TC275: happy hacking for TC275! 这一次看一个全新的章节&#xff0c;BootROM&#xff0c;这是我之前只听过但是没有接触过的一个功能。 1. BootROM包含的三个主要的功能&#xff1a;启动软件、引导加载程序、测试固件。 2. 启…

UI设计都有哪些设计原则,分享三个给你

是什么使一个好UI设计容易阅读&#xff1f;是什么让用户轻松浏览&#xff1f;设计师如何创造一个闪亮的UI&#xff1f;任何软件产品的关键部分都是用户界面。 ​好的UI设计&#xff0c;用户甚至会忽略它。如果做得不好&#xff0c;就会成为用户使用产品的绊脚石。为了更有效地设…