WillLiaowh

WillLiaowh 查看完整档案

广州编辑  |  填写毕业院校  |  填写所在公司/组织填写个人主网站
编辑

世界上最伟大的力量是坚持。

个人动态

WillLiaowh 赞了回答 · 4月20日

解决关于索引的一个问题

联合索引(col2,col3,col1)

关注 2 回答 1

WillLiaowh 提出了问题 · 4月19日

解决关于索引的一个问题

面试被问到一个问题,select * from table where cloumn >1 and cloumn=2 and cloumn=3怎么建索引最好?

关注 2 回答 1

WillLiaowh 发布了文章 · 4月3日

Redis主从复制及其原理

为什么要有主从复制

为了避免服务的单点故障,通过给主从复制可以把数据复制多个副本放在不同的服务器上,拥有数据副本的服务器可以用于处理客户端的读请求,扩展整体的性能

Redis的主从复制搭建

准备3台机器,主服务器ip为192.168.1.50,从服务器ip为192.168.1.60,192.168.1.70,端口号均为6379
1.修改redis.conf配置文件

* 3台redis都设置为后台运行
  `daemonize yes`
* 2台从服务器添加(若命令行来复制的话,重启之后会无效)
 `slaveof 192.168.1.50 6379`    

2.启动3台redis
redis-server /opt/redis-3.0.7/redis.conf
3.客户端连接redis

./redis-cli 
127.0.0.1:6379> ping
PONG

4.使用info replication命令查看主从关系

192.168.1.50:6379> info replication
Replication
role:master
connected_slaves:2
slave0:ip=192.168.1.60,port=6379,state=online,offset=823,lag=1
slave1:ip=192.168.1.70,port=6379,state=online,offset=837,lag=0
master_repl_offset:837
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:2
repl_backlog_histlen:836

192.168.1.60:6379> info replication
Replication
role:slave
master_host:192.168.1.50
master_port:6379
master_link_status:up
master_last_io_seconds_ago:0
master_sync_in_progress:0
slave_repl_offset:893
slave_priority:100
slave_read_only:1
connected_slaves:0
master_repl_offset:0
repl_backlog_active:0
repl_backlog_size:1048576
repl_backlog_first_byte_offset:0
repl_backlog_histlen:0

192.168.1.70:6379> info replication
Replication
role:slave
master_host:192.168.1.50
master_port:6379
master_link_status:up
master_last_io_seconds_ago:3
master_sync_in_progress:0
slave_repl_offset:767
slave_priority:100
slave_read_only:1
connected_slaves:0
master_repl_offset:0
repl_backlog_active:0
repl_backlog_size:1048576
repl_backlog_first_byte_offset:0
repl_backlog_histlen:0

a.在主服务器中写入数据,然后可以在其他的从服务器中读取数据

192.168.1.50:6379> set test 'Hello World'
OK

192.168.1.60:6379> get test
"Hello World"

192.168.1.70:6379> get test
"Hello World"

b.从服务器中写入数据,会提示不能在只读的从服务器中写入数据

192.168.1.60:6379> set test2 hello
(error) READONLY You can't write against a read only slave.

主从复制原理

Redis的主从复制过程大体上分3个阶段:建立连接数据同步命令传播

建立连接

从服务器发出slaveof命令之后,根据主服务器的ip地址和端口建立连接

数据同步

在主从服务器建立连接确认各自身份之后,就开始数据同步,从服务器向主服务器发送PSYNC命令,执行同步操作,并把自己的数据库状态更新至主服务器的数据库状态
Redis的主从同步分为:

全量同步

有两种情况下是完整重同步,一是slave连接上master第一次复制的时候;二是如果当主从断线,重新连接复制的时候有可能是完整重同步
全量同步的步骤:

  • 从服务器连接主服务器,发送SYNC命令
  • 主服务器接收到SYNC命名后,开始执行bgsave命令生成RDB文件并使用缓冲区记录此后执行的所有写命令
  • 主服务器bgsave执行完后,向所有从服务器发送快照文件,并在发送期间继续记录被执行的写命令
  • 从服务器载入RDB文件,同步执行来自主服务器缓冲区的写命令
增量同步

用于网络中断等情况后的复制,只将中断期间主服务执行的写命令发送给从服务器
只能执行全量同步的情况:
1.当主从服务器offset的差距过大超过复制积压缓冲区长度时,将无法执行部分复制,只能执行全量同步
2.主从服务器初次复制时,主服务器将自己的runid发送给从服务器,从服务器将这个runid保存起来;当断线重连时,从服务器会将这个runid发送给主服务器;主服务器根据runid判断能否进行部分复制,如果不一致,只能执行全量同步

命令传播

当完成数据同步之后,主从服务器的数据暂时达到一致状态,当主服务器执行了客户端的写命令之后,主从的数据便不再一致。为了能够使主从服务器的数据保持一致性,主服务器会对从服务器执行命令传播操作,即每执行一个写命令就会向从服务器发送同样的写命令

查看原文

赞 0 收藏 0 评论 0

WillLiaowh 发布了文章 · 3月28日

MySQL事务

什么是事务

事务提供一种机制,可以将一个活动涉及的所有操作纳入到一个不可分割的执行单元,组成事务的所有操作只有在所有操作均能正常执行的情况下方能提交,只要其中任一操作执行失败,都将导致整个事务的回滚。简单地说,事务提供一种“要么什么都不做,要么做全套(All or Nothing)机制。
假如A要给B转账1000元,这个转账会涉及到两个关键操作就是:将A的余额减少1000元,将B的余额增加1000元。事务就是保证这两个关键操作要么都成功,要么都要失败。

ACID

事务应该具有4个属性:原子性、一致性、隔离性、持久性。这四个属性通常称为ACID特性。
原子性(Atomicity):原子性是指单个事务本身涉及到的数据库操作,要么全部成功,要么全部失败,不存在完成事务中一部分操作的可能。
以上文说的转账为例,就是要么操作全部成功,A的钱少了,B的钱多了;要么就是全部失败,AB保持和原来一样的数目。
一致性(Consistency):一致性就是事务执行前后的数据状态是稳定的。还是以转账为例,原来AB账户的钱加一起是1000,相互转账完成时候彼此还是1000,所以,对于转账就是金额稳定不变,对于其他的事务操作就是事务执行完成之后,数据库的状态是正确的,没有脏数据。
隔离性(isolation):一个事务的执行不能被其他事务干扰。也就是说多个事务之间是没有相互影响的比如A给B转账和B给C转账这两个事务是没有影响的。
持久性(durability):持久性也称永久性(permanence),指一个事务一旦提交,它对数据库中数据的改变就应该是永久性的。

事务的隔离级别

在多个事务并发操作数据库(多线程、网络并发等)的时候,如果没有有效的避免机制,就会出现脏读、不可重复读和幻读这3种问题。
脏读(Dirty Read)
脏读是指在一个事务处理过程里读取了另一个未提交的事务中的数据。
不可重复读(Nonrepeatable Read)
不可重复读是指在对于数据库中的某个数据,一个事务范围内多次查询却返回了不同的数据值,这是由于在查询间隔,被另一个事务修改并提交了。
幻读(Phantom Read)
一个事务内前后多次读取,数据总量不一致。
不可重复读和幻读有些相似,两者的区别在于:不可重复读的重点在于修改,同样的条件, 你读取过的数据,再次读取出来发现值不一样了;而幻读的重点在于新增或者删除对幻读的定义,记录的减少也应该算是幻读),同样的条件, 第 1 次和第 2 次读出来的记录数不一样。

隔离级别
读未提交(Read Uncommitted):一个事务可以读取到另一个事务未提交的修改。这种隔离级别是最弱的,可能会产生脏读,幻读,不可重复读的问题问题。
读已提交(Read Committed):一个事务只能读取另一个事务已经提交的修改。其避免了脏读,仍然存在不可以重复读和幻读的问题。SQL Server和Oracle的默认隔离级别就是这个。
可重复读(Repeated Read):同一个事务中多次读取相同的数据返回的结果是一样的。其避免了脏读和不可重复读问题,但是幻读依然存在。MySQL中的默认隔离级别就是这个,不过MySQL通过多版本并发控制(MVCC)、Next-key Lock等技术解决了幻读问题。
串行化(Serializable):这是数据库最高的隔离级别,这种级别下,事务“串行化顺序执行”,也就是一个一个排队执行。在这种级别下,脏读、不可重复读、幻读都可以被避免,但是执行效率奇差,性能开销也最大。

MySQL是如何解决幻读

可重复读的隔离级别没有办法彻底的解决幻读的问题,如果需要解决幻读的话也有两个办法:
1.使用串行化读的隔离级别
2.MVCC+next-key locks:next-key locks由record locks(索引加锁) 和 gap locks(间隙锁,每次锁住的不光是需要使用的数据,还会锁住这些数据附近的数据)

快照读

当执行select操作是innodb默认会执行快照读,会记录下这次select后的结果,之后select 的时候就会返回这次快照的数据,即使其他事务提交了不会影响当前select的数据,这就实现了可重复读了。

当前读

对于会对数据修改的操作(update、insert、delete)都是采用当前读的模式。在执行这几个操作时会读取最新的记录,即使是别的事务提交的数据也可以查询到。假设要update一条记录,但是在另一个事务中已经delete掉这条数据并且commit了,如果update就会产生冲突,所以在update的时候需要知道最新的数据。

MVCC仅能解决快照读情况下,也就是select查询的时候的幻读问题。对于当前读情况下,也就是update、insert、delete等操作时产生幻读问题MVCC无法解决,需要使用临键锁也就是next-key locks解决

查看原文

赞 0 收藏 0 评论 0

WillLiaowh 发布了文章 · 3月28日

Redisson 分布式锁实现原理

使用

image.png

加锁机制

lock()底层是通过一段lua脚本实现的
image.png
KEYS[1]代表你加锁的那个key,RLock lock = redisson.getLock("myLock");这里你自己设置了加锁的那个锁key就是“myLock”
ARGV[1]代表的就是锁key的默认生存时间,默认30秒
ARGV[2]代表的是加锁的客户端的ID,类似于下面这样:8743c9c0-0795-4907-87fd-6c719a6b4586:1
加锁流程:
1.判断是否存在这个加锁的key
2.如果不存在,通过hset命令加锁
3.设置过期时间

锁互斥机制

如果客户端2来尝试加锁,执行了同样的一段lua脚本。
1.第一个if判断这个key发现已存在,走第二个if判断key中的客户端的id是否与是客户端2的id
2.如果发现不是,客户端2会获取到pttl myLock返回的一个数字,这个数字代表了myLock这个锁key的剩余生存时间。比如还剩15000毫秒的生存时间。此时客户端2会进入一个while循环,不停的尝试加锁。
实际上的原理:
当锁正在被占用时,等待获取锁的进程并不是通过一个 while(true) 死循环去获取锁,而是利用了 Redis 的发布订阅机制,通过 await 方法阻塞等待锁的进程,有效的解决了无效的锁申请浪费资源的问题

watch dog自动延期机制

客户端1加锁的锁key默认生存时间才30秒,如果超过了30秒,客户端1还想一直持有这把锁,怎么办呢?
简单!只要客户端1一旦加锁成功,就会启动一个watch dog看门狗,他是一个后台线程,会每隔10秒检查一下,如果客户端1还持有锁key,过期时间继续延长为30s。

可重入加锁机制

image.png
上面那段lua脚本。第一个if判断肯定不成立,“exists myLock”会显示锁key已经存在了。第二个if判断会成立,因为myLock的hash数据结构中包含的那个ID,就是客户端1的那个ID,也就是“8743c9c0-0795-4907-87fd-6c719a6b4586:1”,此时就会执行可重入加锁的逻辑,他会用:incrby myLock 8743c9c0-0795-4907-87fd-6c71a6b4586:1 1 ,通过这个命令,对客户端1的加锁次数,累加1,变为myLock 8743c9c0-0795-4907-87fd-6c71a6b4586:1 2

锁释放机制

如果执行lock.unlock(),就可以释放分布式锁,此时的业务逻辑也是非常简单的。其实说白了,就是每次都对myLock数据结构中的那个加锁次数减1。如果发现加锁次数是0了,说明这个客户端已经不再持有锁了,此时就会用:“del myLock”命令,从redis里删除这个key

此种方案Redis分布式锁的缺陷

方案最大的问题,就是如果你对某个redis master实例,写入了myLock这种锁key的value,此时会异步复制给对应的master slave实例。但是这个过程中一旦发生redis master宕机,主备切换,redis slave变为了redis master。接着就会导致,客户端2来尝试加锁的时候,在新的redis master上完成了加锁,而客户端1也以为自己成功加了锁。此时就会导致多个客户端对一个分布式锁完成了加锁。这时系统在业务语义上一定会出现问题,导致各种脏数据的产生
所以这个就是redis cluster,或者是redis master-slave架构的主从异步复制导致的redis分布式锁的最大缺陷:在redis master实例宕机的时候,可能导致多个客户端同时完成加锁

参考大佬:
拜托,面试请不要再问我Redis分布式锁的实现原理!

查看原文

赞 0 收藏 0 评论 0

WillLiaowh 赞了文章 · 3月25日

百亿级数据分表后怎么分页查询?

当业务规模达到一定规模之后,像淘宝日订单量在5000万单以上,美团3000万单以上。数据库面对海量的数据压力,分库分表就是必须进行的操作了。而分库分表之后一些常规的查询可能都会产生问题,最常见的就是比如分页查询的问题。一般我们把分表的字段称作shardingkey,比如订单表按照用户ID作为shardingkey,那么如果查询条件中不带用户ID查询怎么做分页?又比如更多的多维度的查询都没有shardingkey又怎么查询?

唯一主键

一般我们数据库的主键都是自增的,那么分表之后主键冲突的问题就是一个无法避免的问题,最简单的办法就是以一个唯一的业务字段作为唯一的主键,比如订单表的订单号肯定是全局唯一的。

常见的分布式生成唯一ID的方式很多,最常见的雪花算法Snowflake、滴滴Tinyid、美团Leaf。以雪花算法举例来说,一毫秒可以生成4194304多个ID。

第一位不使用,默认都是0,41位时间戳精确到毫秒,可以容纳69年的时间,10位工作机器ID高5位是数据中心ID,低5位是节点ID,12位序列号每个节点每毫秒累加,累计可以达到2^12 4096个ID。

分表

第一步,分表后要怎么保证订单号的唯一搞定了,现在考虑下分表的问题。首先根据自身的业务量和增量来考虑分表的大小。

举个例子,现在我们日单量是10万单,预估一年后可以达到日100万单,根据业务属性,一般我们就支持查询半年内的订单,超过半年的订单需要做归档处理。

那么以日订单100万半年的数量级来看,不分表的话我们订单量将达到100万X180=1.8亿,以这个数据量级部分表的话肯定单表是扛不住的,就算你能扛RT的时间你也根本无法接受吧。根据经验单表几百万的数量对于数据库是没什么压力的,那么只要分256张表就足够了,1.8亿/256≈70万,如果为了保险起见,也可以分到512张表。那么考虑一下,如果业务量再增长10倍达到1000万单每天,分表1024就是比较合适的选择。

通过分表加上超过半年的数据归档之后,单表70万的数据就足以应对大部分场景了。接下来对订单号hash,然后对256取模的就可以落到具体的哪张表了。

那么,因为唯一主键都是以订单号作为依据,以前你写的那些根据主键ID做查询的就不能用了,这就涉及到了历史一些查询功能的修改。不过这都不是事儿对吧,都改成以订单号来查就行了。这都不是问题,问题在我们的标题说的点上。

C端查询

说了半天,总算到了正题了,那么分表之后查询和分页查询的问题怎么解决?

首先说带shardingkey的查询,比如就通过订单号查询,不管你分页还是怎么样都是能直接定位到具体的表来查询的,显然查询是不会有什么问题的。

如果不是shardingkey的话,上面举例说的以订单号作为shardingkey的话,像APP、小程序这种一般都是通过用户ID查询,那这时候我们通过订单号做的sharding怎么办?很多公司订单表直接用用户ID做shardingkey,那么很简单,直接查就完了。那么订单号怎么办,一个很简单的办法就是在订单号上带上用户ID的属性。举个很简单的例子,原本41位的时间戳你觉得用不完,用户ID是10位的,订单号的生成规则带上用户ID,落具体表的时候根据订单号中10位用户ID hash取模,这样无论根据订单号还是用户ID查询效果都是一样的。

当然,这种方式只是举例,具体的订单号生成的规则,多少位,包含哪些因素根据自己的业务和实现机制来决定。

好,那么无论你是订单号还是用户ID作为shardingkey,按照以上的两种方式都可以解决问题了。那么还有一个问题就是如果既不是订单号又不是用户ID查询怎么办?最直观的例子就是来自商户端或者后台的查询,商户端都是以商户或者说卖家的ID作为查询条件来查的,后台的查询条件可能就更复杂了,像我碰到的有些后台查询条件能有几十个,这怎么查???别急,接下来分开说B端和后台的复杂查询。

现实中真正的流量大头都是来自于用户端C端,所以本质上解决了用户端的问题,这个问题就解了大半,剩下来自商户卖家端B端、后台支持运营业务的查询流量并不会很大,这个问题就好解。

其他端查询

针对B端的非shardingkey的查询有两个办法解决。

双写,双写就是下单的数据落两份,C端和B端的各自保存一份,C端用你可以用单号、用户ID做shardingkey都行,B端就用商家卖家的ID作为shardingkey就好了。有些同学会说了,你双写不影响性能吗?因为对于B端来说轻微的延迟是可以接受的,所以可以采取异步的方式去落B端订单。你想想你去淘宝买个东西下单了,卖家稍微延迟个一两秒收到这个订单的消息有什么关系吗?你点个外卖商户晚一两秒收到这个订单有什么太大影响吗?

这是一个解决方案,另外一个方案就是走离线数仓或者ES查询,订单数据落库之后,不管你通过binlog还是MQ消息的都形式,把数据同步到数仓或者ES,他们支持的数量级对于这种查询条件来说就很简单了。同样这种方式肯定是稍微有延迟的,但是这种可控范围的延迟是可以接受的。

而针对管理后台的查询,比如运营、业务、产品需要看数据,他们天然需要复杂的查询条件,同样走ES或者数仓都可以做得到。如果不用这个方案,又要不带shardingkey的分页查询,兄弟,这就只能扫全表查询聚合数据,然后手动做分页了,但是这样查出来的结果是有限制的。

比如你256个片,查询的时候循环扫描所有的分片,每个片取20条数据,最后聚合数据手工分页,那必然是不可能查到全量的数据的。

总结

分库分表后的查询问题,对于有经验的同学来说其实这个问题都知道,但是我相信其实大部分同学做的业务可能都没来到这个数量级,分库分表可能都停留在概念阶段,面试被问到后就手足无措了,因为没有经验不知道怎么办。

分库分表首先是基于现有的业务量和未来的增量做出判断,比如拼多多这种日单量5000万的,半年数据得有百亿级别了,那都得分到4096张表了对吧,但是实际的操作是一样的,对于你们的业务分4096那就没有必要了,根据业务做出合理的选择。

对于基于shardingkey的查询我们可以很简单的解决,对于非shardingkey的查询可以通过落双份数据和数仓、ES的方案来解决,当然,如果分表后数据量很小的话,建好索引,扫全表查询其实也不是什么问题。

查看原文

赞 7 收藏 5 评论 1

WillLiaowh 关注了用户 · 3月25日

艾小仙 @goudantiezhuerzi

关注 3

WillLiaowh 发布了文章 · 3月21日

不看源码就硬聊AQS实现原理

看了下AQS的源码,有点复杂,不适合简单入门,我总结了下。

概述

Java中的大部分同步类(Lock、Semaphore、ReentrantLock等)都是基于AbstractQueuedSynchronizer(简称为AQS)实现的。AQS是一种提供了原子式管理同步状态、阻塞和唤醒线程功能以及队列模型的简单框架。

从ReentrantLock独占模式看AQS原理

public void test () throw Exception {
    //初始化
    ReentrantLock lock = new ReentrantLock(true);
    //加锁
    lock.lock();
    try {
        ...
    } finally {
        lock.unlock();
    }
}

lock()

image.png
看看非公平锁的实现:
image.png
核心流程从这里开始:

1.compareAndSetState():线程进来直接利用CAS尝试抢占锁;setExclusiveOwnerThread():如果抢占成功state值会被改为1,且设置对象独占锁线程为当前线程

2.acquire(1):若利用CAS尝试抢占锁失败,也就是获取锁失败,则进入Acquire方法进行后续处理
Acquire方法实现:
image.png
1.tryAcquire():再次尝试获取锁,如果加锁成功则返回true,不再执行以下步骤,否则继续执行以下步骤
2.addWaiter():走到这里说明加锁失败,创建一个Node节点绑定当前的线程,加入到一个FIFO的双向链表中,然后返回这个Node
3.acquireQueued():这个方法会先判断当前传入的Node对应的前置节点是否为head节点,如果是则尝试加锁,如果加锁失败或者Node的前置节点不是head节点,用LockSupport.park()挂起当前线程。
上述流程图:
image.png

unlock()

image.png
image.png
1.tryRelease():state被设置成0,Lock对象的独占锁被设置为null
2.unparkSuccessor():唤醒head的后置节点,被唤醒的线程二会接着尝试获取锁,用CAS指令修改state数据。
上述流程图:
image.png

总结:
AQS中 维护了一个volatile int state(代表共享资源)和一个FIFO线程等待队列(多线程争用资源被阻塞时会进入此队列)。
这里volatile能够保证多线程下的可见性,当state=1则代表当前对象锁已经被占有,其他线程来加锁时则会失败,加锁失败的线程会被放入一个FIFO的等待队列中,比列会被UNSAFE.park()操作挂起,等待其他获取锁的线程释放锁才能够被唤醒。
另外state的操作都是通过CAS来保证其并发修改的安全性。

非公平锁和公平锁

线程二释放锁的时候,唤醒被挂起的线程三线程三执行tryAcquire()方法使用CAS操作来尝试修改state值,如果此时又来了一个线程四也来执行加锁操作,同样会执行tryAcquire()方法。这种情况就会出现竞争,线程四如果获取锁成功,线程三仍然需要待在等待队列中被挂起。这就是所谓的非公平锁线程三辛辛苦苦排队等到自己获取锁,却眼巴巴的看到线程四插队获取到了锁。
非公平锁执行流程:
image.png
公平锁在加锁的时候,会先判断AQS等待队列中是存在节点,如果存在其他等待线程,那么自己也会加入到等待队列尾部,做到真正的先来后到,有序加锁。
公平锁执行流程:
image.png

非公平锁和公平锁的区别:
非公平锁性能高于公平锁性能。非公平锁可以减少CPU唤醒线程的开销,整体的吞吐效率会高点,CPU也不必取唤醒所有线程,会减少唤起线程的数量
非公平锁性能虽然优于公平锁,但是会存在导致线程饥饿的情况。在最坏的情况下,可能存在某个线程一直获取不到锁。不过相比性能而言,饥饿问题可以暂时忽略,这可能就是ReentrantLock默认创建非公平锁的原因之一了。

从CountDownLatch共享模式看AQS原理

    void test() throws Exception {
        CountDownLatch latch = new CountDownLatch(2);
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("线程1执行");
                latch.countDown();
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("线程2执行");
                latch.countDown();
            }
        }).start();
        latch.await();
        System.out.println("线程3执行");
    }

初始化

image.png
初始化state值,当state值>0代表锁被占有,=0说明锁被释放

await()

image.png
image.png
image.png
1.tryAcquireShared():state不等于0的时候,tryAcquireShared()返回的是-1,此时获取锁失败,也就是说count未减到0的时候所有调用await()方法的线程都要排队。
2.doAcquireSharedInterruptibly():创建一个Node节点绑定当前的线程,加入到一个FIFO的双向链表中,先判断当前传入的Node对应的前置节点是否为head节点,如果是则尝试加锁,如果加锁失败或者Node的前置节点不是head节点,用LockSupport.park()挂起当前线程。

countDown()

image.png
image.png
1.tryReleaseShared():释放锁,通过自旋的CAS操作对state-1,如果state=0,返回true执行doReleaseShared()
2.doReleaseShared():唤醒等待await()的线程

总结

独占模式流程:
1.tryRequire()方法尝试获取锁,具体通过CAS操作尝试修改state值,成功则设置state值为1,且设置对象独占锁线程为当前线程
2.获取失败,创建一个Node节点绑定当前的线程,加入到一个FIFO的双向链表中
3.如果持有锁的线程使用tryRelease()释放了锁,state重新设置为0,独占线程设置为null,唤醒队列中的第一个Node节点中的线程再次争抢锁

共享模式流程:
1.tryRequireShared()方法尝试获取锁,具体通过判断当前state值,>0则代表获取锁失败,=0则获取锁成功
2.获取失败,创建一个Node节点绑定当前的线程,加入到一个FIFO的双向链表中
3.如果持有锁的线程使用tryRelease()释放了锁,会state进行-1,当state=0时,唤醒队列中所有的Node节点中的线程

参考大佬:
我画了35张图就是为了让你深入 AQS
从ReentrantLock的实现看AQS的原理及应用

查看原文

赞 0 收藏 0 评论 0

WillLiaowh 赞了回答 · 3月8日

关于Kafka重试达到次数之后的处理方案

不是已经回答过你一次了吗,当重试次数超过预设的阈值后,需要怎么处理是由你自己的业务场景决定。常见的做法包括

  • 继续重试直到天荒地老,如果是网线断了这种方法非常低效。
  • 如果业务对丢失消息不敏感,可以直接抛弃这条信息
  • 如果业务对丢消息敏感,可以把错误记录下来,然后告警让人工介入处理

总之怎么处理是由你的业务决定,消费者也一样。

关注 1 回答 1

WillLiaowh 发布了文章 · 3月7日

Spring Aop实现原理

JDK动态代理
jdk动态代理是利用反射机制生成一个实现代理接口的匿名类,在调用业务方法前调用InvocationHandler处理。代理类必须实现InvocationHandler接口,并且,JDK动态代理只能代理实现了接口的类,没有实现接口的类是不能实现JDK动态代理。
使用JDK动态代理类基本步骤:
1、需要被代理的类和接口

public interface OrderService {
    public void save(UUID orderId, String name);
}

public class OrderServiceImpl implements OrderService {
    @Override
    public void save(UUID orderId, String name) {
        System.out.println("call save()方法,save:" + name);
    }
}

2、代理类,需要实现InvocationHandler接口,重写invoke方法

public class JDKProxy implements InvocationHandler {
    //需要代理的目标对象
    private Object targetObject;
    
    public Object createProxyInstance(Object targetObject) {
        this.targetObject = targetObject;
        return Proxy.newProxyInstance(this.targetObject.getClass().getClassLoader(),
                this.targetObject.getClass().getInterfaces(), this);
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //被代理对象
        OrderServiceImpl bean = (OrderServiceImpl) this.targetObject;
        Object result = null;
        //切面逻辑(advise),此处是在目标类代码执行之前
        System.out.println("---before invoke----");
        if (bean.getUser() != null) {
            result = method.invoke(targetObject, args);
        }
        System.out.println("---after invoke----");
        return result;
    }
}

3、使用Proxy.newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h)动态创建代理类对象,通过代理类对象调用业务方法。

public class AOPTest {
    public static void main(String[] args) {
        JDKProxy factory = new JDKProxy();
        //Proxy为InvocationHandler实现类动态创建一个符合某一接口的代理实例  
        OrderService orderService = (OrderService) factory.createProxyInstance(new OrderServiceImpl("JDK"));
        //由动态生成的代理对象来orderService 代理执行程序
        orderService.save(UUID.randomUUID(), "aoho");
    }

}

---before invoke----
call save()方法,save:JDK
---after invoke----

Cglib动态代理
cglib是针对类来实现代理的,它会对目标类产生一个代理子类,通过方法拦截技术对过滤父类的方法调用。代理子类需要实现MethodInterceptor接口
1.被代理类,该类可以实现接口,也可以不实现接口

public class OrderManager {
    public void save(UUID orderId, String name) {
        System.out.println("call save()方法,save:" + name);
    }
}

2.代理类,实现MethodInterceptor接口,重写intercept方法

public class CGLibProxy implements MethodInterceptor {
        // CGLib需要代理的目标对象
        private Object targetObject;

       public Object createProxyObject(Object obj) {
        this.targetObject = obj;
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(obj.getClass());
        //回调方法的参数为代理类对象CglibProxy,最后增强目标类调用的是代理类对象CglibProxy中的intercept方法 
        enhancer.setCallback(this);
        //增强后的目标类
        Object proxyObj = enhancer.create();
        // 返回代理对象
        return proxyObj;
    }

    @Override
    public Object intercept(Object proxy, Method method, Object[] args,
                            MethodProxy methodProxy) throws Throwable {
        Object obj = null;
        //切面逻辑(advise),此处是在目标类代码执行之前
        System.out.println("---before intercept----");
        obj = method.invoke(targetObject, args);
        System.out.println("---after intercept----");
        return obj;
    }
}

3.创建代理类对象,通过代理类对象调用业务方法

public class AOPTest {
    public static void main(String[] args) {
        OrderManager order = (OrderManager) new CGLibProxy().createProxyObject(new OrderManager("aoho"));
        order.save(UUID.randomUUID(), "CGLIB");
    }
    
---before invoke----
call save()方法,save:CGLIB
---after invoke----

JDK与Cglib动态代理对比?
1、JDK动态代理只能代理实现了接口的类,没有实现接口的类不能实现JDK的动态代理;
2、Cglib动态代理是针对类实现代理的,运行时动态生成被代理类的子类拦截父类方法调用,因此不能代理声明为final类型的类和方法;

Spring如何选择两种代理模式的?
1、如果目标对象实现了接口,则默认采用JDK动态代理;
2、如果目标对象没有实现接口,则使用Cglib代理;
3、如果目标对象实现了接口,但强制使用了Cglib,则使用Cglib进行代理
我们可以结合源码来看下,上面的选择:

image.png

参考:
深入理解Spring AOP的动态代理

查看原文

赞 1 收藏 1 评论 0

认证与成就

  • 获得 9 次点赞
  • 获得 3 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 3 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2019-04-13
个人主页被 2.9k 人浏览