면접 전에 회사의 면접 자료에 대해 자세히 읽어보세요. 이후 면접에 큰 도움이 될 것입니다. 오늘은 실제 Shopee 서버 인터뷰 질문 15개(답변 분석 포함)를 공유해 드리겠습니다. 오셔서 귀하의 레벨이 어떤지 확인해보세요!
연결리스트의 헤드노드가 주어지면 오름차순으로 정렬하여 정렬된 연결리스트를 반환해주세요.
예제 1:
输入:head = [4,2,1,3] 输出:[1,2,3,4]
예제 2:
输入:head = [-1,5,3,4,0] 输出:[-1,0,3,4,5]
이 문제는 이중 포인터 + 병합 정렬 알고리즘을 사용하여 해결할 수 있으며 주로 다음 네 단계를 따릅니다
1. 빠르고 느린 포인터 방법, 탐색 중간 노드를 찾기 위한 연결 리스트
2. 중간 노드에서 연결 리스트를 잘라낸다
3. 병합 정렬을 사용하여 왼쪽과 오른쪽 하위 연결 목록을 각각 정렬
4. 하위 연결 목록을 병합
The 완전한 코드는 다음과 같습니다.
class Solution { public ListNode sortList(ListNode head) { //如果链表为空,或者只有一个节点,直接返回即可,不用排序 if (head == null || head.next == null) return head; //快慢指针移动,以寻找到中间节点 ListNode slow = head; ListNode fast = head; while(fast.next!=null && fast.next.next !=null){ fast = fast.next.next; slow = slow.next; } //找到中间节点,slow节点的next指针,指向mid ListNode mid = slow.next; //切断链表 slow.next = null; //排序左子链表 ListNode left = sortList(head); //排序左子链表 ListNode right = sortList(mid); //合并链表 return merge(left,right); } public ListNode merge(ListNode left, ListNode right) { ListNode head = new ListNode(0); ListNode temp = head; while (left != null && right != null) { if (left.val <= right.val) { temp.next = left; left = left.next; } else { temp.next = right; right = right.next; } temp = temp.next; } if (left != null) { temp.next = left; } else if (right != null) { temp.next = right; } return head.next; } }
첫 번째 검토 관련 개념:
Clear text: 암호화되지 않은 정보/데이터를 의미합니다.
암호문: 일반 텍스트가 암호화 알고리즘에 의해 암호화된 후 데이터 보안을 보장하기 위해 암호문이 됩니다.
Key: 평문을 암호문으로, 또는 암호문을 평문으로 변환하는 알고리즘에 입력되는 매개변수입니다. 키는 대칭키와 비대칭키로 구분됩니다.
암호화: 일반 텍스트를 암호문으로 바꾸는 프로세스입니다.
복호화: 암호문을 일반 텍스트로 줄이는 과정입니다.
대칭 암호화 알고리즘: 암호화 및 복호화에 동일한 키를 사용하는 암호화 알고리즘입니다. 일반적인 대칭 암호화 알고리즘에는 AES, 3DES, DES, RC5, RC6 등이 포함됩니다.
비대칭 암호화 알고리즘: 비대칭 암호화 알고리즘에는 두 개의 키(공개 키와 개인 키)가 필요합니다. 공개 키와 개인 키는 쌍으로 존재합니다. 공개 키를 사용하여 데이터를 암호화하면 해당 개인 키만 해독할 수 있습니다. 주요 비대칭 암호화 알고리즘은 RSA, Elgamal, DSA, D-H, ECC입니다.
먼저 TCP 연결은 3방향 핸드셰이크를 기반으로 하고 연결 해제는 4파를 기반으로 합니다. 안정적인 연결 및 연결 해제를 보장합니다.
두 번째로, TCP의 신뢰성은 상태 유지에도 반영됩니다. TCP는 전송된 데이터, 허용된 데이터, 허용되지 않은 데이터를 기록하고 데이터 패킷이 순서대로 도착하는지 확인합니다. 데이터 전송 오류.
다시 한번, TCP의 신뢰성은 제어 가능성에도 반영됩니다. 여기에는 메시지 확인, ACK 응답, 시간 초과 재전송(발신자), 순서를 벗어난 데이터 재전송(수신자), 중복 데이터 삭제, 흐름 제어(슬라이딩 윈도우) 및 혼잡 제어와 같은 메커니즘이 있습니다.
4.1 차단 IO 모델
애플리케이션 프로세스가 IO 호출을 시작하지만 커널 데이터가 아직 준비되지 않은 경우 애플리케이션 프로세스가 성공 프롬프트를 반환하기 전에 커널 데이터가 준비되어 커널에서 사용자 공간으로 복사될 때까지 차단하고 대기했습니다. 이 IO 작업을 차단 IO라고 합니다.
4.2 Non-blocking IO 모델
커널 데이터가 아직 준비되지 않은 경우 먼저 사용자 프로세스에 오류 메시지를 반환하여 기다릴 필요 없이 다시 요청할 수 있습니다. 여론조사를 통해. 이는 비차단 IO입니다. 순서도는 다음과 같습니다.
4.3 IO 다중화 모델
IO 다중화 선택
응용 프로그램 프로세스는 호출을 통해 동시에 여러 장치를 모니터링할 수 있습니다. select 함수 fd, select 함수에 의해 모니터링되는 fd에서 데이터 상태가 준비된 한 select 함수는 읽기 가능한 상태로 돌아간 다음 애플리케이션 프로세스가 데이터를 읽기 위해 recvfrom 요청을 시작합니다.
select에는 몇 가지 단점이 있습니다.
최대 연결 수는 제한되어 있으며 일반적으로 Linux 시스템에서는 1024개입니다.
select 함수가 반환된 후 fdset를 순회하여 준비된 설명자 fd를 찾습니다.
IO 다중화 epoll
select의 문제를 해결하기 위해 이벤트 드라이브를 사용하여 다중화 모델 epoll이 탄생했습니다. 플로우 차트는 다음과 같습니다.
epoll은 먼저 epoll_ctl()을 통해 fd(파일 설명자)를 등록합니다. 특정 fd가 준비되면 커널은 콜백 메커니즘을 사용하여 프로세스가 epoll_wait()를 호출하면 알림을 받습니다. 여기에서는 파일 설명자를 탐색하는 까다로운 작업이 제거되고 이벤트 콜백을 수신하는 메커니즘이 사용됩니다. 이것이 epoll의 하이라이트입니다.
4.4 IO 모델의 신호 기반 모델
신호 기반 IO는 더 이상 데이터가 준비되었는지 확인하기 위해 활성 조회를 사용하지 않고 커널에 신호를 보냅니다(sigaction 호출 시 SIGIO 신호 생성). 그러면 애플리케이션 사용자 프로세스는 차단하지 않고 다른 작업을 수행할 수 있습니다. 커널 데이터가 준비되면 SIGIO 신호를 통해 데이터를 읽을 수 있게 준비되었음을 애플리케이션 프로세스에 알립니다. 애플리케이션 사용자 프로세스는 신호를 받은 후 즉시 recvfrom을 호출하여 데이터를 읽습니다.
4.5 IO 모델 비동기 IO(AIO)
AIO는 전체 IO 프로세스의 비차단을 실현합니다. 즉, 애플리케이션 프로세스가 시스템 호출을 발행한 후 즉시 반환되지만 반환되는 것은 무엇입니까? 즉시는 처리 결과가 아니며, 제출이 성공적으로 이루어졌음을 의미합니다. 커널 데이터가 준비되면 데이터를 사용자 프로세스 버퍼에 복사하고 IO 작업이 완료되었음을 사용자 프로세스에 알리는 신호를 보냅니다.
프로세스는 다음과 같습니다.
Hystrix 작업 흐름 다이어그램은 다음과 같습니다.
1. Hystrix는 HystrixCommand와 HystrixObserv라는 두 가지 명령 개체를 제공합니다. 가능한 명령 , 종속성 요청 작업 중 하나를 대신하여 종속성을 요청하는 데 필요한 매개변수를 생성자에 전달합니다.
2. 명령 실행
Hystrix 명령을 실행하는 방법에는 네 가지가 있습니다.
Hystrix 캐시가 활성화되어 있으면 이전에 동일한 명령 실행에 대한 캐시가 있는지 판단됩니다. 작업이 실행됩니다. 있는 경우 캐시된 응답을 포함하는 Observable이 직접 반환됩니다. 캐시된 결과가 없지만 캐싱이 활성화된 경우 실행 결과는 후속 사용을 위해 캐시됩니다.
4. 회로 차단기가 열려 있는지 확인하세요. 회로 차단기(회로 차단기)는 퓨즈와 유사합니다. 위험이 발생하면 회로를 보호하기 위해 퓨즈가 끊어지지만, 회로 차단기는 다음과 같은 경우 단락을 유발할 수 있습니다. 우리가 설정한 임계값(예: 요청 실패율이 50%에 도달)에 도달하고 요청 실행을 거부합니다.
루퍼가 열리면 Hystrix는 명령을 실행하지 않고 Fallback 처리 로직에 직접 들어갑니다.
5. 스레드 풀/세마포어/큐 상태를 확인하세요. Hystrix 격리 방법에는 스레드 풀 격리와 세마포어 격리가 포함됩니다. Hystrix 스레드 풀을 사용할 때 Hystrix는 기본적으로 각 종속 서비스에 10개의 스레드를 할당합니다. 10개의 스레드가 모두 사용 중이면 명령 실행이 거부되고 대신 즉시 fallback 로직 실행으로 점프합니다.
6. 특정 작업을 수행하려면 HystrixObservableCommand.construct() 또는 HystrixCommand.run()을 사용하여 사용자의 실제 작업을 실행하세요.
7. 명령 실행이 시작되거나 명령 실행이 종료되거나 예외가 발생할 때마다 성공, 실패, 거부, 시간 초과 및 기타 표시기와 같은 실행 상태가 기록됩니다. 데이터는 정기적으로 처리된 후 설정된 조건에 따라 루퍼를 열지 여부를 결정합니다.
8. 명령이 실패하면 Fallback 논리를 실행합니다. 명령이 실패하면 사용자가 지정한 Fallback 논리를 실행합니다. 위 그림에서 회로 차단, 스레드 풀 거부, 세마포어 거부, 실행 실행 및 실행 시간 초과는 모두 Fallback 처리에 들어갑니다.
9. 실행 결과 반환 원래 개체 결과는 사용자에게 반환되기 전에 호출 방법에 따라 일부 처리가 수행됩니다.
6. 지연 시나리오 처리HTTPS = HTTP + SSL/TLS, 즉 SSL/TLS를 사용하여 데이터를 암호화 및 해독하고 Http를 사용하여 전송합니다.
SSL(Secure Sockets Layer)은 네트워크 통신에 보안과 데이터 무결성을 제공하는 보안 프로토콜입니다.
TLS, 전송 계층 보안(Secure Transport Layer Protocol)은 SSL 3.0의 후속 버전입니다.
http 요청 프로세스
1. 사용자는 브라우저에 https URL을 입력한 후 서버의 443 포트에 연결합니다.
2. 서버에는 디지털 인증서 세트가 있어야 합니다. 직접 만들 수도 있고 기관에 신청할 수도 있습니다. 차이점은 직접 발급한 인증서는 클라이언트에서 확인해야 한다는 것입니다. 이 인증서 세트는 실제로 공개 키와 개인 키의 쌍입니다.
3. 서버는 자체 디지털 인증서(공개 키 포함)를 클라이언트에 보냅니다.
4. 클라이언트는 서버로부터 디지털 인증서를 받은 후 이를 확인합니다. 실패하면 경고 상자가 나타납니다. 인증서가 정상이면 키가 생성되고(대칭 암호화) 인증서의 공개 키로 암호화됩니다.
5. 클라이언트는 HTTPS에서 두 번째 HTTP 요청을 시작하고 암호화된 클라이언트 키를 서버에 보냅니다.
6. 서버는 클라이언트로부터 암호문을 수신한 후 자체 개인 키를 사용하여 이를 비대칭적으로 해독합니다. 해독 후 클라이언트 키를 사용하여 반환된 데이터를 대칭적으로 암호화합니다. 이렇게 하면 데이터가 암호문이 됩니다.
7. 서버는 암호화된 암호문을 클라이언트에 반환합니다.
8. 클라이언트는 서버가 반환한 암호문을 수신하고 자체 키(클라이언트 키)를 사용하여 이를 대칭적으로 해독한 후 서버가 반환한 데이터를 얻습니다.
8.1 데이터베이스의 4가지 주요 격리 수준
dirty reading, non-reading 문제를 해결하기 위해 동시 트랜잭션에 존재하는 반복 읽기, 팬텀 읽기 등 질문, 데이터베이스 삼촌은 네 가지 격리 수준을 설계했습니다. 커밋되지 않은 읽기, 커밋된 읽기, 반복 가능한 읽기 및 직렬화 가능입니다.
읽기 커밋되지 않은 격리 수준: 두 개의 데이터를 동시에 수정할 수 없도록 제한합니다. 그러나 데이터가 수정되면 트랜잭션이 제출되지 않더라도 다른 트랜잭션에서 읽을 수 있는 수준입니다. 트랜잭션 격리는 읽기, 반복 읽기 및 가상 읽기 문제입니다.
읽기 커밋 격리 수준: 현재 트랜잭션은 다른 트랜잭션에서 제출한 데이터만 읽을 수 있으므로 이 트랜잭션 격리 수준은 더티 읽기 문제를 해결합니다. 여전히 존재하는 반복 읽기 및 팬텀 읽기 문제
반복 읽기: 데이터를 읽을 때 수정을 제한하여 반복 읽기 문제를 해결하지만, 범위 데이터를 읽을 때 데이터를 삽입할 수 있으므로 팬텀 읽기도 발생합니다.
직렬화: 트랜잭션에 대한 가장 높은 격리 수준입니다. 이 수준에서는 모든 트랜잭션이 순차적으로 실행됩니다. 더티 읽기, 반복 불가능한 읽기 및 팬텀 읽기의 모든 동시성 문제를 피할 수 있습니다. 그러나 이 트랜잭션 격리 수준에서는 트랜잭션 실행이 매우 성능 집약적입니다.
4가지 주요 격리 수준에는 어떤 종류의 동시성 문제가 존재합니까?
격리 수준 | 더티 읽기 | 반복 불가능한 읽기 | 팬텀 읽기 |
---|---|---|---|
커밋되지 않은 읽기 | √ | √ | √ |
읽기 제출 | × | √ | √ |
반복 읽기 | × | × | √ |
직렬화 | × | × | × |
8.2 읽기 보기 가시성 규칙
Variable | Description |
---|---|
m_ids | 현재 시스템의 활성(커밋되지 않은) 읽기 및 쓰기 트랜잭션 ID는 다음과 같습니다. 목록 . |
max_limit_id | 는 읽기 보기가 생성될 때 시스템의 다음 트랜잭션에 할당되어야 하는 id 값을 나타냅니다. |
min_limit_id | 는 Read View 생성 시 현재 시스템에서 활성화된 읽기 및 쓰기 트랜잭션 중 가장 작은 트랜잭션 ID, 즉 m_ids에서 가장 작은 값을 나타냅니다. |
creator_trx_id | 현재 읽기 보기의 거래 ID를 생성합니다 |
읽기 보기의 가시성 규칙은 다음과 같습니다.
데이터 트랜잭션 ID가 trx_id 인 경우 이 버전을 생성한 트랜잭션이 <code>읽기를 생성하기 전에 제출되었음을 나타냅니다. 보기
(트랜잭션 ID가 증가하기 때문에) 현재 트랜잭션에서 이 버전에 액세스할 수 있습니다. trx_id ,表明生成该版本的事务在生成<code>Read View
前,已经提交(因为事务ID是递增的),所以该版本可以被当前事务访问。
如果trx_id>= max_limit_id
,表明生成该版本的事务在生成Read View
后才生成,所以该版本不可以被当前事务访问。
如果 min_limit_id =<trx_id max_limit_id>,需要分3种情况讨论</trx_id>
1)如果
m_ids
包含trx_id
,则代表Read View
生成时刻,这个事务还未提交,但是如果数据的trx_id
等于creator_trx_id
的话,表明数据是自己生成的,因此是可见的。2)如果
m_ids
包含trx_id
,并且trx_id
不等于creator_trx_id
,则Read View
生成时,事务未提交,并且不是自己生产的,所以当前事务也是看不见的;3)如果
m_ids
不包含trx_id
,则说明你这个事务在Read View
生成之前就已经提交了,修改的结果,当前事务是能看见的。
8.3 可重复读实现原理
数据库是通过加锁实现隔离级别的,比如,你想一个人静静,不被别人打扰,你可以把自己关在房子,并在房门上加上一把锁!串行化隔离级别就是加锁实现的。但是如果频繁加锁,性能会下降。因此设计数据库的大叔想到了MVCC。
可重复读的实现原理就是MVCC多版本并发控制。在一个事务范围内,两个相同的查询,读取同一条记录,却返回了不同的数据,这就是不可重复读。可重复读隔离级别,就是为了解决不可重复读问题。
查询一条记录,基于MVCC,是怎样的流程呢?
获取事务自己的版本号,即事务ID
获取Read View
查询得到的数据,然后Read View中的事务版本号进行比较。
如果不符合Read View的可见性规则, 即就需要Undo log中历史快照;
最后返回符合规则的数据
InnoDB 实现MVCC,是通过Read View+ Undo Log
实现的,Undo Log
保存了历史快照,Read View
可见性规则帮助判断当前版本的数据是否可见。
可重复读(RR)隔离级别,是如何解决不可重复读问题的?
假设存在事务A和B,SQL执行流程如下
在可重复读(RR)隔离级别下,一个事务里只会获取一次read view
trx_id>= max_limit_id
인 경우 이 버전을 생성한 트랜잭션이 읽기 보기
를 생성한 후 생성되었기 때문에 이 버전에 액세스할 수 없다는 의미입니다. 현재 거래.
min_limit_id =<trx_id>인 경우 3가지 상황에서 논의해야 합니다</trx_id>
1)
m_ids에는 <code>읽기 보기
가 생성된 시간을 나타내는trx_id
가 포함되어 있습니다. > 데이터의creator_trx_id
와 동일하면 데이터가 자체적으로 생성되어 표시된다는 의미입니다.2)
m_ids
에trx_id
가 포함되어 있고trx_id
가creator_trx_id
와 같지 않은 경우읽기 보기가 생성되면 트랜잭션이 제출되지 않고 자체적으로 생성되지 않으므로 현재 트랜잭션이 보이지 않습니다.
3)
m_ids
에trx_id가 포함되어 있지 않으면; code>이면 <code>읽기 보기
가 생성되기 전에 거래가 제출되었으며 수정 결과는 현재 거래에서 볼 수 있음을 의미합니다.
8.3 반복 읽기 구현 원칙
🎜 데이터베이스는 잠금을 통해 격리 수준을 달성합니다. 예를 들어 다음과 같습니다. 혼자 있고 싶고 다른 사람의 방해를 받지 않으려면 집에 들어가 문에 자물쇠를 추가하면 됩니다! 직렬화 격리 수준은 잠금을 통해 구현됩니다. 그러나 자주 잠그면 성능이 저하됩니다. 그래서 데이터베이스를 설계한 삼촌은 MVCC를 생각했습니다. 🎜🎜반복 읽기 구현 원리는 MVCC 다중 버전 동시성 제어입니다. 트랜잭션 범위 내에서 두 개의 동일한 쿼리가 동일한 레코드를 읽었지만 다른 데이터를 반환합니다. 이는 반복 불가능한 읽기입니다. 반복 읽기 격리 수준은 반복 불가능 읽기 문제를 해결하는 것입니다. 🎜🎜MVCC를 기반으로 레코드를 쿼리하는 프로세스는 무엇인가요? 🎜🎜🎜🎜트랜잭션의 버전 번호, 즉 트랜잭션 ID를 가져옵니다🎜🎜🎜읽기 보기🎜🎜🎜 쿼리에서 얻은 데이터를 가져온 다음 다음에서 트랜잭션 버전 번호를 비교합니다. 읽기 보기. 🎜🎜🎜읽기 보기의 가시성 규칙이 충족되지 않으면 실행 취소 로그의 기록 스냅샷이 필요합니다.🎜🎜🎜마지막으로 규칙을 충족하는 데이터를 반환합니다🎜🎜InnoDB MVCC는읽기 보기+ 실행 취소 로그
를 통해 구현됩니다. 실행 취소 로그
는 기록 스냅샷을 저장하고 읽기 보기
에 표시됩니다. > 성적 규칙은 현재 버전의 데이터가 표시되는지 여부를 결정하는 데 도움이 됩니다. 🎜🎜반복 읽기(RR) 격리 수준은 반복 불가능 읽기 문제를 어떻게 해결합니까? 🎜🎜트랜잭션 A와 B가 있다고 가정하고, SQL 실행 과정은 다음과 같습니다🎜🎜🎜🎜반복 읽기(RR) 격리 수준에서 읽기 보기
는 트랜잭션에서 한 번만 얻을 수 있으며, 이는 복제본을 생성하여 쿼리된 각 데이터가 동일한지 확인합니다. 🎜🎜현재 core_user 테이블이 있다고 가정하고 다음과 같이 초기화 데이터를 삽입합니다. 🎜🎜🎜🎜🎜MVCC를 기반으로 실행 프로세스를 살펴보겠습니다. 🎜🎜1. A는 트랜잭션을 시작하고 먼저 트랜잭션 ID 100을 얻습니다. 🎜🎜2.B 트랜잭션을 열고 트랜잭션 ID를 101🎜🎜3. 트랜잭션 A가 읽기 뷰에 해당하는 값을 생성합니다🎜변수 | 값 |
---|---|
m_ids | 100, 101 |
max_limit_id | 102 |
min_limit _id | 100 |
creator_trx_id | 100 |
然后回到版本链:开始从版本链中挑选可见的记录:
由图可以看出,最新版本的列name的内容是孙权,该版本的trx_id值为100。开始执行read view可见性规则校验:
min_limit_id(100)=<trx_id(100)<102; creator_trx_id = trx_id =100;
由此可得,trx_id=100的这个记录,当前事务是可见的。所以查到是name为孙权的记录。
4、事务B进行修改操作,把名字改为曹操。把原数据拷贝到undo log,然后对数据进行修改,标记事务ID和上一个数据版本在undo log的地址。
5、事务B提交事务
6、事务A再次执行查询操作,因为是RR(可重复读)隔离级别,因此会复用老的Read View副本,Read View对应的值如下
변수 | 값 |
---|---|
m_ids | 100, 101 |
max_limit_id | 102 |
min_limit _id | 100 |
creator_trx_id | 100 |
然后再次回到版本链:从版本链中挑选可见的记录:
从图可得,最新版本的列name的内容是曹操,该版本的trx_id值为101。开始执行read view可见性规则校验:
min_limit_id(100)=<trx_id(101)<max_limit_id(102); 因为m_ids{100,101}包含trx_id(101), 并且creator_trx_id (100) 不等于trx_id(101)
所以,trx_id=101这个记录,对于当前事务是不可见的。这时候呢,版本链roll_pointer跳到下一个版本,trx_id=100这个记录,再次校验是否可见:
min_limit_id(100)=<trx_id(100)< max_limit_id(102); 因为m_ids{100,101}包含trx_id(100), 并且creator_trx_id (100) 等于trx_id(100)
所以,trx_id=100这个记录,对于当前事务是可见的,所以两次查询结果,都是name=孙权的那个记录。即在可重复读(RR)隔离级别下,复用老的Read View副本,解决了不可重复读的问题。
1. 查询条件包含or,可能导致索引失效
2. 如何字段类型是字符串,where时一定用引号括起来,否则索引失效
3. like通配符可能导致索引失效。
4. 联合索引,查询时的条件列不是联合索引中的第一个列,索引失效。
5. 在索引列上使用mysql的内置函数,索引失效。
6. 对索引列运算(如,+、-、*、/),索引失效。
7. 索引字段上使用(!= 或者 ,not in)时,可能会导致索引失效。
8. 索引字段上使用is null, is not null,可能导致索引失效。
9. 左连接查询或者右连接查询查询关联的字段编码格式不一样,可能导致索引失效。
10. mysql估计使用全表扫描要比使用索引快,则不使用索引。
虚拟内存,是虚拟出来的内存,它的核心思想就是确保每个程序拥有自己的地址空间,地址空间被分成多个块,每一块都有连续的地址空间。同时物理空间也分成多个块,块大小和虚拟地址空间的块大小一致,操作系统会自动将虚拟地址空间映射到物理地址空间,程序只需关注虚拟内存,请求的也是虚拟内存,真正使用却是物理内存。
现代操作系统使用虚拟内存,即虚拟地址取代物理地址,使用虚拟内存可以有2个好处:
虚拟内存空间可以远远大于物理内存空间
多个虚拟内存可以指向同一个物理地址
零拷贝实现思想,就利用了虚拟内存这个点:多个虚拟内存可以指向同一个物理地址,可以把内核空间和用户空间的虚拟地址映射到同一个物理地址,这样的话,就可以减少IO的数据拷贝次数啦,示意图如下:
排行版的实现,一般使用redis的zset数据类型。
使用格式如下:
zadd key score member [score member ...],zrank key member
层内部编码:ziplist(压缩列表)、skiplist(跳跃表)
使用场景如排行榜,社交需求(如用户点赞)
实现demo如下:
分布式锁,是控制分布式系统不同进程共同访问共享资源的一种锁的实现。秒杀下单、抢红包等等业务场景,都需要用到分布式锁,我们项目中经常使用Redis作为分布式锁。
选了Redis分布式锁的几种实现方法,大家来讨论下,看有没有啥问题哈。
命令setnx + expire分开写
setnx + value值是过期时间
set的扩展命令(set ex px nx)
set ex px nx + 校验唯一随机值,再删除
Redisson
12.1 命令setnx + expire分开写
if(jedis.setnx(key,lock_value) == 1){ //加锁 expire(key,100); //设置过期时间 try { do something //业务请求 }catch(){ } finally { jedis.del(key); //释放锁 } }
如果执行完setnx加锁,正要执行expire设置过期时间时,进程crash掉或者要重启维护了,那这个锁就“长生不老”了,别的线程永远获取不到锁啦,所以分布式锁不能这么实现。
12.2 setnx + value值是过期时间
long expires = System.currentTimeMillis() + expireTime; //系统时间+设置的过期时间 String expiresStr = String.valueOf(expires); // 如果当前锁不存在,返回加锁成功 if (jedis.setnx(key, expiresStr) == 1) { return true; } // 如果锁已经存在,获取锁的过期时间 String currentValueStr = jedis.get(key); // 如果获取到的过期时间,小于系统当前时间,表示已经过期 if (currentValueStr != null && Long.parseLong(currentValueStr) < System.currentTimeMillis()) { // 锁已过期,获取上一个锁的过期时间,并设置现在锁的过期时间(不了解redis的getSet命令的小伙伴,可以去官网看下哈) String oldValueStr = jedis.getSet(key_resource_id, expiresStr); if (oldValueStr != null && oldValueStr.equals(currentValueStr)) { // 考虑多线程并发的情况,只有一个线程的设置值和当前值相同,它才可以加锁 return true; } } //其他情况,均返回加锁失败 return false; }
笔者看过有开发小伙伴就是这么实现分布式锁的,但是这种方案也有这些缺点:
过期时间是客户端自己生成的,分布式环境下,每个客户端的时间必须同步。
没有保存持有者的唯一标识,可能被别的客户端释放/解锁。
锁过期的时候,并发多个客户端同时请求过来,都执行了jedis.getSet(),最终只能有一个客户端加锁成功,但是该客户端锁的过期时间,可能被别的客户端覆盖。
12.3 set的扩展命令(set ex px nx)(注意可能存在的问题)
if(jedis.set(key, lock_value, "NX", "EX", 100s) == 1){ //加锁 try { do something //业务处理 }catch(){ } finally { jedis.del(key); //释放锁 } }
这个方案可能存在这样的问题:
锁过期释放了,业务还没执行完。
锁被别的线程误删。
12.4 set ex px nx + 校验唯一随机值,再删除
if(jedis.set(key, uni_request_id, "NX", "EX", 100s) == 1){ //加锁 try { do something //业务处理 }catch(){ } finally { //判断是不是当前线程加的锁,是才释放 if (uni_request_id.equals(jedis.get(key))) { jedis.del(key); //释放锁 } } }
在这里,判断当前线程加的锁和释放锁是不是一个原子操作。如果调用jedis.del()释放锁的时候,可能这把锁已经不属于当前客户端,会解除他人加的锁。
一般也是用lua脚本代替。lua脚本如下:
if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end;
这种方式比较不错了,一般情况下,已经可以使用这种实现方式。但是存在锁过期释放了,业务还没执行完的问题(实际上,估算个业务处理的时间,一般没啥问题了)。
12.5 Redisson
分布式锁可能存在锁过期释放,业务没执行完的问题。有些小伙伴认为,稍微把锁过期时间设置长一些就可以啦。其实我们设想一下,是否可以给获得锁的线程,开启一个定时守护线程,每隔一段时间检查锁是否还存在,存在则对锁的过期时间延长,防止锁过期提前释放。
当前开源框架Redisson就解决了这个分布式锁问题。我们一起来看下Redisson底层原理是怎样的吧:
只要线程一加锁成功,就会启动一个watch dog
看门狗,它是一个后台线程,会每隔10秒检查一下,如果线程1还持有锁,那么就会不断的延长锁key的生存时间。因此,Redisson就是使用Redisson解决了锁过期释放,业务没执行完问题。
零拷贝就是不需要将数据从一个存储区域复制到另一个存储区域。它是指在传统IO模型中,指CPU拷贝的次数为0。它是IO的优化方案
传统IO流程
零拷贝实现之mmap+write
零拷贝实现之sendfile
零拷贝实现之带有DMA收集拷贝功能的sendfile
13.1 传统IO流程
流程图如下:
用户应用进程调用read函数,向操作系统发起IO调用,上下文从用户态转为内核态(切换1)
DMA控制器把数据从磁盘中,读取到内核缓冲区。
CPU把内核缓冲区数据,拷贝到用户应用缓冲区,上下文从内核态转为用户态(切换2),read函数返回
用户应用进程通过write函数,发起IO调用,上下文从用户态转为内核态(切换3)
CPU将应用缓冲区中的数据,拷贝到socket缓冲区
DMA控制器把数据从socket缓冲区,拷贝到网卡设备,上下文从内核态切换回用户态(切换4),write函数返回
从流程图可以看出,传统IO的读写流程,包括了4次上下文切换(4次用户态和内核态的切换),4次数据拷贝(两次CPU拷贝以及两次的DMA拷贝)。
13.2 mmap+write实现的零拷贝
mmap 的函数原型如下:
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
addr:指定映射的虚拟内存地址
length:映射的长度
prot:映射内存的保护模式
flags:指定映射的类型
fd:进行映射的文件句柄
offset:文件偏移量
mmap使用了虚拟内存,可以把内核空间和用户空间的虚拟地址映射到同一个物理地址,从而减少数据拷贝次数!
mmap+write
实现的零拷贝流程如下:
用户进程通过mmap方法向操作系统内核发起IO调用,上下文从用户态切换为内核态。
CPU利用DMA控制器,把数据从硬盘中拷贝到内核缓冲区。
上下文从内核态切换回用户态,mmap方法返回。
用户进程通过write方法向操作系统内核发起IO调用,上下文从用户态切换为内核态。
CPU将内核缓冲区的数据拷贝到的socket缓冲区。
CPU利用DMA控制器,把数据从socket缓冲区拷贝到网卡,上下文从内核态切换回用户态,write调用返回。
可以发现,mmap+write实现的零拷贝,I/O发生了4次用户空间与内核空间的上下文切换,以及3次数据拷贝。其中3次数据拷贝中,包括了2次DMA拷贝和1次CPU拷贝。
mmap是将读缓冲区的地址和用户缓冲区的地址进行映射,内核缓冲区和应用缓冲区共享,所以节省了一次CPU拷贝‘’并且用户进程内存是虚拟的,只是映射到内核的读缓冲区,可以节省一半的内存空间。
sendfile实现的零拷贝
sendfile是Linux2.1内核版本后引入的一个系统调用函数,API如下:
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
out_fd:为待写入内容的文件描述符,一个socket描述符。,
in_fd:为待读出内容的文件描述符,必须是真实的文件,不能是socket和管道。
offset:指定从读入文件的哪个位置开始读,如果为NULL,表示文件的默认起始位置。
count:指定在fdout和fdin之间传输的字节数。
sendfile表示在两个文件描述符之间传输数据,它是在操作系统内核中操作的,避免了数据从内核缓冲区和用户缓冲区之间的拷贝操作,因此可以使用它来实现零拷贝。
sendfile实现的零拷贝流程如下:
sendfile实现的零拷贝
用户进程发起sendfile系统调用,上下文(切换1)从用户态转向内核态
DMA控制器,把数据从硬盘中拷贝到内核缓冲区。
CPU将读缓冲区中数据拷贝到socket缓冲区
DMA控制器,异步把数据从socket缓冲区拷贝到网卡,
上下文(切换2)从内核态切换回用户态,sendfile调用返回。
可以发现,sendfile实现的零拷贝,I/O发生了2次用户空间与内核空间的上下文切换,以及3次数据拷贝。其中3次数据拷贝中,包括了2次DMA拷贝和1次CPU拷贝。那能不能把CPU拷贝的次数减少到0次呢?有的,即带有DMA收集拷贝功能的sendfile
!
sendfile+DMA scatter/gather实现的零拷贝
linux 2.4版本之后,对sendfile做了优化升级,引入SG-DMA技术,其实就是对DMA拷贝加入了scatter/gather操作,它可以直接从内核空间缓冲区中将数据读取到网卡。使用这个特点搞零拷贝,即还可以多省去一次CPU拷贝。
sendfile+DMA scatter/gather实现的零拷贝流程如下:
用户进程发起sendfile系统调用,上下文(切换1)从用户态转向内核态
DMA控制器,把数据从硬盘中拷贝到内核缓冲区。
CPU把内核缓冲区中的文件描述符信息(包括内核缓冲区的内存地址和偏移量)发送到socket缓冲区
DMA控制器根据文件描述符信息,直接把数据从内核缓冲区拷贝到网卡
上下文(切换2)从内核态切换回用户态,sendfile调用返回。
可以发现,sendfile+DMA scatter/gather
实现的零拷贝,I/O发生了2次用户空间与内核空间的上下文切换,以及2次数据拷贝。其中2次数据拷贝都是包DMA拷贝。这就是真正的 零拷贝(Zero-copy) 技术,全程都没有通过CPU来搬运数据,所有的数据都是通过DMA来进行传输的。
synchronized是Java中的关键字,是一种同步锁。synchronized关键字可以作用于方法或者代码块。
一般面试时。可以这么回答:
反编译后,monitorenter、monitorexit、ACC_SYNCHRONIZED
monitor监视器
Java Monitor 的工作机理
对象与monitor关联
14.1 monitorenter、monitorexit、ACC_SYNCHRONIZED
如果synchronized作用于代码块,反编译可以看到两个指令:monitorenter、monitorexit,JVM使用monitorenter和monitorexit两个指令实现同步;如果作用synchronized作用于方法,反编译可以看到ACCSYNCHRONIZED标记,JVM通过在方法访问标识符(flags)中加入ACCSYNCHRONIZED来实现同步功能。
同步代码块是通过monitorenter和monitorexit来实现,当线程执行到monitorenter的时候要先获得monitor锁,才能执行后面的方法。当线程执行到monitorexit的时候则要释放锁。
同步方法是通过中设置ACCSYNCHRONIZED标志来实现,当线程执行有ACCSYNCHRONIZED标志的方法,需要获得monitor锁。每个对象都与一个monitor相关联,线程可以占有或者释放monitor。
14.2 monitor监视器
monitor是什么呢?操作系统的管程(monitors)是概念原理,ObjectMonitor是它的原理实现。
在Java虚拟机(HotSpot)中,Monitor(管程)是由ObjectMonitor实现的,其主要数据结构如下:
ObjectMonitor() { _header = NULL; _count = 0; // 记录个数 _waiters = 0, _recursions = 0; _object = NULL; _owner = NULL; _WaitSet = NULL; // 处于wait状态的线程,会被加入到_WaitSet _WaitSetLock = 0 ; _Responsible = NULL ; _succ = NULL ; _cxq = NULL ; FreeNext = NULL ; _EntryList = NULL ; // 处于等待锁block状态的线程,会被加入到该列表 _SpinFreq = 0 ; _SpinClock = 0 ; OwnerIsThread = 0 ; }
ObjectMonitor中几个关键字段的含义如图所示:
14.3 Java Monitor 的工作机理
想要获取monitor的线程,首先会进入_EntryList队列。
当某个线程获取到对象的monitor后,进入Owner区域,设置为当前线程,同时计数器count加1。
如果线程调用了wait()方法,则会进入WaitSet队列。它会释放monitor锁,即将owner赋值为null,count自减1,进入WaitSet队列阻塞等待。
如果其他线程调用 notify() / notifyAll() ,会唤醒WaitSet中的某个线程,该线程再次尝试获取monitor锁,成功即进入Owner区域。
同步方法执行完毕了,线程退出临界区,会将monitor的owner设为null,并释放监视锁。
14.4 对象与monitor关联
在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头(Header),实例数据(Instance Data)和对象填充(Padding)。
对象头主要包括两部分数据:Mark Word(标记字段)、Class Pointer(类型指针)。
Mark Word 是用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等。
重量级锁,指向互斥量的指针。其实synchronized是重量级锁,也就是说Synchronized的对象锁,Mark Word锁标识位为10,其中指针指向的是Monitor对象的起始地址。
分布式id生成方案主要有:
UUID
数据库自增ID
基于雪花算法(Snowflake)实现
百度 (Uidgenerator)
美团(Leaf)
什么是雪花算法?
雪花算法是一种生成分布式全局唯一ID的算法,生成的ID称为Snowflake IDs。这种算法由Twitter创建,并用于推文的ID。
一个Snowflake ID有64位。
第1位:Java中long的最高位是符号位代表正负,正数是0,负数是1,一般生成ID都为正数,所以默认为0。
接下来前41位是时间戳,表示了自选定的时期以来的毫秒数。
接下来的10位代表计算机ID,防止冲突。
其余12位代表每台机器上生成ID的序列号,这允许在同一毫秒内创建多个Snowflake ID。
雪花算法
最后PHP中文网祝大家找到一份满意的工作!!!
【面试题专题】
프런트엔드: [프런트엔드 인터뷰 질문][js 인터뷰 질문][ vue 인터뷰 질문][ajax 인터뷰 질문][react 인터뷰 질문]
데이터베이스: [mysql 인터뷰 질문][ redis 인터뷰 질문][oracle 인터뷰 질문]
백엔드: [PHP 인터뷰 질문][thinkphp 인터뷰 질문][python 인터뷰 질문][java 인터뷰 질문][ 안드로이드 인터뷰 질문】