Db层治理:SQL精细化及并发更新数据丢失问题解决最佳实践 | 得物技术

发表于 2年以前  | 总阅读数:509 次

1.背景

1.1 概述
1.1.1 SQL精细化提出背景

目前使用比较广泛的数据库持久层框架有两个,JPA和Mybatis,先来简单比较一下这两种框架。

从选择上来看,和业务场景是有关系的,迭代比较频繁、复杂性较高,配合MYSQL数据库,我们的数据库持久层选用的是Mybatis,Mybatis的一个很重要的特性就是开发可以自己写SQL,或者借助自动生成工具生成SQL,在实际的开发工作中,没有统一的规范,我们习惯于SQL Mapper文件中的通用的或者万能的查询、更新、插入等方法上叠加自己的增删改查操作,随着业务的场景增多,SQL的复杂性也会随着上升,那么在开发完成开发之后,一个可以review的SQL是我们迫切需要的,大家在CR代码的时候,可以很快的发现SQL中可能存在的一些问题,比如慢SQL、是否走到索引、走了哪个索引等。SQL质量的好坏将会影响整体应用的性能以及整体链路的压力,所以对于SQL的精细化管理是很有必要的一项工作。

1.1.2 并发更新导致数据丢失问题

我们的数据库表结构设计的时候,往往会预留一个feature字段,很多的业务增值信息都会存放到这个字段,以一个大json存储,如果我们在各种场景对同一条记录的这个字段更新,如果没有做到并发控制,那么则会导致数据被覆盖,形成脏数据。

针对1.1.1和1.1.2两个问题,结合我们的业务,针对db层的治理,尝试给出一些建议。

1.2 SQL精细化管理的收益

SQL精细化管理作为解决慢SQL的一个重要组成部分,可以带来如下收益:

(1)如果Mybatis的SQL Mapper无法有效的进行review,那么其中存在的风险,我们第一时间是无法直观感知的,我们需要可review的SQL,将风险提前暴露

(2)解决慢SQL问题,提高查询效率,提升用户体验及提高系统吞吐

(3)避免潜在OOM风险,导致应用崩溃

(4)避免慢SQL问题(慢SQL消耗的资源往往比正常SQL要高几倍、几十倍、几百倍)导致MYSQL崩溃,拖垮整个数据库,降低CPU使用率

(5)解决代码及SQL缺陷问题,可以提升我们订单服务的稳定性

1.3 并发更新导致数据丢失问题现象产生的原因

例如有如下SQL更新feature字段:


<update id="update"
        parameterType="OrderEntity">
    update table_order
    set feature = #{feature,jdbcType=VARCHAR}
    where order_no = #{orderNo,jdbcType=VARCHAR}
</update>

当存在两个线程同时更新,则会出现以下情况:

我们最终的期望结果是:

JSON:

a=1;b=1;c=1;

那么随着Thread-1和Thread-2的先后顺序不同,我们得到的结果有可能会是:

a=1;b=1;

或:

a=1;c=1;

所以就出现了脏数据,并不是我们最终想要的。

2.SQL精细化最佳实践

2.1 不要使用工具自动生成的通用SQL

这种SQL是自动生成的,SQL条件也是拼接的,需要在程序中去拼接查询条件或者更新值,我们也看不出执行的SQL到底是什么样的,不能直观看到具体的查询SQL是什么,需要在代码中一层层往上找,找到赋值的地方,如:


<select id="selectByExample" parameterType="OrderExample" resultMap="BaseResultMap">
  select
  <if test="distinct">
    distinct
  </if>
  <include refid="Base_Column_List" />
  from table_order
  <if test="_parameter != null">
    <include refid="Example_Where_Clause" />
  </if>
  <if test="orderByClause != null">
    order by ${orderByClause}
  </if>
</select>
2.2 必须清晰看到索引情况

为了避免我们的SQL执行全表扫描(对于InnoDB来说就是直接扫描全部的聚簇索引记录),我们需要让我们的SQL走到我们设定的索引上,减少执行查询时遍历的数据行数,提升查询性能。SQL map里面要能一眼看出这条SQL是否走了索引,具体走了哪些索引。比如下面的SQL,就不能明确看出走了哪个索引。


<select id="selectOrderList"
            parameterType="OrderEntity"
            resultMap="BaseResultMap">
        select
        <include refid="Base_Column_List_2_Report"/>
        from table_order ro
        <where>
            <if test="orderNo != null">
                and ro.order_no = #{orderNo}
            </if>
            <if test="subOrderNo != null">
                and ro.sub_order_no = #{subOrderNo}
            </if>
            <if test="refundOrderNo != null">
                and ro.refund_order_no = #{refund_order_no}
            </if>
            <if test="startTime != null and endTime != null">
                and ro.create_time between #{startTime} and #{endTime}
            </if>
        </where>
        order by ro.create_time asc
        LIMIT #{offSet},#{limit}
    </select>
2.3 非必需不要使用动态SQL

因为Mybatis提供的动态SQL功能,可以很便捷的拼装SQL,但也有弊端:

(1)同时也会将查询数据库的参数交易后置到Mapper中,如果出现遗漏,或者校验不正确,使用动态SQL极有可能存在内存溢出隐患,导致OOM

(2)代码可读性差。

要求:在新增的代码不要出现动态SQL,索引字段不要动态,需要明确,例如下面一种场景就是索引不明确,refund_no、sub_order_no、order_no三个字段都可能走索引:


<select id="getDetail"  parameterType="OrderEntity" resultMap="BaseResultMap">
  select <include refid="Base_Column_List" />
  from table_order
  <where>
    <if test="refund_No != null and refundNo != ''">
      and refund_No =#{refundNo}
    </if>
    <if test="subOrderNo != null and subOrderNo != ''">
      and sub_order_no =#{subOrderNo}
    </if>
    <if test="orderNo != null and orderNo != ''">
      and order_no =#{orderNo}
    </if>
  </where>
</select>

(3)因为订单都是比较重数据的需要提供一些查询类的接口给到外部,比如客服、商家后台等,这类偏向于B端的查询,很多时候查询条件是很多的,也是可选的,为了支持这些查询接口,我们需要明确哪些是必传字段,必传字段在dao层就需要强制校验,mapper层不再做校验。例如我们针对商家接口查询,一般要求卖家id、时间范围都是必须传值的。

2.4 update操作,需要明确更新字段,
不要传po对象作为参数,
在SQL mapper中进行if判断

更新字段要明确,不要在mapper中做if判断,正例:


<update id="update"
        parameterType="OrderEntity">
    update table_order
    set feature = #{feature,jdbcType=VARCHAR}
    where order_no = #{orderNo,jdbcType=VARCHAR}
</update>

反例:

<update id="update"
        parameterType="OrderEntity">
    update table_order
    <set>
        <if test="orderNo != null">
            order_no = #{orderNo,jdbcType=VARCHAR},
        </if>
        <if test="subOrderNo != null">
            sub_order_no = #{subOrderNo,jdbcType=VARCHAR},
        </if>
        <if test="bizType != null">
            biz_type = #{bizType,jdbcType=VARCHAR},
        </if>
        <if test="refundType != null">
            refund_type = #{refundType,jdbcType=TINYINT},
        </if>
        <if test="orderStatus != null">
            order_status = #{orderStatus,jdbcType=INTEGER},
        </if>
    </set>
    where order_no = #{orderNo,jdbcType=VARCHAR}
</update>
2.5 order by排序问题

需要注意两点:

① 如果业务对记录结果是否排序无要求,尽量不要用order by排序,排序比较耗时。

② 因为innodb的索引结构是B+树,索引中的数据是按照一定顺序进行排列的,在排序查询过程中,需要利用索引,避免额外的排序,即order by字段需要走到索引。

2.5.1 order by join查询

在join查询中,oder by排序,查看执行计划,避免Extra 字段有“Using filesort”出现,“Using filesort”表示的就是需要文件排序,如果排序数据量太大,内存放不下,则不得不利用磁盘临时文件辅助排序。要尽量避免这种Using Sort出现。

2.5.2 order by 多个条件排序查询

当odrer by多个字段的时候,容易导致使用文件排序,不要轻易的写order by 多个字段这样的SQL,去掉不需要的order by字段,只保留一个必须的。

2.6 limit的使用

当我们的查询SQL,没有走到唯一索引或者主键索引,或者传入空参数,那么返回极有可能是多条数据,甚至几十万,几百万条数据,这时候,我们需要加limit限制,一定要有兜底,避免发生大批量的数据查询,全表扫描情况出现,造成查询超时,或者查询返回大量数据,使应用内存急剧上升,导致oom。如果条件中带有主键查询的,直接使用limit 1 。

2.7 不允许select *全部字段

我们尽量查询我们需要输出的字段或者需要用到字段,不要查询没必要输出的字段,需要查询的字段可以单独的列出来,一律不允许用select *查询。

HTML:

<SQL id="Base_Column_List">
  order_no, sub_order_no, user_id, biz_type, pay_tool, pay_status, out_pay_no
</SQL>

有三个方面原因:

① 一个是查询只需要的字段如果是索引,那么可以直接使用index里面的数据就可以返回,不用做回表查询

② 数据量大的场景会影响网络io传输性能

③ 可能会造成大对象

2.8 join查询

① 能不能join查询,尽量不要要join查询

② 必须用join查询的,在做join查询的时候,要注意驱动表的选择,一般我们选用小表作为驱动表,被驱动表可以走索引。

2.9 范围查询

对于需要提供一段时间范围查询的接口,我们需要对这个时间范围进行限制,不能过大,如果超出范围,要提前抛异常,一定要进行强制校验,比如针对时间范围的数据查询,一般根据数据量,确定一个查询时间范围,范围太大,会造成慢SQL。

2.10 联合索引问题

对于使用到联合索引查询SQL的时候,我们需要注意联合索引的顺序,一般情况下,SQL执行的时候,会根据在前面顺序在前面的先进行过滤,我们的期望是顺序越在前面的索引可以过滤掉越多的数据,这样层层推进,各列应按按量排序。

我们希望排在前面的字段可以过滤掉绝大部分数据。联合索引选择性高的索引排在前面,减少扫描行数。

2.11 in查询

in 操作能避免则避免,若实在避免不了,需要仔细评估 in 后边的集合元素数量,控制在 1000 个之内。一定要加强制校验。如下面的查询条件中的subOrderNoList需要在外层做大小校验。特别是在分库分表场景下,为了提升查询效率,很有必要对in查询条件数量做限制,因为分库分表场景下,数据是散落在不同,需要去对应数据库中读取数据再做聚合返回,效率比较低。

HTML:

<select id="queryDetail" resultMap="BaseResultMap">
    select
    <include refid="Base_Column_List"/>
    from table_order
    where order_no in
    <foreach collection="orderNoList" open="(" close=")" separator="," item="orderNo" index="i">
        #{orderNo}
    </foreach>
    order by create_time desc
    limit 20
</select>
2.12 update操作之前做select查询

在做update操作之前尽量先查询以下,避免不必要的数据库交互操作。

2.13 分页查询大小限制

分页查询pagesize大小要加强制校验,超过限定值,直接异常返回。

2.14 超大分页场景

MySQL 并不是跳过 offset 行,而是取 offset+N 行,然后返回放弃前 offset 行,返回 N 行,那当 offset 特别大的时候,效率就非常的低下,要么控制返回的总页数,要么对超过特定阈值的页数进行控制,offset和pageSize需要做限制

select * from table_order LIMIT #{offset}, #{pageSize};
2.15 批量插入

对于批量插入数据操作,我们希望可以一次执行,避免多次重复数据库交互操作。

反例:

Java:



public void batchSave(List<TableOrder> orderEntityList) {
    if (CollectionUtil.isEmpty(orderEntityList)) {
        return;
    }
    for (OrderEntity orderEntity : orderEntityList) {
        save(orderEntity);
    }
}

正例:

SQL:

INSERT into table_order(cloum1,cloum2,....cloumx) values('?','?',....'?'),('?','?',....'?'),('?','?',....'?');
2.16 like 语句的优化

%%这种匹配方式完全无法使用索引,从而进行全表扫描导致效率极低,需要通过走到索引查找数据

2.17 不要在程序中拼接查询条件

Java:


public Criteria andBizTypeIn(List<String> values) {
    addCriterion("biz_type in", values, "bizType");
    return (Criteria) this;
}
2.18 select count问题

统计查询一律使用select count(*),按照效率排序的话,count(字段)<count(主键 id)<count(1)≈count(*)

2.19 删除mapper中不需要的操作

例如一些delete物理删除这些操作方法可以清理掉。这些方法都是通过自动生成工具生成的,因为正常情况下,我们一般不会对数据库数据做物理删除。

2.20 影响行数结果判断

对于任何update更新操作,都需要判断返回值,即影响行数。在进行update操作的时候,正常情况下,数据库返回影响行数,如果数据库中没有需要进行update的数据,也是不报错的,这就需要我们在应用层自己进行判断,如果更新行数是0,或者多于1条,我们的业务处理逻辑该怎么走。

Java:



int effect = repository.update(param1,param2)
if (effect < 1){
    throw new Exception("update操作执行失败");
}
2.21 list.get(0)

对于任何list.get(0),都需要是排序之后的结果。例如我们在dao层查询的时候,是用list查询,返回列表,也没有进行排序,在controller层直接get(0),很容易取错数据。

2.22 list返回

对于任何查询,只要是where条件中没有主键或唯一健,都需要用list返回,不能简单的根据场景来认为数据库中只可能有一条记录,容易引起Too Many results error。存在一些场景,比如,创建退货,然后取消退货,再创建退货,再取消退货,根据订单号来查询可以查询多条记录,但我们只需要最新的一条,没有做limit限制,就会异常。

3.并发更新覆盖数据解决最佳实践

一般解决这种并发导致数据的方法有悲观锁和乐观锁两种机制。

3.1 悲观锁

使用悲观锁的原理就是,当我们在查询出feature信息之后,就会把当前行数据进行锁定,然后去修改feature字段,直到我们修改数据提交完毕之后,其他事物才可以进行修改,前提是我们需要将执行的SQL放在同一个事物中,否则达不到锁定行数据的目的。


start transaction
select feature from table_order where order_no = '100' for update;
update table_order set feature = new_feature where order_no = '100';
commit

select...for update是MySQL提供的实现悲观锁的方式。此时在refund_order表中,refund_no为100的那条数据就被我们锁定了,其它的要执行的事务必须等本次事务提交之后才能执行。这样我们可以保证当前的数据不会被其它事务修改。

select feature from table_order where order_no = '100' for update;

悲观锁虽然可以强保证数据不会被覆盖,但也存在一些问题,比如:

(1)事物粒度控制不好的情况下,容易产生死锁

(2)InnoDB行锁是通过给索引上的索引项加锁来实现的,只有通过索引条件检索数据,InnoDB才使用行级锁,否则,InnoDB将使用表锁。如果SQL未走到索引,将会带来灾难性事件。

(3)悲观锁依赖数据库,效率较低,但更新失败的概率也较低,一般在金融、支付行业中使用相对较多。在目前行业内的高并发架构中,倾向于fail-fast模式,悲观锁相对使用较少。

3.2 乐观锁

乐观锁相对于悲观锁而言,认为数据在一般情况下不会造成冲突,在数据进行提交更新的时候,才会正式对数据的冲突进行检测,如果冲突,则失败返回。

3.2.1 全局version乐观锁

乐观锁一般常用的实现机制是利用版本号(version)来实现。一般通过为数据库表增加一个数字类型的 “version” 字段,当读取数据时,将version字段的值一同读出,数据每更新一次,对此version值+1。当我们提交更新的时候,判断数据库表对应记录的当前版本信息与第一次取出来的version值进行比对,如果数据库表当前版本号与第一次取出来的version值相等,则予以更新,否则认为是过期数据,返回更新失败。

执行过程如下:

SQL:

select (feature,version) from table_order where refund_no='100';
update table_order set feature=new_feature,version=version+1 where table_order ='100' and version= version;

我们可以看到若采用版本号version作为乐观锁,则每次只有一个事务能更新成功,业务感知上就是大量操作失败。

3.2.2 feature_version版本字段

因为version字段是全局的,利用全局字段version进行处理,则会发现refund_order表的其他字段变更也会失败。那么如何解决这个问题?业内的做法是从锁的粒度上进行优化,比如针对feature字段专门新增一个feature_version字段,表示feature的版本号,很好的避免了version对所有字段变更的冲突。执行SQL如下:


update    
    table_order
set    
    features=#features#,    
    features_version= features_version +1,
    version = version +1
where    
    order_no=#order_no#    
    and features_version =#ori_features_version#

通过3.2.1和3.2.2对比,我们可以清晰的发现,3.2.2中方案是较优的,在我们的实际业务中也是采取这一方式进行了改造。

4.总结与展望

前面已经谈到,对于订单系统的核心来说就是数据,那么对于我们应用来说,就是对这些数据进行增删改查操作,对Db层进行梳理和改造,对我们业务数据的正确性以及应用性能提升是有很大帮助的,然而,Db层的治理工作,对于一个应用来说,有牵一发而动全身的影响,针对这方面的改造需要关注以下几个点:

(1) 测试需要充分,测试的场景需要梳理全面,可以从Mapper层反推涉及的场景接口,因为涉及Mapper层的改动,是没办法进行对比的。

(2)在没有全流量对比工具的前提下,需要做好灰度切流工作,在灰度粒度上,可以选择按接口维度,或者按机器维度,也可以开始按接口维度,等所有场景覆盖全之后,再按照机器灰度。如果订单在夜间、白天处理逻辑不一样,我们灰度需要考虑到隔夜观察,也可以利用一些辅助工具例如对账等。

(3)Db层治理工作一直在路上,完成全面梳理改造只是迈出的第一步,一个好的规范如何进行推进下去,让大家保持统一的规范,随着时间的推移,这种规范能够继续保持不走样,是我们需要思考的问题。

本文由哈喽比特于2年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/klrORFnM8hYLuyVWPha6EQ

 相关推荐

刘强东夫妇:“移民美国”传言被驳斥

京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。

发布于:1年以前  |  808次阅读  |  详细内容 »

博主曝三大运营商,将集体采购百万台华为Mate60系列

日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。

发布于:1年以前  |  770次阅读  |  详细内容 »

ASML CEO警告:出口管制不是可行做法,不要“逼迫中国大陆创新”

据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。

发布于:1年以前  |  756次阅读  |  详细内容 »

抖音中长视频App青桃更名抖音精选,字节再发力对抗B站

今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。

发布于:1年以前  |  648次阅读  |  详细内容 »

威马CDO:中国每百户家庭仅17户有车

日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。

发布于:1年以前  |  589次阅读  |  详细内容 »

研究发现维生素 C 等抗氧化剂会刺激癌症生长和转移

近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。

发布于:1年以前  |  449次阅读  |  详细内容 »

苹果据称正引入3D打印技术,用以生产智能手表的钢质底盘

据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。

发布于:1年以前  |  446次阅读  |  详细内容 »

千万级抖音网红秀才账号被封禁

9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...

发布于:1年以前  |  445次阅读  |  详细内容 »

亚马逊股东起诉公司和贝索斯,称其在购买卫星发射服务时忽视了 SpaceX

9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。

发布于:1年以前  |  444次阅读  |  详细内容 »

苹果上线AppsbyApple网站,以推广自家应用程序

据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。

发布于:1年以前  |  442次阅读  |  详细内容 »

特斯拉美国降价引发投资者不满:“这是短期麻醉剂”

特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。

发布于:1年以前  |  441次阅读  |  详细内容 »

光刻机巨头阿斯麦:拿到许可,继续对华出口

据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。

发布于:1年以前  |  437次阅读  |  详细内容 »

马斯克与库克首次隔空合作:为苹果提供卫星服务

近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。

发布于:1年以前  |  430次阅读  |  详细内容 »

𝕏(推特)调整隐私政策,可拿用户发布的信息训练 AI 模型

据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。

发布于:1年以前  |  428次阅读  |  详细内容 »

荣耀CEO谈华为手机回归:替老同事们高兴,对行业也是好事

9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI操控无人机能力超越人类冠军

《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI生成的蘑菇科普书存在可致命错误

近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。

发布于:1年以前  |  420次阅读  |  详细内容 »

社交媒体平台𝕏计划收集用户生物识别数据与工作教育经历

社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”

发布于:1年以前  |  411次阅读  |  详细内容 »

国产扫地机器人热销欧洲,国产割草机器人抢占欧洲草坪

2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。

发布于:1年以前  |  406次阅读  |  详细内容 »

罗永浩吐槽iPhone15和14不会有区别,除了序列号变了

罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。

发布于:1年以前  |  398次阅读  |  详细内容 »
 相关文章
Android插件化方案 5年以前  |  237231次阅读
vscode超好用的代码书签插件Bookmarks 2年以前  |  8067次阅读
 目录