快取架構中的服務詳解!SpringBoot中二級快取服務的實現

攻城獅Chova發表於2021-05-22

建立快取服務

建立快取服務介面專案

  • 建立myshop-service-redis-api專案,該專案只負責定義介面
  • 建立專案的pom.xml:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.oxford</groupId>
        <artifactId>myshop-dependencies</artifactId>
        <version>1.0.0-SNAPSHOT</version>
        <relativePath>../myshop-dependencies/pom.xml</relativePath>
    </parent>

    <artifactId>myshop-service-redis-api</artifactId>
    <packaging>jar</packaging>
</project>
  • 定義資料Redis介面RedisService:
package com.oxford.myshop.service.redis.api

public interface RedisService{
	void set(String key,Object value);
	
	void set(String key,Object value,int seconds);

	void del(String key);

	Object get(String key);
}

建立快取服務提供者專案

  • 建立myshop-service-redis-provider專案,該專案用作快取服務提供者
  • 建立專案的pom.xml:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.oxford</groupId>
        <artifactId>myshop-dependencies</artifactId>
        <version>1.0.0-SNAPSHOT</version>
        <relativePath>../myshop-dependencies/pom.xml</relativePath>
    </parent>

    <artifactId>myshop-service-redis-api</artifactId>
    <packaging>jar</packaging>

	<dependencies>
		<!-- Spring Boot Starter Settings-->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-redis</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>


		<!--Common Setting-->
		<dependency>
			<groupId>org.apache.commons</groupId>
			<artifactId>commons-pool2</artifactId>
		</dependency>
		<dependency>
			<groupId>de.javakaffee</groupId>
			<artifactId>kryo-serializers</artifactId>
		</dependency>

		<!--Project Settings-->
		<dependency>
			<groupId>com.oxford</groupId>
			<artifactId>my-shop-commons-dubbo</artifactId>
			<version>${Project.parent.version}</version>
		</dependency>
		<dependency>
			<groupId>com.oxford</groupId>
			<artifactId>my-shop-service-redis-api</artifactId>
			<version>${Project.parent.version}</version>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
				<configuration>
					<mainClass>com.oxford.myshop.service.redis.provider.MyshopServiceRedisProviderApplication</mainClass>
				</configuration>
			</plugin>
		</plugins>
	</build>
</project>

Redis底層實現的Java的lettuce客戶端

  • 建立快取服務介面實現類RedisServiceImpl
package com.oxford.myshop.service.redis.provider.api.impl;

@Service(version="${service.versions.redis.v1}")
public class RedisServiceImpl implements RedisService{
	
	@Override
	public void set(String key,Object value){
		redisTemplate.opsForValue().set(key,value);
	}

	@Override
	public void set(String key,Object value,int seconds){
		redisTemplate.opsForValue().set(key,value,seconds,TimeUnit.SECONDS);
	}

	@Override
	public void del(String key){
		redisTemplate.delete(key);
	}

	@Override
	public Object get(String key){
		return redisTemplate.opsForValue().get(key);
	}
}
  • 建立啟動類SpringBootApplication
package com.oxford.myshop.service.redis.provider;

import com.alibaba.dubbo.container.Main;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.hystrix.EnableHystrix;
import org.springframework.cloud.netflix.hystrix.dashboard.EnableHystrixDashboard;



@EnableHystrix
@EnableHystrixDashboard
public class MyShopServiceRedisrProviderApplication {
    public static void main(String[]args) {
        SpringApplication.run(MyShopServiceRedisProviderApplication.class,args);
        Main.main(args);
    }
}
  • 建立配置檔案application.yml
spring:
  application:
    name: myshop-service-redis-provider
  redis:
  	lettuce:
      pool:
      	max-active: 8
      	max-idle: 8
      	max-wait: -1ms
      	min-idle: 0
    sentinel:
      master: mymaster
      nodes: 192.168.32.255:26379,192.168.32.255:26380,192.168.32.255:26381
server:
  port: 8503

services:
  version:
    redis:
  	  v1: 1.0.0
    user:
      v1: 1.0.0

dubbo:
  scan:
    basePackages: com.oxford.myshop.service.redis.provider.api.impl
  application:
    id: com.oxford.myshop.service.redis.provider.api
    name: com.oxford.myshop.service.redis.provider.api
    qos-port: 22224
    qos-enable: true
  protocal:
    id: dubbo
    name: dubbo
    port: 20883
    status: server
    serialization: kryo

  regitry:
    id: zookeeper
    address: zookeeper://localhost:2181?backup=192.168.32.255:2182,192.168.32.255:2183

management:
  endpoint:
    dubbo:
      enabled: true
    dubbo-shutdown:
      enabled: true
    dubbo-configs:
      enabled: true
    dubbo-sevicies:
      enabled: true
    dubbo-reference:
      enabled: true
    dubbo-properties:
      enabled: true
  health:
    dubbo:
      status:
        defaults: memory
        extras: load,threadpool

建立快取服務消費者專案

  • 在pom檔案中引入redis介面依賴
  • 在快取服務消費者專案的ServiceImpl中呼叫RedisService
@Reference(version="services.versions.redis.v1")
private RedisService redisService;

MyBatis Redis二級快取

MyBatis快取

  • 一級快取:
    • MyBatis會在表示會話的SqlSession物件中建立一個簡單的快取: 將每次查詢到的結果快取起來,當下次查詢的時候,如果判斷先前有個完全一樣的查詢,會直接從快取中直接將結果取出,返回給使用者,不需要再進行一次資料庫查詢
    • 一級快取是SqlSession級別的快取:
      • 在運算元據庫時需要構造SqlSession物件
      • 物件中有一個(記憶體區域)資料結構(HashMap)用於儲存快取資料
      • 不同的SqlSession之間的快取資料區域(HashMap)互不影響,
      • 一級快取的作用域是同一個SqlSession
      • 在同一個SqlSession中兩次執行相同的SQL語句: 第一次執行完畢會將資料庫中查詢的資料寫到快取(記憶體),第二次會從快取中獲取資料,將不再從資料庫查詢,從而提高查詢效率
      • 當一個SqlSession結束後該SqlSession中的一級快取就不存在了
      • MyBatis預設開啟一級快取
  • 二級快取:
    • 二級快取是Mapper級別的快取: 多個SqlSession去操作同一個Mapper的SQL語句,多個SqlSession去運算元據庫得到資料會存在二級快取區域,多個SqlSession可以共用二級快取,二級快取是跨SqlSession的
    • 二級快取的作用域是mapper的同一個namespace
    • 不同的SqlSession兩次執行相同namespace下的SQL語句且向SQL中傳遞引數也相同即最終執行相同的SQL語句: 第一次執行完畢會將資料庫中查詢的資料寫到快取(記憶體),第二次會從快取中獲取資料將不再從資料庫查詢,從而提高查詢效率
    • MyBatis預設沒有開啟二級快取,需要在setting全域性引數中配置開啟二級快取

配置MyBatis二級快取

SpringBoot中開啟MyBatis二級快取
  • 在myshop-service-user-provider的配置檔案中開啟MyBatis二級快取
spring:
  application:
    name: myshop-service-user-provider
  datasource:
    druid:
      url: jdbc:mysql://localhost:3306/myshop?useUnicode=true&characterEncoding=utf-8&useSSL=false
      username: root
      password: 123456
      initial-size: 1
      min-idle: 1
      main-active: 20
      test-on-borrow: true
      driver-class-name: com.mysql.cj.jdbc.Driver
  redis:
  	lettuce:
      pool:
      	max-active: 8
      	max-idle: 8
      	max-wait: -1ms
      	min-idle: 0
    sentinel:
      master: mymaster
      nodes: 192.168.32.255:26379,192.168.32.255:26380,192.168.32.255:26381

server:
  port: 8501

# MyBatis Config properties
mybatis:
  configuration:
  	cache-enabled: true
  type-aliases-package: com.oxford.myshop.commons.domain
  mapper-location: classpath:mapper/*.xml

services:
  version:
  	redis:
  	  v1: 1.0.0
    user:
      v1: 1.0.0

dubbo:
  scan:
    basePackages: com.oxford.myshop.service.user.provider.api.impl
  application:
    id: com.oxford.myshop.service.user.provider.api
    name: com.oxford.myshop.service.user.provider.api
    qos-port: 22222
    qos-enable: true
  protocal:
    id: dubbo
    name: dubbo
    port: 20001
    status: server
    serialization: kryo

  regitry:
    id: zookeeper
    address: zookeeper://localhost:2181?backup=192.168.32.255:2182,192.168.32.255:2183

management:
  endpoint:
    dubbo:
      enabled: true
    dubbo-shutdown:
      enabled: true
    dubbo-configs:
      enabled: true
    dubbo-sevicies:
      enabled: true
    dubbo-reference:
      enabled: true
    dubbo-properties:
      enabled: true
  health:
    dubbo:
      status:
        defaults: memory
        extras: load,threadpool
  • 在myshop-commons-mapper的pom.xml中增加redis依賴:
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-data-redis</artifacted>
</dependency>

<dependency>
	<groupId>org.apache.commons</groupId>
	<artifactId>commons-pool2</artifacted>
</dependency>
實體類實現序列化介面並宣告序列號
private static final long serialVersionUID = 82897704415244673535L
IDEA生成序列號方法:
- 使用GenerateSerialVersionUID外掛生成,安裝完外掛後在實現了序列化介面的類中
- 使用快捷鍵Alt+Insert即可撥出生成選單,即可自動生成序列號
實現Mybatis Cache介面,自定義快取為Redis
  • 在myshop-commons專案中建立ApplicationContextHolder類
package com.oxford.myshop.commons.context;

@Component
public class ApplicationContextHolder implements ApplicationContextAware,DisposableBean{

	private static final Logger logger=LoggerFactory.getLogger(ApplicationContext.class);

	private static ApplicationContext applicationContext;

	/**
	 * 獲取儲存在靜態變數中的ApplicationContext
	 */
	 public static ApplicationContext getApplicationContext(){
	 	assertContextInjected();
	 	return applicationContext;
	 }

	/**
	 * 從靜態變數applicationContext中獲取Bean,自動轉型成所賦值物件的型別
	 */
	 public static <T> T getBean(String name){
	 	assertContextInjected();
	 	return (T) applicationContext.getBean(name);
	 }

	/**
	 * 從靜態變數applicationContext中獲取Bean,自動轉型成所賦值物件的型別
	 */
	public static <T> T getBean(Class<T> clazz){
	 	assertContextInjected();
	 	return (T) applicationContext.getBean(clazz);
	 }

	/**
	 * 實現DisposableBean介面,在Context關閉時清理靜態變數
	 */
	 public void destroy() throws Exception{
	 	logger.debug("清除 SpringContext 中的 ApplicationContext: {}",applicationContext);
	 	applicationContext=null;
	 }

	/**
	 * 實現ApplicationContextAware介面,注入Context到靜態變數中
	 */
	 public void setApplicationContext(ApplicationContext applicationContext) throws BeanException{
	 	ApplicationContext.applicationContext=applicationContext;
	 }

	/**
	 * 斷言Context已經注入
	 */
	 private static void assertContextInjected(){
		Validate.validState(applicationContext !=null,"applicationContext 屬性未注入,請在配置檔案中配置定義ApplicationContextContext");
	}
}
  • 在myshop-commons-mapper專案中建立一個RedisCache的工具類
package com.oxford.myshop.commons.utils;

public class RedisCache implements Cache{
	private static final Logger logger=LoggerFactory.getLogger(RedisCache.class);

	private final ReadWriteLock readWriteLock=new ReentranReadWriteLock();
	private final String id;
	private RedisTemplate redisTemplate;

	private static final long EXPIRE_TIME_IN_MINUTES=30		// redis過期時間

	public RedisCache(String id){
		if(id==null){
			throw new IllegalArgumentException("Cache instances require an ID");
		}
		this.id=id;
	}

	@Override
	public String getId(){
		return id;
	}

	/**
	 * Put query result to redis 
	 */
	@Override
	public void putObject(Object key,Object value){
		try{
			RedisTemplate redisTemplate=getRedisTemplate();
			ValueOperations opsForValue=redisTemplate.opsForValue();
			opsForValue.set(key, value, EXPIRE_TIME_IN_MINUTES, TimeUnit.MINUTES);
			logger.debug("Put query result to redis");
		}catch(Throwable t){
			logger.error("Redis put failed",t);
		}
	}

	/**
	 * Get cached query result from redis 
	 */
	 @Override
	 public Object getObject(Object key){
		try{
			RedisTemplate redisTemplate=getRedisTemplate();
			ValueOperations opsForValue=redisTemplate.opsForValue();
			opsForValue.set(key, value, EXPIRE_TIME_IN_MINUTES, TimeUnit.MINUTES);
			logger.debug("Get cache query result from redis");
			return opsForValue.get(key);
		}catch(Throwable t){
			logger.error("Redis get failed, fail over to db");
			return null;
		}
	}

	/**
	 * Get cached query result from redis 
	 */
	 @Override
	 public Object getObject(Object key){
		try{
			RedisTemplate redisTemplate=getRedisTemplate();
			ValueOperations opsForValue=redisTemplate.opsForValue();
			opsForValue.set(key, value, EXPIRE_TIME_IN_MINUTES, TimeUnit.MINUTES);
			logger.debug("Get cache query result from redis");
			return opsForValue.get(key);
		}catch(Throwable t){
			logger.error("Redis get failed, fail over to db");
			return null;
		}
	}

	/**
	 * Remove cached query result from redis 
	 */
	 @Override
	 @SuppressWarnings("unchecked")
	 public Object removeObject(Object key){
		try{
			RedisTemplate redisTemplate=getRedisTemplate();
			redisTemplate.delete(key);
			logger.debug("Remove cached query result from redis");
		}catch(Throwable t){
			logger.error("Redis remove failed");
		}
			return null;
	}

	/**
	 * Clear this cache instance
	 */
	 @Override
	 public void clear(){
	 	RedisTemplate redisTemplate=getRedisTemplate();
	 	redisTemplate.execute((RedisCallback)->{
	 		connection.flushDb();
	 		return null;
	 	});
	 	logger.debug("Clear all the cached query result from redis");
	 }

	@Override
	public int getSize(){
		return 0;
	}
	
	@Override
	public ReadWriteLock getReadWriteLock(){
		return readWriteLock;
	}

	private RedisTemplate getRedisTemplate(){
		if(redisTemplate==null){
			redisTemplate=ApplicationContextHolder.getBean("redisTemplate");
		}
		return redisTemplate;
	}
}
Mapper介面類中標註註解
  • 在Mapper介面類上標註註解,宣告使用二級快取
@CacheNamespace(implementation=RedisCache.class)

相關文章