boot2.0第七章redis

最新mybatis配置

        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.2</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>

	如:
    <version>6.0.6</version>


#數據庫配置
#spring.datasource.url=jdbc:mysql://localhost:3306/test

spring.datasource.url=jdbc:mysql://localhost:3306/test?serverTimezone=UTC&useUnicode=true&characterEncoding=utf8&useSSL=false
#serverTimezone 必須要配置

#可以不配置數據庫驅動,Spring Boot會自己發現
#spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

com.mysql.jdbc.Driver 是 mysql-connector-java 5中的,
com.mysql.cj.jdbc.Driver 是 mysql-connector-java 6中的

在設定時區的時候,如果設定serverTimezone=UTC,會比中國時間早8個小時,如果在中國,可以選擇Asia/Shanghai或者Asia/Hongkong,
serverTimezone=Asia/Shanghai

redis介紹和xml (spring-data-redis)

  • 加速系統,redis 和 mongoDB
  • redis 是運行在內存中的數據庫。支持7中數據類型
  • 開源,使用 ansic語言編寫,遵循 BSD協議,基於內存可持久化的日誌型。
  • 運行速度 是關係數據庫的 幾倍 到 幾十倍。
  • redis 1秒內 完成 10萬次的 讀寫。
  • 將常用的數據類型存儲在redis中,網站性能大幅提高
  • 正常網站的查詢 和 更新 是 1:9 到 3:7
  • 當一個會員登錄網站,可以把常用數據 一次性查詢放入redis
  • redis提供簡單的事務,lua語言增加運算能力(原子性)
  • spring 通過 spring-data-redis 對redis進行支持
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-redis</artifactId>
			<exclusions>
				<!--不依賴Redis的異步客戶端lettuce -->
				<exclusion>
					<groupId>io.lettuce</groupId>
					<artifactId>lettuce-core</artifactId>
				</exclusion>
			</exclusions>
		</dependency>
		<!--引入Redis的客戶端驅動jedis -->
		<dependency>
			<groupId>redis.clients</groupId>
			<artifactId>jedis</artifactId>
		</dependency>

  • redis是 鍵值數據庫,以字符串爲中心
  • 包括:字符串,散列,列表(鏈表),集合,有序集合,基數 和 地理位置

Spring-data-redis

  • redis連接驅動有 Jedis ,其他還有 Lettuce (少), Jredis 和 Srp 不被推薦使用

  • 提供 RedisConnectionFactory ,生成 RedisConnection接口對象(Redis底層封裝)

  • Spring提供實現類 JedisConnection 去封裝原有 jedis

  • 第一步肯定要配置這個工廠了,先配置redis連接池 (最大連接數,超時時間)

  • RedisConnectionFactory —— JedisConnectionFactory —— 連接工廠管理redis連接池

  • 通過工廠獲取到 RedisConnection

    • AbstractRedisConnection——JedisConnection 封裝jedis操作
    • RedisClusterConnection——JedisClusterConection 集羣支持
    • StringRedisConnection——DefaultStringRedisConnection 字符串支持

創建redisConnectionFactory

//@Configuration
public class RedisConfig {

	private RedisConnectionFactory connectionFactory = null;
	
	@Bean(name = "redisConnectionFactory")
	public RedisConnectionFactory initConnectionFactory() {
		if (this.connectionFactory != null) {
			return this.connectionFactory;
		}
		JedisPoolConfig poolConfig = new JedisPoolConfig();
		// 最大空閒數
		poolConfig.setMaxIdle(50);
		// 最大連接數
		poolConfig.setMaxTotal(100);
		// 最大等待毫秒數
		poolConfig.setMaxWaitMillis(2000);
		// 創建Jedis連接工廠
		JedisConnectionFactory connectionFactory = new JedisConnectionFactory(poolConfig);
		// 配置Redis連接服務器
		RedisStandaloneConfiguration rsc = connectionFactory.getStandaloneConfiguration();
		rsc.setHostName("192.168.10.128");
		rsc.setPort(6379);
		rsc.setPassword(RedisPassword.of("123456"));
		this.connectionFactory = connectionFactory;
		return connectionFactory;
	}
	
	@Bean(name="redisTemplate")
	public RedisTemplate<Object, Object> initRedisTemplate() {
	    RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
	    redisTemplate.setConnectionFactory(initConnectionFactory());
	    
	    RedisSerializer<String> stringRedisSerializer = redisTemplate.getStringSerializer();
	    redisTemplate.setKeySerializer(stringRedisSerializer);//key 序列化
	    //setValueSerializer 也設置成string多好
	    redisTemplate.setHashKeySerializer(stringRedisSerializer);//hash key序列化
	    redisTemplate.setHashValueSerializer(stringRedisSerializer);//hash Value序列化
	    
	  return redisTemplate;
	}
}
  • redisTemplate 會自動從工程獲取連接,關閉連接

測試redisTemplate

keys k* 命令行查看存儲的key

散列結構:hget hash field //hget map放入的鍵名字 hashMap的鍵 得到的是hvalue

    public static void main(String[] args) {
        //SpringApplication.run(DemoStartSevenApplication.class, args);
        AnnotationConfigApplicationContext context=new AnnotationConfigApplicationContext(RedisConfig.class);
        RedisTemplate bean = context.getBean(RedisTemplate.class);
        bean.opsForValue().set("key1","我的測試ke1");
        bean.opsForHash().put("hash","field","hvalue");
    }

自定義序列化

  • redis是一種基於字符串存儲的NoSQL,而java是基於對象的,

  • java提供了序列化機制。只要實現 java.io.serializable接口 即可。將對象進行序列化 得到二進制字符串

  • 序列化器設置 RedisSerializer

    • GenericToString Serializer
    • Jackson2Json RedisSerializer
    • GenericJackson2 RedisSerializer
    • Jdk Serialization RedisSerializer (默認)
    • OxmSerializer
    • String RedisSerializer (常用)
    • JacksonJson RedisSerializer (@Deprecated)
  • RedisSerializer 有 bye[] serialize(T t) 和 deserialize (byte[] b)

  • 實現Serializable接口對象 ——redis序列化容器——二進制(存到redis)

        RedisSerializer<String> stringRedisSerializer = redisTemplate.getStringSerializer();
        redisTemplate.setKeySerializer(stringRedisSerializer);//key 序列化
        redisTemplate.setValueSerializer(stringRedisSerializer);//value 序列化
        redisTemplate.setHashKeySerializer(stringRedisSerializer);//hash key序列化
        redisTemplate.setHashValueSerializer(stringRedisSerializer);//hash Value序列化

		redisTemplate.setDefaultSerializer(stringRedisSerializer);
	    redisTemplate.setStringSerializer(stringRedisSerializer);//字符串序列化,自動賦值爲這個序列化器
        bean.opsForValue().set("key1","我的測試ke1");
        bean.opsForHash().put("hash","field","hvalue");
        //是兩條連接的操作, 爲了克服這個問題spring提供了 RedisCallback和SessionCallback
		//string操作最多,所以提供了 StringRedisTemplate 這個類繼承 RedisTemplate

spring 對redis數據類型的操作

redis支持7種數據類型: redisTemplate

  • 字符串 Value Operations rt.opsForValue().set(“key1”,“我的測試ke1”);

  • 散列 Hash Operations rt.opsForHash().put(“hash”,“field”,“hvalue”);

  • 列表(鏈表)List Operatios rt.opsForList();

  • 集合 Set Operatios bean.opsForSet();

  • 有序集合 ZSet Operations bean.opsForZSet();

  • 基數 HyperLog Operations bean.opsForHyperLogLog();

  • 地理位置 Geo Operations 地理位置 bean.opsForGeo();

  • 有時候需要對某一個鍵值對 做連續的操作,連續操作一個散列數據類型 或者 列表多次

  • 我們就可以對某個鍵進行多次操作

    • BoundXXX operations
      • Bound Value Operations rt.boundValueOps(“string”); //string是key

      • Bound Hash Operations XXXXX

      • Bound List Operations

      • Bound Set Operations

      • Bound ZSet Operations

      • Bound HyperLog Operations

      • Bound Geo Operations rt.boundGeoOps(“geo”);

                BoundHashOperations hashOps = stringRedisTemplate.boundHashOps("hash");
                // 刪除兩個個字段
                hashOps.delete("field1", "field2");
                // 新增一個字段
                hashOps.put("filed4", "value5");
        

SessionCallback 和 RedisCallback接口

  • 作用是讓redis回調 (使用同一條Redis連接進行回調,在同一條redis執行多個方法)
  • 同一條連接下執行多個Redis命令
  • SessionCallback 提供了良好的封裝(使用它)
  • RedisCallback 接口比較底層
	// 高級接口,比較友好,一般情況下,優先使用它
	public static void useSessionCallback(RedisTemplate redisTemplate) {
	    redisTemplate.execute(new SessionCallback() {
	        @Override
	        public Object execute(RedisOperations ro) 
	                throws DataAccessException {
	            ro.opsForValue().set("key1", "value1");
	            ro.opsForHash().put("hash", "field", "hvalue");
	            return null;
	        }
	    });
	}
// 需要處理底層的轉換規則,如果不考慮改寫底層,儘量不使用它
public static void useRedisCallback(RedisTemplate redisTemplate) {
    redisTemplate.execute(new RedisCallback() {
        @Override
        public Object doInRedis(RedisConnection rc)
                throws DataAccessException {
            rc.set("key1".getBytes(), "value1".getBytes());
            rc.hSet("hash".getBytes(), "field".getBytes(), "hvalue".getBytes());
            return null;
        }
    });
}
  • RedisCallback接口並不是那麼友好,能夠改一些底層的東西。比如序列化

  • lambda

	//報錯,但可用
	public static void useSessionCallback(RedisTemplate redisTemplate) {
	    redisTemplate.execute((RedisOperations ro) -> {
	        ro.opsForValue().set("key1", "value1");
	        ro.opsForHash().put("hash", "field", "hvalue");
	        return null;
	    });
	}


	public static void useRedisCallback(RedisTemplate redisTemplate) {
	    redisTemplate.execute((RedisConnection rc) -> {
	        rc.set("key1".getBytes(), "value1".getBytes());
	        rc.hSet("hash".getBytes(), "field".getBytes(), "hvalue".getBytes());
	        return null;
	    });
	}

spring boot 配置和使用 Redis

#數據庫配置
spring.datasource.url=jdbc:mysql://localhost:3306/spring_boot_chapter7
spring.datasource.username=root
spring.datasource.password=123456
#可以不配置數據庫驅動,Spring Boot會自己發現
#spring.datasource.driver-class-name=com.mysql.jdbc.Driver


#=========================數據庫相關配置完畢===================================

spring.datasource.tomcat.max-idle=10
spring.datasource.tomcat.max-active=50
spring.datasource.tomcat.max-wait=10000
spring.datasource.tomcat.initial-size=5
#設置默認的隔離級別爲讀寫提交
spring.datasource.tomcat.default-transaction-isolation=2

#=========================數據源相關配置完畢===================================

#mybatis配置
mybatis.mapper-locations=classpath:com/springboot/chapter7/mapper/*.xml
mybatis.type-aliases-package=com.springboot.chapter7.pojo

#日誌配置爲DEBUG級別,這樣日誌最爲詳細
logging.level.root=DEBUG  #全局日誌爲debug
logging.level.org.springframework=DEBUG
logging.level.org.org.mybatis=DEBUG

#Redis配置
spring.redis.jedis.pool.min-idle=5  #連接池屬性
spring.redis.jedis.pool.max-active=10
spring.redis.jedis.pool.max-idle=10
spring.redis.jedis.pool.max-wait=2000

spring.redis.port=6379 #服務器屬性
spring.redis.host=192.168.11.128
spring.redis.password=123456

#redis連接超時時間,單位毫秒
spring.redis.timeout=1000

#緩存配置
spring.cache.type=REDIS
spring.cache.cache-names=redisCache

spring.cache.redis.use-key-prefix=false
spring.cache.redis.key-prefix=

spring.cache.redis.cache-null-values=true

spring.cache.redis.time-to-live=600000
  • 會自動生成 RedisConnectioFactory,RedisTemplate,StringRedisTemplate (只使用字符串,不支持java對象)

  • 通過RedisTemplate 序列化器 來處理

    main的啓動類上都行:
    	@Autowired
    	private RedisTemplate redisTemplate = null;
    
    	// 設置RedisTemplate的序列化器
    	private void initRedisTemplate() {
    		RedisSerializer stringSerializer = redisTemplate.getStringSerializer();
    		redisTemplate.setKeySerializer(stringSerializer);
    		redisTemplate.setHashKeySerializer(stringSerializer);
    	}
    
    	// 自定義初始化方法
    	@PostConstruct
    	public void init() {
    	    initRedisTemplate();
    	}
    	
    
    

操作redis數據類型(字符和散列)

  • 字符串

  • 散列

  • 列表

  • 集合

  • 有序列表

  • 大部分只需要一次操作所以使用RedisTemplate

  • 需要多次執行Redis命令,可以用 SessionCallback

        @RequestMapping("/stringAndHash")
        @ResponseBody
        public Map<String, Object> testStringAndHash() {
            //設置了一個 key1
            redisTemplate.opsForValue().set("key1", "value1");
            // 注意這裏使用了JDK的序列化器,所以Redis保存的時候不是整數,不能運算
            //設置了一個1
            redisTemplate.opsForValue().set("int_key", "1");
            //設置了一個1
            stringRedisTemplate.opsForValue().set("int", "1");
            // 使用運算。+1
            stringRedisTemplate.opsForValue().increment("int", 1);
            // 獲取底層Jedis連接
            Jedis jedis = (Jedis) stringRedisTemplate.getConnectionFactory().getConnection().getNativeConnection();
            // 減一 操作,這個命令RedisTemplate不支持,所以筆者先獲取底層的連接再操作
            jedis.decr("int");
            
            Map<String, String> hash = new HashMap<String, String>();
            hash.put("field1", "value1");//定義散列的第一個值
            hash.put("field2", "value2");//第二個值
            // 存入一個散列數據類型
            stringRedisTemplate.opsForHash().putAll("hash", hash);
            
            // 新增一個字段
            stringRedisTemplate.opsForHash().put("hash", "field3", "value3");
            
            // 綁定散列操作的key,這樣可以連續對同一個散列數據類型進行操作
            BoundHashOperations hashOps = stringRedisTemplate.boundHashOps("hash");
            // 刪除兩個個字段
            hashOps.delete("field1", "field2");
            
            // 新增一個字段
            hashOps.put("filed4", "value5");
            
            Map<String, Object> map = new HashMap<String, Object>();
            map.put("success", true);
            
            return map;
        }
    

使用string 操作鏈表

  • 列表是一種鏈表結構
  • 查詢性能不高,而增刪節點的性能高。
  • 存在從左到右 或 從右到左的操作
	@RequestMapping("/list")
    @ResponseBody
    public Map<String, Object> testList() {
        // 插入兩個列表,注意它們再鏈表的順序
        // 鏈表從左到右順序爲v10,v8,v6,v4,v2
        stringRedisTemplate.opsForList().leftPushAll("list1", "v2", "v4", "v6", "v8", "v10");
        // 鏈表從左到右順序爲v1,v2,v3,v4,v5,v6
        stringRedisTemplate.opsForList().rightPushAll("list2", "v1", "v2", "v3", "v4", "v5", "v6");
        // 綁定list2鏈表操作
        BoundListOperations listOps = stringRedisTemplate.boundListOps("list2");
        // 從右邊彈出一個成員 v6
        Object result1 = listOps.rightPop();
        // 獲取定位元素,Redis從0開始計算,這裏值爲v2
        Object result2 = listOps.index(1);
        // 從左邊插入鏈表。插入第一個
        listOps.leftPush("v0");
        // 求鏈表長度
        Long size = listOps.size();
        // 求鏈表下標區間成員,整個鏈表下標範圍爲0到size-1,這裏不取最後一個元素
        List elements = listOps.range(0, size - 2);
        Map<String, Object> map = new HashMap<String, Object>();
        map.put("success", true);
        return map;
    }

使用String操作集合

  • redis中是不允許成員重複的

  • 是一個散列的結構,而且是無序的。

  • redis還提供了 交集,並集,差集

    	@RequestMapping("/set")
    	@ResponseBody
    	public Map<String, Object> testSet() {
    		// 請注意:這裏v1重複2次,由於集合不允許重複,所以只是插入5個成員到集合中
    		stringRedisTemplate.opsForSet().add("set1", "v1", "v1", "v2", "v3", "v4", "v5");
    		stringRedisTemplate.opsForSet().add("set2", "v2", "v4", "v6", "v8");
    		// 綁定set1集合操作
    		BoundSetOperations setOps = stringRedisTemplate.boundSetOps("set1");
    		// 增加兩個元素
    		setOps.add("v6", "v7");
    		// 刪除兩個元素
    		setOps.remove("v1", "v7");
    		// 返回所有元素
    		Set set1 = setOps.members();
    		// 求成員數
    		Long size = setOps.size();
    		// 求交集
    		Set inter = setOps.intersect("set2");
    		// 求交集,並且用新集合inter保存
    		setOps.intersectAndStore("set2", "inter");
    		// 求差集
    		Set diff = setOps.diff("set2");
    		// 求差集,並且用新集合diff保存
    		setOps.diffAndStore("set2", "diff");
    		// 求並集
    		Set union = setOps.union("set2");
    		// 求並集,並且用新集合union保存
    		setOps.unionAndStore("set2", "union");
    		Map<String, Object> map = new HashMap<String, Object>();
    		map.put("success", true);
    		return map;
    	}
    

使用string操作有序集合

  • 熱門商品,或 最大的購買買家。 刷新需要及時,用數據庫太慢

  • 有序集合也是一種散列表存儲的方式

  • 有序性 只是靠它在數據結構中增加一個屬性 score分數 得以支持

  • Spring提供了 TypedTuple 接口,有兩個方法。還提供了實現類 DefaultTypedTuple

  • TypedTuple——getValue getScore

    • DefaultTypedTuple —— score value

    • value 保存有序集合的值 (買家)

    • score 保存分數 (買家話費的金錢)

      Tuple
      英 /tjʊpəl; ˈtʌpəl/  美 /ˈtjʊpəl; ˈtʌpəl/  全球(美國)  
      n. [計] 元組,重數
      
      
    @RequestMapping("/zset")
        @ResponseBody
        public Map<String, Object> testZset() {
            Set<ZSetOperations.TypedTuple<String>> typedTupleSet = new HashSet<>();
            for (int i = 1; i <= 9; i++) {
                // 分數
                double score = i * 0.1;
                // 創建一個TypedTuple對象,存入值和分數
                ZSetOperations.TypedTuple<String> typedTuple = new DefaultTypedTuple<String>("value" + i, score);
                typedTupleSet.add(typedTuple);
            }
            // 往有序集合插入元素
            stringRedisTemplate.opsForZSet().add("zset1", typedTupleSet);
            // 綁定zset1有序集合操作
            BoundZSetOperations<String, String> zsetOps = stringRedisTemplate.boundZSetOps("zset1");
            // 增加一個元素
            zsetOps.add("value10", 0.26);
            Set<String> setRange = zsetOps.range(1, 6);
            // 按分數排序獲取有序集合
            Set<String> setScore = zsetOps.rangeByScore(0.2, 0.6);
            // 定義值範圍
            RedisZSetCommands.Range range = new RedisZSetCommands.Range();
            range.gt("value3");// 大於value3
            // range.gte("value3");// 大於等於value3
            // range.lt("value8");// 小於value8
            range.lte("value8");// 小於等於value8
            // 按值排序,請注意這個排序是按字符串排序
            Set<String> setLex = zsetOps.rangeByLex(range);
            // 刪除元素
            zsetOps.remove("value9", "value2");
            // 求分數
            Double score = zsetOps.score("value8");
            // 在下標區間下,按分數排序,同時返回value和score
            Set<ZSetOperations.TypedTuple<String>> rangeSet = zsetOps.rangeWithScores(1, 6);
            // 在分數區間下,按分數排序,同時返回value和score
            Set<ZSetOperations.TypedTuple<String>> scoreSet = zsetOps.rangeByScoreWithScores(1, 6);
            // 按從大到小排序
            Set<String> reverseSet = zsetOps.reverseRange(2, 8);
            Map<String, Object> map = new HashMap<String, Object>();
            map.put("success", true);
            return map;
        }
    
  • TypeTuple保存集合的元素,默認從小到大排序

redis的一些特殊用法

  • 事務
  • 流水線:大批量執行Redis命令的時候
  • 發佈訂閱
  • Lua語 :利用 Redis執行 Lua的原子性 來達到 數據一致性的目的

使用Redis事務

  • 命令組合 watch… multi … exec

  • 在一個 Redis 連接中執行 多個命令,考慮使用 SessionCallback接口

  • watch命令是 可以監控 Redis的一些鍵,multi命令是開始事務

    • 開始事務後,客戶端命令不會馬上執行,而是放到一個隊列裏
    • 執行一些返回數據的命令,結果返回null
  • exe命令的意義在於執行事務

    • 在隊列命令執行前會判斷 被 watch監控 的 redis的鍵 的數據是否發生過變化

    • (重新賦值 都算變化過)

    • 如果變化過,那就取消事務。否則就執行事務。要麼全部執行,要麼全部不執行。

    • 且不會被其他客戶端打斷。

  • watch命令 —— multi 命令開啓事務 —— 命令進入隊列 ——exec命令執行事務

  • 監控鍵值對 是否發生變化 (會判斷watch命令的鍵值對是否發生變化)

    • 是 ,取消事務,取消監控的鍵值對
    • 否,執行事務,取消監控的鍵值對
  • 需要保證 RedisTemplate的鍵和散列結構 的 field 使用 字符串 序列化(STringRedisSerializer)

        @RequestMapping("/multi")
        @ResponseBody
        public Map<String, Object> testMulti() {
            redisTemplate.opsForValue().set("key1", "value1");
            List list = (List) redisTemplate.execute((RedisOperations operations) -> {
                // 設置要監控key1
                operations.watch("key1");
                // 開啓事務,在exec命令執行前,全部都只是進入隊列
                operations.multi();
                operations.opsForValue().set("key2", "value2");
                //operations.opsForValue().increment("key1", 1);// ①
                // 獲取值將爲null,因爲redis只是把命令放入隊列,
                Object value2 = operations.opsForValue().get("key2");
                System.out.println("命令在隊列,所以value爲null【" + value2 + "】");
                operations.opsForValue().set("key3", "value3");
                Object value3 = operations.opsForValue().get("key3");
                System.out.println("命令在隊列,所以value爲null【" + value3 + "】");
                // 執行exec命令,將先判別key1是否在監控後被修改過,如果是不執行事務,否則執行事務
                return operations.exec();// ②
            });
            System.out.println(list);
            Map<String, Object> map = new HashMap<String, Object>();
            map.put("success", true);
            return map;
        }
    
     			// 設置要監控key1
                operations.watch("key1");
                // 開啓事務,在exec命令執行前,全部都只是進入隊列
                operations.multi();
    			// 執行exec命令
                return operations.exec();// ②
    
  • 在 @2 處打斷點,並且手動修改redis中的key1值,此時跳過斷點,所有的不會被執行(在最後一步,會檢查key1,如果和最開始不一致,本次不執行)

    • 因爲 先使用 watch 監控了 key1。operations.watch(“key1”);
    • 之後的 multi 讓之後的命令進入隊列。
    • 在exec 方法運行前,我們手動改了 key1 。事務規則,沒有修改過 纔會執行事務。
  • 把 @1 處標記放開。 此處會報錯,沒法對 string類型 加1 ,但最終 key2 key3還是有值的。

    • 這就是和 數據庫事務不一樣
    • redis事務 先讓命令進入 隊列,並沒有檢測 +1 命令是否執行成功
    • 只有 在 exec命令執行的時候 纔會發現錯誤
    • 錯誤後面的依舊會執行
    • 爲了解決這個問題,在執行事務前,都會嚴格的檢查,避免這樣的情況

Redis流水線

  • 默認情況下,Redis客戶端是一條條發送給 Redis服務器的。(性能不高)
  • 關係數據庫可以批量,執行SQL,一次發送所有SQL
  • Redis也可以,叫做流水線。pipline技術
  • 網絡傳輸造成了瓶頸,使用了流水線大幅度 提升Redis性能

Pipeline
英 /ˈpaɪplaɪn/ 美 /ˈpaɪplaɪn/ 全球(英國)
n. 管道;輸油管;傳遞途徑


- 

```java
@RequestMapping("/pipeline")
	@ResponseBody
	public Map<String, Object> testPipeline() {
		Long start = System.currentTimeMillis();
		List list = (List) redisTemplate.executePipelined((RedisOperations operations) -> {
			for (int i = 1; i <= 100000; i++) {
				operations.opsForValue().set("pipeline_" + i, "value_" + i);
				String value = (String) operations.opsForValue().get("pipeline_" + i);
				if (i == 100000) {
					System.out.println("命令只是進入隊列,所以值爲空【" + value + "】");
				}
			}
			return null;
		});
		Long end = System.currentTimeMillis();
		System.out.println("耗時:" + (end - start) + "毫秒。");
		Map<String, Object> map = new HashMap<String, Object>();
		map.put("success", true);
		return map;
	}

10萬條:耗時:417毫秒。
1萬條:90毫秒
 redisTemplate.executePipelined
不使用流水線,每秒3萬條。
流水線大約能提升10倍的性能
  • 這裏還是用的 SessionCallback 接口

使用Redis發佈訂閱

  • 在企業分配任務後,可以通過郵件,短信或者微信,通知到相關責任人。

  • Redis提供一個通道,讓消息能發送到這個通道上。多個系統可以監聽這個渠道(短信,微信,郵件)

  • 當一條消息發送到 渠道,渠道就會通知它的監聽者。

  • 消息 發送到 渠道 ——短信,微信,郵件。 監聽

定義消息監聽器 MessageListener

@Component
public class RedisMessageListener implements MessageListener {
    @Override //pattern渠道名稱
    public void onMessage(Message message, byte[] pattern) {
        // 消息體
        String body = new String(message.getBody());
        // 渠道名稱
        String topic = new String(pattern); 
        System.out.println(body);
        System.out.println(topic);
    }
}

配置:需要工廠,定時任務,通道,消息監聽器

@SpringBootApplication(scanBasePackages = "com.springboot.chapter7")
public class Chapter7Application {

	// RedisTemplate
	@Autowired
	private RedisTemplate redisTemplate = null;
	
	//redis 連接工廠
	@Autowired
	private RedisConnectionFactory connectionFactory = null;


	// Redis消息監聽器
	@Autowired
	private MessageListener redisMsgListener = null;
	
	// 任務池
	private ThreadPoolTaskScheduler taskScheduler = null;
	
	/**
	 * 創建任務池,運行線程等待處理Redis的消息
	 * @return
	 */
	@Bean
	public ThreadPoolTaskScheduler initTaskScheduler() {
		if (taskScheduler != null) {
			return taskScheduler;
		}
	    //創建任務池
		taskScheduler = new ThreadPoolTaskScheduler();
	    //設置任務池的大小
		taskScheduler.setPoolSize(20);
		return taskScheduler;
	}
	
	/**
	 * 定義Redis的監聽容器
	 * @return 監聽容器
	 */
	@Bean
	public RedisMessageListenerContainer initRedisContainer() {
		RedisMessageListenerContainer container = new RedisMessageListenerContainer();
		// 1,Redis連接工廠
		container.setConnectionFactory(connectionFactory);
		// 2,設置運行任務池。
		container.setTaskExecutor(initTaskScheduler());
		// 3,定義監聽渠道,名稱爲topic1
		Topic topic = new ChannelTopic("topic1");
		// 4,使用監聽器監聽Redis的消息。(上面自定義的)
		container.addMessageListener(redisMsgListener, topic);
		return container;
	}
	
	public static void main(String[] args) {
		SpringApplication.run(Chapter7Application.class, args);
	}

}
  • 定義了一個任務池,設置了任務池的大小(20個)
  • 它將可以運行線程,等待redis消息的傳入
  • 定義了接收 topic1 渠道的消息

測試

    @Resource
    private RedisTemplate redisTemplate;

    @GetMapping("/myTest")
    public String myTest(){
        redisTemplate.convertAndSend("topic1","我的對象,哈哈哈,測試監聽者");
        return "hahaha";
    }

或者:publish topic1 哈哈哈
都會進入:自定義消息監聽器裏面

使用Lua腳本

  • Redis在 2.6 版本號 提供了 Lua腳本的支持

  • 執行Lua腳本具備 原子性。保證數據的一致性

  • 更強大的運算能力,比redis自身的事務,要好一些。

  • 有兩種運行的方法

    • 直接發送Lua到服務器去執行
    • 先把Lua發給redis,Redis對 Lua緩存,返回一個 SHA1的 32位編碼回來,
      • 之後只需發給SHA1和相關參數給Redis便可以執行了
  • 爲什麼要通過32位編碼執行

    • 如果Lua腳本長,需要網絡傳遞,傳遞的速度跟不上redis的執行速度
    • 如果只傳遞32位編碼和參數,需要傳遞的消息就少了許多,可以極大的減少網絡傳輸的內容。
  • Spring提供了,RedisScript接口,還有實現類:DefaultRedisScript

    public interface RedisScript<T> {
        String getSha1();//獲取腳本的 sha1
        
        @Nullable
        Class<T> getResultType();//獲取返回值。返回的java的類型
        
        String getScriptAsString();//獲取腳本的字符串
    }
    
    @RequestMapping("/lua")
    	@ResponseBody
    	public Map<String, Object> testLua() {
    		DefaultRedisScript<String> rs = new DefaultRedisScript<String>();
    		// 設置腳本
    		rs.setScriptText("return 'Hello Redis'");
    		// 定義返回類型,注意如果沒有這個定義Spring不會返回結果
    		rs.setResultType(String.class);
    		RedisSerializer<String> stringSerializer = redisTemplate.getStringSerializer();
    		// 執行Lua腳本
    		String str = (String) redisTemplate.execute(rs, stringSerializer, stringSerializer, null);
    		Map<String, Object> map = new HashMap<String, Object>();
    		map.put("str", str);
    		return map;
    	}
    
    
    

Spring緩存註解操作Redis

  • 簡化Redis使用,提供了緩存註解

  • CacheManager

    • EhCache CacheManager
    • JCache CacheManager
    • Redis CacheManager
    • SimpleCacheManager
  • 需要配置緩存管理器,(緩存類型,超時時間)

  • RedisCacheManager 最常用

緩存管理器配置


#日誌配置爲DEBUG級別,這樣日誌最爲詳細
logging.level.root=DEBUG
logging.level.org.springframework=DEBUG
logging.level.org.org.mybatis=DEBUG

#Redis配置
spring.redis.jedis.pool.min-idle=5
spring.redis.jedis.pool.max-active=10
spring.redis.jedis.pool.max-idle=10
spring.redis.jedis.pool.max-wait=2000
spring.redis.port=6379
spring.redis.host=192.168.11.128
spring.redis.password=123456

#redis連接超時時間,單位毫秒
spring.redis.timeout=1000



spring.cache.caffeine.spec= #caffeine緩存配置細節
spring.cache.couchbase.expiration=0ms
spring.cache.ehcache.config= *
spring.cache.infinispan.config= *
spring.cache.jcache.config= #
spring.cache.jcache.provider= *

#是否允許redis緩存空值
spring.cache.redis.cache-null-values=true 
#是否啓用鍵前綴
spring.cache.redis.use-key-prefix=false
#redis的鍵前綴
spring.cache.redis.key-prefix=
#緩存超時時間戳,配置爲0,則不設置超時時間
spring.cache.redis.time-to-live=600000

#緩存配置。會自動推測
spring.cache.type=REDIS
#配置緩存的名稱
spring.cache.cache-names=redisCache

開啓緩存

@EnableCaching

配置mybatis

pom

		<dependency>
			<groupId>org.mybatis.spring.boot</groupId>
			<artifactId>mybatis-spring-boot-starter</artifactId>
			<!--<version>1.3.1</version> 記得註釋 -->
		</dependency>
		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<scope>runtime</scope>
		</dependency>

properties

#數據庫配置
spring.datasource.url=jdbc:mysql://localhost:3306/spring_boot_chapter7
spring.datasource.username=root
spring.datasource.password=123456
#可以不配置數據庫驅動,Spring Boot會自己發現
#spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.tomcat.max-idle=10
spring.datasource.tomcat.max-active=50
spring.datasource.tomcat.max-wait=10000
spring.datasource.tomcat.initial-size=5
#設置默認的隔離級別爲讀寫提交
spring.datasource.tomcat.default-transaction-isolation=2

#mybatis配置
mybatis.mapper-locations=classpath:com/springboot/chapter7/mapper/*.xml
mybatis.type-aliases-package=com.springboot.chapter7.pojo

pojo

@Alias("user")
public class User implements Serializable {
	private static final long serialVersionUID = 7760614561073458247L;
	private Long id;
	private String userName;
	private String note;
}

mapper接口

@Repository
public interface UserDao {
    // 獲取單個用戶
    User getUser(Long id);
    
    // 保存用戶
    int insertUser(User user);
    
    // 修改用戶
    int updateUser(User user);
    
    // 查詢用戶,指定MyBatis的參數名稱
    List<User> findUsers(@Param("userName") String userName,
                         @Param("note") String note);
    
    // 刪除用戶
    int deleteUser(Long id); 
}

主類的包掃描

//指定掃描的MyBatis Mapper
@MapperScan(basePackages = "com.springboot.chapter7", annotationClass = Repository.class)

userMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.springboot.demostartseven.dao.UserDao">

	<select id="getUser" parameterType="long" resultType="user">
		select id, user_name as userName, note from t_user
		where id = #{id}
	</select>

	<insert id="insertUser" useGeneratedKeys="true" keyProperty="id"
		parameterType="user">
		insert into t_user(user_name, note)
		values(#{userName}, #{note})
	</insert>

	<update id="updateUser">
		update t_user
		<set>
			<if test="userName != null">user_name =#{userName},</if>
			<if test="note != null">note =#{note}</if>
		</set>
		where id = #{id}
	</update>

	<select id="findUsers" resultType="user">
		select id, user_name as userName, note from t_user
		<where>
			<if test="userName != null">
				and user_name = #{userName}
			</if>
			<if test="note != null">
				and note = #{note}
			</if>
		</where>
	</select>

	<delete id="deleteUser" parameterType="long">
		delete from t_user where id = #{id}
	</delete>
</mapper>
  • useGeneratedKeys=“true” keyProperty=“id”

  • MyBatis機會將數據庫生成的主鍵 回填到POJO id中

mybatis配置思路

1.pom  mybatis-starter 和 mysql驅動
2.properties  連接配置, mapper.xml位置,pojo位置
3.主類的main  配置mapper接口位置 @MapperScan(basePackages = "com.XX", annotationClass = Repository.class)
4.dao接口 @Repository
5.sql的xml配置  

service層使用緩存

public interface UserService {
    // 獲取單個用戶
    User getUser(Long id);
    
    // 保存用戶
    User insertUser(User user);
    
    // 修改用戶,指定MyBatis的參數名稱
    User updateUserName(Long id, String userName);
    
    // 查詢用戶,指定MyBatis的參數名稱
    List<User> findUsers(String userName, String note);    
    
    // 刪除用戶
    int deleteUser(Long id);  
}
@Service
public class UserServiceImpl implements UserService {

	@Autowired
	private UserDao userDao = null;

	// 插入用戶,最後MyBatis會回填id,取結果id緩存用戶
	@Override
	@Transactional
    //把結果放入redis註解
	@CachePut(value = "redisCache", key = "'redis_user_'+#result.id")
	public User insertUser(User user) {
		userDao.insertUser(user);
		return user;
	}

	// 獲取id,取參數id緩存用戶
	@Override
	@Transactional
	@Cacheable(value = "redisCache", key = "'redis_user_'+#id")
	public User getUser(Long id) {
		return userDao.getUser(id);
	}

	// 更新數據後,充值緩存,使用condition配置項使得結果返回爲null,不緩存
	@Override
	@Transactional
	@CachePut(value = "redisCache", condition = "#result != 'null'", key = "'redis_user_'+#id")
	public User updateUserName(Long id, String userName) {
		// 此處調用getUser方法,該方法緩存註解失效,
		// 所以這裏還會執行SQL,將查詢到數據庫最新數據
		User user = this.getUser(id);
		if (user == null) {
			return null;
		}
		user.setUserName(userName);
		userDao.updateUser(user);
		return user;

	}

	// 命中率低,所以不採用緩存機制
	@Override
	@Transactional
	public List<User> findUsers(String userName, String note) {
		return userDao.findUsers(userName, note);
	}

	// 移除緩存
	@Override
	@Transactional
	@CacheEvict(value = "redisCache", key = "'redis_user_'+#id", beforeInvocation = false)
	public int deleteUser(Long id) {
		return userDao.deleteUser(id);
	}
}
	@CachePut(value = "redisCache", key = "'redis_user_'+#result.id") //放入redis_user_11
	
	@Cacheable(value = "redisCache", key = "'redis_user_'+#id")
	
	@CachePut(value = "redisCache", condition = "#result != 'null'", key = "'redis_user_'+#id") //在緩存中通過定義的鍵去查詢,查到就返回,否則執行該方法,返回結果。並放入緩存
	
	@CacheEvict(value = "redisCache", key = "'redis_user_'+#id", beforeInvocation = false)//刪除。是否在方法之前就移除,默認爲false
	

action

@Controller
@RequestMapping("/user")
public class UserController {
    
    @Autowired
    private UserService userService = null;
    
    @RequestMapping("/getUser")
    @ResponseBody
    public User getUser(Long id) {
        return userService.getUser(id);
    }
    
    @RequestMapping("/insertUser")
    @ResponseBody
    public User insertUser(String userName, String note) {
        User user = new User();
        user.setUserName(userName);
        user.setNote(note);
        userService.insertUser(user);
        return user;
    }
    
    @RequestMapping("/findUsers")
    @ResponseBody
    public List<User> findUsers(String userName, String note) {
        return userService.findUsers(userName, note);
    }
    
    @RequestMapping("/updateUserName")
    @ResponseBody
    public Map<String, Object> updateUserName(Long id, String userName) {
        User user = userService.updateUserName(id, userName);
        boolean flag = user != null;
        String message = flag? "更新成功" : "更新失敗";
        return resultMap(flag, message);
    }
    
    @RequestMapping("/deleteUser")
    @ResponseBody
    public Map<String, Object> deleteUser(Long id) {
        int result = userService.deleteUser(id);
        boolean flag = result == 1;
        String message = flag? "刪除成功" : "刪除失敗";
        return resultMap(flag, message);
    }
    
    private Map<String, Object> resultMap(boolean success, String message) {
        Map<String, Object> result = new HashMap<String, Object>();
        result.put("success", success);
        result.put("message", message);
        return result;
    }
}

緩存註解 自調用 失效的問題

	@Override
	@Transactional
	@CachePut(value = "redisCache", condition = "#result != 'null'", key = "'redis_user_'+#id")
	public User updateUserName(Long id, String userName) {
		// 此處調用getUser方法,該方法緩存註解失效,
		// 所以這裏還會執行SQL,將查詢到數據庫最新數據
		User user = this.getUser(id);
		if (user == null) {
			return null;
		}
		user.setUserName(userName);
		userDao.updateUser(user);
		return user;
	}
  • String的緩存機制 也是 基於SpringAop原理,Aop是通過動態代理實現的

緩存的髒數據問題

  • 修改id爲1的用戶,更新到數據庫,使用key_1 保存到redis

  • 修改id爲1的用戶,更新到數據庫,使用key_2保存到redis

  • 這樣 key_1 緩存爲髒數據了。 (說不通,此時 爲什麼 不用 key_1保存呢?)

  • 如redis事務。 數據的讀寫 策略不一樣

  • 比如排名,允許髒讀,不允許一直都是錯的。

  • 可以設置redis失效時間

  • 也可設置redis的超時時間(實時性高的,超時時間設置短點)

  • 對於數據的寫操作,一般會從數據庫中先讀取最新的數據,在更新數據

自定義緩存管理器

  • 不希望 採用Spring boot機制 帶來的 鍵命名方式

  • 不希望緩存永不超時

  • 有兩種方法:

    • 通過配置消除緩存 的前綴 和 自定義 超時時間 的屬性 來定製生成創建 緩存管理器
    • 不採用spring boot爲我們生成的,而是通過自己的代碼
#緩存配置
spring.cache.type=REDIS
spring.cache.cache-names=redisCache

#是否允許redis的值爲空
spring.cache.redis.cache-null-values=true

#是否啓用redis前綴。 false是禁用前綴
spring.cache.redis.use-key-prefix=false
#redis的前綴爲
spring.cache.redis.key-prefix=hua-test

#緩存超時時間
spring.cache.redis.time-to-live=600 000   //600 秒。 10分鐘過後redis就會超時。
keys * #查詢redis存在的鍵值對
get redis_user_1 #獲取Id爲1的用戶信息
ttl redis_user_1 #查詢鍵的剩餘超時秒數
	@Autowired //連接工廠
	private RedisConnectionFactory connectionFactory = null;

	@Bean(name = "redisCacheManager" )
	public RedisCacheManager initRedisCacheManager() {
		// Redis加鎖的寫入器
		RedisCacheWriter writer= RedisCacheWriter.lockingRedisCacheWriter(connectionFactory);
        
		// 啓動Redis緩存的默認設置
		RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
        
		// 設置JDK序列化器
		config = config.serializeValuesWith(SerializationPair.fromSerializer(new JdkSerializationRedisSerializer()));
        
		// 禁用前綴
		config = config.disableKeyPrefix();
        
		//設置10分鐘超時
		config = config.entryTtl(Duration.ofMinutes(10));
        //Duration.ofMinutes(10).getSeconds() 600秒
        
		// 創建緩Redis存管理器
		RedisCacheManager redisCacheManager = new RedisCacheManager(writer, config);
		return redisCacheManager;
	}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章