마이바티스+레디스 사용

목차

1. Mybatis 캐싱 메커니즘 Mybatis는 1단계 및 2단계 캐시를 제공합니다.

2. 캐시를 사용하지 않음

3. 두 번째 수준 캐시를 켭니다.

4. 분산 캐시

1. Redis 캐시 사용자 정의

2. 맞춤형 RedisCache를 사용하세요

3. RedisCache 개선

4. 테스트

5. 발생하는 문제

6. 최적화

6. 면접질문

캐시 침투

캐시 눈사태

캐시 분석


1. Mybatis 캐싱 메커니즘 Mybatis는 1단계 및 2단계 캐시를 제공합니다.

First-level 캐시 : 로컬 캐시 또는 sqlSession 레벨 캐시라고도 불리는 스레드 레벨 캐시로, 기본적으로 1차 캐시가 존재하며, 동일한 세션에서 동일한 작업이 두 번 쿼리되면 캐시에서 가져옵니다.
두 번째 수준 캐시 : 전역 범위 캐시, 현재 sqlSession 외에 다른 캐시도 사용할 수 있습니다. 두 번째 수준 캐시도 기본적으로 활성화되어 있습니다. 이를 달성하려면 매퍼 파일에 <cache/>만 작성하면 됩니다. 두 번째 수준 캐시를 구현하려면 직렬화 인터페이스를 구현하기 위해 pojo가 필요합니다. 그렇지 않으면 오류가 발생합니다. .

2. 캐시를 사용하지 않음


먼저 캐싱 없이 시도해 보겠습니다.

查询sql的代码如下,我这里使用的service会调用mapper实现一个分页查询:

List<UserListVO> s1 = service.getUserListVOByPage(2, 5);
List<UserListVO> s2 = service.getUserListVOByPage(2, 5);
List<UserListVO> s3 = service.getUserListVOByPage(1, 3);
大致结果如下:

==>  Preparing: select u.id,u.username,u.password,u.name,u.gender,u.role,u.balance,u.telephone from user u where status=0 limit ?,?
==> Parameters: 5(Integer), 5(Integer)
<==    Columns: id, username, password, name, gender, role, balance, telephone
<==        Row: 6, admin4, admin4, 33, 1, 2, 1000, 18586747625
<==        Row: 7, admin7, admin5, 44, 1, 2, 1000, 18749888293
<==        Row: 8, admin8, admin5, 44, 1, 2, 1000, 17488660630
<==        Row: 9, admin9, admin5, 44, 1, 2, 1000, 17173666055
<==        Row: 10, admin10, admin5, 44, 1, 2, 1000, 16749049960
<==      Total: 5
 
==>  Preparing: select u.id,u.username,u.password,u.name,u.gender,u.role,u.balance,u.telephone from user u where status=0 limit ?,?
==> Parameters: 5(Integer), 5(Integer)
<==    Columns: id, username, password, name, gender, role, balance, telephone
<==        Row: 6, admin4, admin4, 33, 1, 2, 1000, 18586747625
<==        Row: 7, admin7, admin5, 44, 1, 2, 1000, 18749888293
<==        Row: 8, admin8, admin5, 44, 1, 2, 1000, 17488660630
<==        Row: 9, admin9, admin5, 44, 1, 2, 1000, 17173666055
<==        Row: 10, admin10, admin5, 44, 1, 2, 1000, 16749049960
<==      Total: 5
 
==>  Preparing: select u.id,u.username,u.password,u.name,u.gender,u.role,u.balance,u.telephone from user u where status=0 limit ?,?
==> Parameters: 0(Integer), 3(Integer)
<==    Columns: id, username, password, name, gender, role, balance, telephone
<==        Row: 1, root, root, 李显超, 0, 1, 98312, 17674574650
<==        Row: 2, admin, admin, 舒芬, 1, 2, 1189, 18012113193
<==        Row: 3, admin1, admin1, lxc, 1, 1, 1000, 11503995061
<==      Total: 3

보시다시피 jdbc 쿼리 작업은 세 번 실행되었습니다. 처음 두 쿼리는 정확히 동일했지만 데이터베이스는 여전히 여러 번 쿼리되었습니다. 이때 궁금하실 수도 있습니다. 1단계 캐시가 없는지요? ?

여기서는 서비스를 사용하고 있기 때문에 각 서비스 호출은 새로운 데이터베이스 세션을 다시 생성하므로 서비스 메서드 호출이 완료되면 트랜잭션이 자동으로 제출되고 세션이 닫힙니다. 이 경우 첫 번째 수준 캐시는 예, 두 번째 수준 캐시만 사용하여 데이터베이스에 대한 부담을 줄일 수 있습니다.

3. 두 번째 수준 캐시를 켭니다.

구체적인 단계:

1. mybatis 구성 파일에 표시된 대로 두 번째 수준 캐시를 활성화하는 것이 가장 좋습니다.

2. 매퍼 파일에 2차 캐시를 사용하기 위한 플래그를 추가합니다. 

3. 쿼리할 pojo 클래스가 직렬화 인터페이스를 구현하도록 합니다.

이때 다시 쿼리하면 결과는 다음과 같다.

Creating a new SqlSession
Cache Hit Ratio [com.lxc.sales.mapper.UserMapper]: 0.0
JDBC Connection [com.alibaba.druid.proxy.jdbc.ConnectionProxyImpl@32fd5bc] will be managed by Spring
==>  Preparing: select u.id,u.username,u.password,u.name,u.gender,u.role,u.balance,u.telephone from user u where status=0 limit ?,?
==> Parameters: 5(Integer), 5(Integer)
<==    Columns: id, username, password, name, gender, role, balance, telephone
<==        Row: 6, admin4, admin4, 33, 1, 2, 1000, 18586747625
<==        Row: 7, admin7, admin5, 44, 1, 2, 1000, 18749888293
<==        Row: 8, admin8, admin5, 44, 1, 2, 1000, 17488660630
<==        Row: 9, admin9, admin5, 44, 1, 2, 1000, 17173666055
<==        Row: 10, admin10, admin5, 44, 1, 2, 1000, 16749049960
<==      Total: 5
Releasing transactional SqlSession 
Creating a new SqlSession
Registering transaction synchronization for SqlSession 
Cache Hit Ratio [com.lxc.sales.mapper.UserMapper]: 0.5
Releasing transactional SqlSession 
Creating a new SqlSession
 
Cache Hit Ratio [com.lxc.sales.mapper.UserMapper]: 0.3333333333333333
JDBC Connection [com.alibaba.druid.proxy.jdbc.ConnectionProxyImpl@32fd5bc] will be managed by Spring
==>  Preparing: select u.id,u.username,u.password,u.name,u.gender,u.role,u.balance,u.telephone from user u where status=0 limit ?,?
==> Parameters: 0(Integer), 3(Integer)
<==    Columns: id, username, password, name, gender, role, balance, telephone
<==        Row: 1, root, root, 李显超, 0, 1, 98312, 17674574650
<==        Row: 2, admin, admin, 舒芬, 1, 2, 1189, 18012113193
<==        Row: 3, admin1, admin1, lxc, 1, 1, 1000, 11503995061
<==      Total: 3

처음 두 개의 동일한 쿼리는 한 번만 쿼리되었으며 두 번째 쿼리는 2차 캐시에서 직접 데이터를 가져오는 것임을 알 수 있습니다. 간단한 로컬 캐싱도 구현됩니다.

다음은 분산 캐시 구현입니다.

4. 분산 캐시


위에서 언급했듯이 2차 캐시를 구현하기 위해서는 매퍼에 <cache/> 태그를 작성해야 하는데, 이 태그는 실제로 mybatis에서 제공하는 Cache 인터페이스에 해당합니다. 두 번째 수준 캐시:

 PerpetualCache 클래스는 기본적으로 사용됩니다.

 SQL 문을 키로, 데이터를 값으로 해시 테이블에 저장하는 것이 기본 상황입니다. 이제 Redis 캐시를 구현하려고 하는데 캐시된 데이터는 서버 애플리케이션에 배치되지 않으므로 Cache 인터페이스를 구현하는 캐시 클래스를 작성하고, 캐시된 콘텐츠를 Redis 서버에 저장하고, Redis 서버에서 캐시를 가져옵니다.

1. Redis 캐시 사용자 정의


먼저 Cache 인터페이스를 구현하고 여러 메소드를 구현하기 위해 새로운 RedisCache 클래스를 생성합니다. 전체 프레임워크는 다음과 같습니다.

public class RedisCache implements Cache {
    @Override
    public String getId() {
        return null;
    }
 
    @Override
    public void putObject(Object o, Object o1) {
    }
 
    @Override
    public Object getObject(Object o) {
        return null;
    }
 
    @Override
    public Object removeObject(Object o) {
        return null;
    }
 
    @Override
    public void clear() {
    }
 
    @Override
    public int getSize() {
        return 0;
    }
}


 Cache를 구현한 클래스를 사용하는 경우 생성자에 String 유형의 ID를 제공해야 하며, 위의 PerpetualCache를 참고할 수 있습니다.

getId(): 생성자가 전달한 캐시의 고유 ID를 반환합니다. putObject()
: 캐시에 데이터를 넣습니다.
getObject(): 캐시에서 데이터를 제거합니다.
RemoveObject(): 캐시를 제거합니다
.clear(): 모든 캐시를 지웁니다.
getSize( ): 캐시 개수를 가져옵니다.


2. 맞춤형 RedisCache를 사용하세요


캐시 태그의 기본값을 새 클래스로 변경합니다.

 앞으로는 이 매퍼의 두 번째 수준 캐시가 Redis에 저장되거나 검색될 것입니다.

3. RedisCache 개선


초기화 코드는 다음과 같습니다.

이 클래스의 객체는 mybatis에서 생성해야 하며, mybatis는 각 매퍼 파일마다 객체를 생성하며 각 객체의 ID는 다릅니다.

private final String id;
 
public RedisCache(String id) {
    this.id = id;
}
 
@Override
public String getId() {
    return id;
}


이 시점에서 RedisCache를 시작할 수는 있지만 캐시된 데이터에 접근할 수 없다. 남은 메소드를 구현해야 하는데 문제가 있다. 우리가 캐시를 설치한 컨테이너가 Redis를 필요로 할 때 Redis를 동작시키기 위해서는 단지 우리가 해야 할 일은 다음과 같다. 이전 기사에서 언급한 springboot와 통합된 redis를 사용할 수 있습니다. 그러나 RedisCache는 IOC 컨테이너에 없으며 RedisTemplate에 직접 주입될 수 없습니다.먼저 IOC 컨테이너를 얻기 위해 도구 클래스를 사용자 정의하고 이를 사용하여 IOC 컨테이너에서 redisTemplate을 얻습니다.

@Component
//需要继承ApplicationContextAware
public class ApplicationContextUtils implements ApplicationContextAware {
    //获取到ioc容器
    private static ApplicationContext context;
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        context = applicationContext;
    }
    //获取ioc容器
    public static ApplicationContext getContext(){
        return context;
    }
    //直接获取bean对象
    public static Object getBean(String bean){
        return context.getBean(bean);
    }
}

이제 RedisCache의 IOC 컨테이너에 있는 객체를 가져올 수 있습니다.

그런 다음 모든 메소드를 구현하면 코드는 다음과 같습니다.

public class RedisCache implements Cache {
    private final String id;
 
    public RedisCache(String id) {
        this.id = id;
    }
 
    @Override
    public String getId() {
        return id;
    }
 
    @Override
    public void putObject(Object o, Object o1) {
        RedisTemplate<Object, Object> redis = (RedisTemplate<Object, Object>) ApplicationContextUtils.getBean("objectRedisTemplate");
        redis.opsForHash().put(id, o, o1);
    }
 
    @Override
    public Object getObject(Object o) {
        RedisTemplate<Object, Object> redis = (RedisTemplate<Object, Object>) ApplicationContextUtils.getBean("objectRedisTemplate");
        return redis.opsForHash().get(id, o);
    }
 
    @Override
    public Object removeObject(Object o) {
        RedisTemplate<Object, Object> redis = (RedisTemplate<Object, Object>) ApplicationContextUtils.getBean("objectRedisTemplate");
        return redis.opsForHash().delete(id, o);
    }
 
    @Override
    public void clear() {
        RedisTemplate<Object, Object> redis = (RedisTemplate<Object, Object>) ApplicationContextUtils.getBean("objectRedisTemplate");
        redis.delete(id);
    }
 
    @Override
    public int getSize() {
        RedisTemplate<Object, Object> redis = (RedisTemplate<Object, Object>) ApplicationContextUtils.getBean("objectRedisTemplate");
        return redis.opsForHash().size(id).intValue();
    }
}


mybatis 생성 시 제공된 ID를 redis의 키로 사용하여 해시맵 데이터 구조를 생성하는데, 해시맵의 키는 쿼리 SQL 문이고 값은 해당 데이터입니다.

한 가지 더 설명하자면, 코드의 objectRedisTemplate은 제가 만든 redisTemplate이며, 편의상 모든 직렬화 방식을 jackson 형식으로 변경했습니다. 

4. 테스트


코드 테스트를 시작하기 전에:

 결과는 다음과 같습니다.

Cache Hit Ratio [com.lxc.sales.mapper.UserMapper]: 0.0
==>  Preparing: select u.id,u.username,u.password,u.name,u.gender,u.role,u.balance,u.telephone from user u where status=0 limit ?,?
==> Parameters: 5(Integer), 5(Integer)
<==    Columns: id, username, password, name, gender, role, balance, telephone
<==        Row: 6, admin4, admin4, 33, 1, 2, 1000, 18586747625
<==        Row: 7, admin7, admin5, 44, 1, 2, 1000, 18749888293
<==        Row: 8, admin8, admin5, 44, 1, 2, 1000, 17488660630
<==        Row: 9, admin9, admin5, 44, 1, 2, 1000, 17173666055
<==        Row: 10, admin10, admin5, 44, 1, 2, 1000, 16749049960
<==      Total: 5
 
 
Cache Hit Ratio [com.lxc.sales.mapper.UserMapper]: 0.5
 
 
Cache Hit Ratio [com.lxc.sales.mapper.UserMapper]: 0.3333333333333333
==>  Preparing: select u.id,u.username,u.password,u.name,u.gender,u.role,u.balance,u.telephone from user u where status=0 limit ?,?
==> Parameters: 0(Integer), 3(Integer)
<==    Columns: id, username, password, name, gender, role, balance, telephone
<==        Row: 1, root, root, 李显超, 0, 1, 98312, 17674574650
<==        Row: 2, admin, admin, 舒芬, 1, 2, 1189, 18012113193
<==        Row: 3, admin1, admin1, lxc, 1, 1, 1000, 11503995061
<==      Total: 3


2차 캐시를 사용하고 반복 검색은 한 번만 수행되는 것을 볼 수 있으며, 두 번째 캐시에서 얻은 데이터는 다음과 같습니다.

2개에 해당하는 키-값 쌍이 정확히 2개 있어 문제가 없습니다. 이 두 쿼리의 값은 이미 Redis 서버로 전송되었습니다. 테스트 코드를 다시 시작하면 더 간단해집니다. Redis 서버에서 직접 모두 가져올 수 있으며 데이터베이스 쿼리가 필요하지 않습니다.

5. 발생하는 문제


또한 당사가 추가, 삭제, 수정 작업을 수행하는 경우

 redis에서 키에 해당하는 모든 데이터를 삭제하기 위해 Clear 메소드가 호출됩니다. 즉, 매퍼 파일을 단위로 사용하고 id를 마크로 사용합니다. 다른 테이블에는 영향을 주지 않습니다.

여러 테이블에 관련 쿼리가 없으면 문제가 없으나 관련 쿼리가 있는 경우 테이블 중 하나가 수정되어 해당 테이블의 캐시가 모두 삭제되면 다른 관련 테이블은 수정되지 않습니다. 캐시되면 쿼리된 데이터가 데이터베이스와 일치하지 않게 됩니다.

이때 관련 테이블 2개 중 하나를 추가, 삭제, 수정하면 해당 테이블 자체의 캐시만 삭제하는 것이 아니라 모든 관련 테이블의 캐시를 삭제하도록 몇 가지 수정이 필요합니다. 따라서 각 테이블에 캐시 태그를 설정할 수 없으며, 다음과 같이 두 개의 관련 테이블이 RedisCache 객체를 공유하도록 합니다.

 캐시 태그는 하나만 필요하며, 이 경우 두 매퍼의 캐시가 함께 있게 되며, 이를 삭제하면 모두 삭제됩니다.

6. 최적화


이 단계는 주로 캐시된 키-값 쌍에 대한 최적화 조치입니다. 위 그림에서 볼 수 있듯이 키는 SQL 문과 기타 정보에 해당합니다. 매우 길어보이므로 Redis 성능에 영향을 미칩니다. 최대한 간결하게 디자인하세요.

우리의 목표는 키를 더 짧게 만드는 것이며 고유해야 하며 충돌할 수 없어야 합니다. 이 기능은 암호화 알고리즘 템플릿의 메시지 다이제스트 기술을 사용하여 긴 데이터를 고정 길이 데이터로 변환할 수 있으며 유일한 차이점은 다음과 같습니다.

가장 일반적으로 사용되는 메시지 다이제스트는 MD5입니다. MD5로 키를 암호화한 다음 이를 Redis 서버에 저장합니다.

다음과 같은 형식으로 작성됩니다.

 Redis를 다시 지우고 쿼리를 실행하면 결과는 다음과 같습니다.

 키 이름 길이가 대폭 줄어들고 검색 속도도 빨라졌습니다.

이에 따라 캐시에 대한 시간 제한도 설정할 수 있습니다.

6. 면접질문


캐시 침투


캐시 침투는 클라이언트가 요청한 데이터가 캐시나 데이터베이스에 존재하지 않음을 의미합니다 (예: ID가 -1인 데이터 검색). 이러한 방식으로 캐시는 적용되지 않습니다. 이러한 악의적인 요청은 데이터베이스로 인해 데이터베이스에 엄청난 압박이 가해지고 있습니다. .

일반적으로 사용되는 두 가지 솔루션이 있습니다.

빈 개체 캐시 : 비어 있는 것으로 확인된 데이터도 캐시에 캐시되며, 다음에 동일한 양식을 검색할 때 캐시에서 빈 데이터를 가져옵니다.
장점: 구현이 간단하고 유지관리가 용이
​​단점: 1. 추가 메모리 소모(임의 값 검색이 악의적으로 사용될 수 있음), 해결 방법은 타임아웃을 설정하는 것 2. 단기 불일치가 발생할 수 있음
블룸 필터링 : 블룸 필터를 먼저 액세스할 때 요청 확인하려는 데이터가 존재하면 해제되고, 존재하지 않으면 거부됩니다.
장점: 메모리 사용량이 적고, 중복 키가 없음 
단점: 오판 가능성, 구현이 복잡함
실시간 모니터링: Redis의 적중률이 낮아진 것을 발견하면 메모리로 확인함


또한, ID의 복잡성을 높이는 등의 적극적인 솔루션을 활용하여 ID의 규칙에 의한 조작을 방지하고 기본적인 검증을 수행할 수 있습니다.

캐시 눈사태


캐시 사태는 특정 기간 동안 많은 수의 캐시가 동시에 실패하거나 Redis 서버가 다운되어 데이터베이스에 많은 요청이 도달하여 큰 압박을 받는 것을 의미합니다 .

해결책:

캐시별로 서로 다른 만료 시간 추가 (일반적으로 사용되는 데이터는 더 긴 TTL, 인기가 없는 데이터는 더 짧은 TTL) Rediis 클러스터를
사용하여 서비스 가용성 향상 및 다운타임 방지 캐시 비즈니스에 다운그레이드 전류 제한 정책 추가 다단계 캐시 추가 비즈니스, nginx.캐시 + redis 캐시 + 기타 캐시


캐시 분석


캐시 고장 문제는 핫키 문제 라고도 불리며 , 동시 접속률이 높고 캐시 재구성 업무가 복잡한 키가 갑자기 실패하는 것을 의미하며 , 수많은 접속 요청이 순식간에 데이터베이스에 큰 영향을 미치게 된다 .

Avalanche와의 차이점은 많은 수의 키가 만료되지 않았다는 점입니다 Redis는 여전히 정상 상태이지만 데이터베이스가 붕괴되었습니다.

일반적인 솔루션:

뮤텍스 잠금(Mutex lock) : 동시에 하나의 스레드만 데이터베이스 쿼리를 허용하고 나머지 스레드는 캐시에서 가져오기를 기다리고 있으며, 구현이 간단하고 일관성이 강하지만 성능이 떨어지고 가용성이 떨어집니다.
논리적 만료 : 데이터를 영구적으로 저장하지만 데이터에 추가 논리적 만료 시간을 저장합니다. 데이터를 검색할 때 데이터가 만료된 것으로 감지되면 데이터는 계속 반환되지만 데이터베이스의 새 데이터를 캐시로 동기화하기 위해 새 스레드가 열립니다. 최종 일관성은 구현하기가 복잡합니다.
사전 설정된 인기 데이터 : Redis 피크 액세스 이전에 인기 데이터를 미리 Redis에 저장하고 인기 데이터의 TTL을 늘립니다.
실시간 조정: 인기 데이터를 모니터링하고 키의 TTL을 조정합니다.

추천

출처blog.csdn.net/qq_40453972/article/details/126531235