如何假装成一个服务端开发(八) -- Redis

目录

如何假装成一个服务端开发(一)html

如何假装成一个服务端开发(二)java

如何假装成一个服务端开发(三) c++

如何假装成一个服务端开发(四)web

如何假装成一个服务端开发(五)redis

如何假装成一个服务端开发(六)spring

如何假装成一个服务端开发(七)sql

 

前言

    若是你想在网上再找一个这么详细的入门 Spirng Boot + redis的项目,那你可得费点力气了……由于我就尝试过……数据库

NoSQL

    咱们知道数据库链接和调用是耗时的(包括链接,查询等操做)。并且在高并发的状况下会出现明显的瓶颈。因此如何减小数据库访问就逐渐成为互联网系统加速的重要优化点。为此NoSQL诞生了,其中使用普遍的就是Redis和MongoDB。这里先介绍一下Redis。centos

    Redis 是一种运行在内存的数据库,不少时候咱们都会把从数据库查询出来的数据放入Redis,当用户再次查询相同数据的时候,优先使用Redis中存在的数据,由于是存放在内存中,因此速度很快。另外Redis还能够将数据持久化到磁盘中,不少网站甚至放弃了后台数据库,彻底使用Redis来进行数据存储。缓存

安装Redis

    笔者为了学(fan)习(qiang),特意买了一个廉价的VPS,这里正好利用起来,在服务器上安装了mariadb 和 redis。这里不详细介绍安装流程,你们能够本机安装,网上资料不少。

    PS 笔者使用的是centos7 对于安全的限制很严,安装完成mariadb和redis以后,若是须要远程访问,须要开启防火墙端口。

    PS2 这两个东西须要远程访问都须要作一些设置,好比redis须要去掉bind 127.0.0.1的配置等。

 

Spring 中引入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的依赖咱们选择了jedis而spring默认使用的是lettuce。至于jedis就相似于jdbc这种,至关于链接redis的驱动。redis相似一个数据库,c++链接它须要本身的封装,java固然也要本身的封装,这就是jedis了。

    查看网上的各类讲解和例子,通常都是使用jedis,因此固然跟随大众科技了。

 

第一个入门demo

    老夫写代码就是一把梭!开个玩笑……先来一个列子,固然你大概会  “???”

    首先在application.properties中添加配置项

#配置链接池属性
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
#配置Redis服务器属性
spring.redis.port=6379
spring.redis.host=xxx.xxx.xxx.xxx
#spring.redis.password=123456
#Redis链接超时时间,单位毫秒
spring.redis.timeout=1000

    而后修改XXApplication

@SpringBootApplication
public class RedisApplication {

    @Autowired
    private RedisTemplate redisTemplate = null;

    @PostConstruct
    public void init(){
        initRedisTemplate();
    }

    private void initRedisTemplate(){
        RedisSerializer redisSerializer = redisTemplate.getStringSerializer();
        redisTemplate.setKeySerializer(redisSerializer);
        redisTemplate.setHashKeySerializer(redisSerializer);
    }

   ....

}

    最后咱们须要一个controller来测试

@Controller
@RequestMapping("/redis")
public class RedisController {
    @Autowired
    private RedisTemplate redisTemplate;

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @RequestMapping("/test")
    @ResponseBody
    public String testStringAndHash() {
        redisTemplate.opsForValue().set("username","yxwang");
        return "OK";
}

    访问http://localhost:8080/redis/test 页面输出ok

    而后经过redis看看有没有存入,因为我是远程登陆  redis-cli -h xxx.xx.xx.xx -p 6379

    而后 get username 发现有输出,这就表示已经存进去了。

    来看下咱们的auto-config(spring-boot-autoconfigure就是spring帮咱们作自动配置的核心包)帮咱们作了什么。在application.properties中spring.redis相关的配置项目会被读取到 RedisProperties 这个类中。

    而咱们的配置类 JedisConnectionConfiguration又会读取类 RedisProperties 中的内容。经过IoC向外暴露了这个么一个bean

@Bean
	@ConditionalOnMissingBean(RedisConnectionFactory.class)
	public JedisConnectionFactory redisConnectionFactory() throws UnknownHostException {
		return createJedisConnectionFactory();
	}

    而JedisConnectionFactory这个类继承与RedisConnectionFactory,经过它,能够生成一个RedisConnection的接口对象,这个对象就是对Redis底层接口的封装。

    在RedisAutoConfiguration中提供了两个bean

public class RedisAutoConfiguration {

	@Bean
	@ConditionalOnMissingBean(name = "redisTemplate")
	public RedisTemplate<Object, Object> redisTemplate(
			RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
		RedisTemplate<Object, Object> template = new RedisTemplate<>();
        //注入了 RedisConnectionFactory 这个Factory主要用于生成RedisConnection,用于和Redis创建链接
		template.setConnectionFactory(redisConnectionFactory);
		return template;
	}

	@Bean
	@ConditionalOnMissingBean
	public StringRedisTemplate stringRedisTemplate(
			RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
		StringRedisTemplate template = new StringRedisTemplate();
		template.setConnectionFactory(redisConnectionFactory);
		return template;
	}

}

    这两个bean就是咱们用于最终操做Redis的类,它首先从RedisConnectionFactory中获取Redis链接,而后执行Redis操做,最终还会关闭Redis链接。

    因此咱们解决了application.properties的做用流程,也知道了 redisTemplate stringRedisTemplate的注入流程。

    PS 当不适用spring boot时,咱们也彻底能够拷贝上面的代码,手动生成RedisConnectionFactory,而后再手动生成redisTemplate

    咱们发现,当输出get username获取redis中存储的值时,返回的是 "\xac\xed\x00\x05t\x00\x06yxwang" 这么一串东西。这是怎么回事呢?首先须要清楚的是,Redis 是一种基于字符串存储的 NoSQL,而 Java 是基于对象的语言,对象是没法存储到 Redis 中的,不过 Java 提供了序列化机制,只要类实现了 java.io.Serializable 接口,就表明类的对象可以进行序列化,经过将类对象进行序列化就可以获得二进制字符串,这样 Redis 就能够将这些类对象以字符串进行存储。

    Spring 提供了序列化器的机制,而且实现了几个序列化器

                            

    而上面这个奇怪的字符串就是由于String对象经过了JdkSerializationRedisSerializer序列化以后存入的。可是咱们的key "username" 为何没有变得奇怪呢?由于咱们在XXApplication主动设置了序列化的接口 StringRedisSerializer。

    RedisTemplate能够设置如下序列化器

属  性 描   述 备  注
defaultSerializer 默认序列化器 若是没有设置,则使用 JdkSerializationRedisSerializer
keySerializer Redis 键序列化器 若是没有设置,则使用默认序列化器
valueSerializer Redis 值序列化器 若是没有设置,则使用默认序列化器
hashKeySerializer Redis 散列结构 field 序列化器 若是没有设置,则使用默认序列化器
hashValueSerializer Redis 散列结构 value 序列化器 若是没有设置,则使用默认序列化器
stringSerializer 字符串序列化器

RedisTemplate 自动赋值为 StringRedisSerializer 对象

    那么对于上面例子,最后咱们还须要聊一下的就是@Controller中是如何将数据存入redis中的了。

    咱们经过redisTemplate进行操做(也能够经过stringRedisTemplate,区别就是stringRedisTemplate 至关于redisTemplate<String,String>),首先redisTemplate获取redis链接,而后进行操做,而后关闭链接(上面有提到)。

    redis 可以支持7种类型的数据结构,这7种类型是字符串、散列、列表(链表)、集合、有序集合、基数和地理位置。为此 Spring 针对每一种数据结构的操做都提供了对应的操做接口. 

    PS 最新版本还有一种和分布式相关的 ClusterOperations 这里咱们暂且不表。若有须要能够看这里

操 做 接 口 功  能 备  注 获取接口方法 连续操做接口 获取连续操做接口
GeoOperations 地理位置操做接口 使用很少,本书再也不介绍 redisTemplate.opsForGeo(); BoundGeoOperations redisTemplate.boundGeoOps("geo");
HashOperations 散列操做接口   redisTemplate.opsForHash(); BoundHashOperations redisTemplate.boundHashOps("hash");
HyperLogLogOperations 基数操做接口 使用很少,本书再也不介绍 redisTemplate.opsForHyperLogLog();    
ListOperations 列表(链表)操做接口   redisTemplate.opsForList(); BoundListOperations redisTemplate.boundListOps("list");
SetOperations 集合操做接口   redisTemplate.opsForSet(); BoundSetOperations redisTemplate.boundSetOps("set");
ValueOperations 字符串操做接口   redisTemplate.opsForValue(); BoundValueOperations redisTemplate.boundValueOps("string");
ZSetOperations 有序集合操做接口   redisTemplate.opsForZSet(); BoundZSetOperations redisTemplate.boundZSetOps("zset");

    这里有必要介绍下所谓的连续操做。redis中可与存放多个Hash(list set等都同样),好比咱们存在一个hash,名字叫作 "hash1",那么咱们会像这样添加数据  stringRedisTemplate.opsForHash().put("hash1", "field3", "value3");  因而hash1这个hash表中,存在一个key是field3 ,value是 value3的键值对。

    若是我须要继续添加那么仍是须要 stringRedisTemplate.opsForHash().put("hash1", "xxxx", "xxx"); 因此经过stringRedisTemplate.opsForHash() 返回的HashOperations并不会和hash1绑定,咱们能够用它操做全部的hash表。

    能够经过stringRedisTemplate.boundHashOps("hash1"); 返回一个 BoundHashOperations ,它自动和hash1绑定,能够直接操做hashOps.delete("field1", "field2");

    这里各类数据类型就再也不介绍了,基本上须要使用的时候学习下就行。不过有一点ZSet,可能在java中没有对应的数据结构,它是用来作有权重的列表的,好比用来作排行榜。

    这里帖一段测试代码,用到的时候能够当作参考学习。

@RequestMapping("/zset")
@ResponseBody
public Map<String, Object> testZset() {
    Set<TypedTuple<String>> typedTupleSet = new HashSet<>();
    for (int i=1; i<=9; i++) {
        // 分数
        double score = i*0.1;
        // 建立一个TypedTuple对象,存入值和分数
        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);
    // 定义值范围
    Range range = new 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<TypedTuple<String>> rangeSet = zsetOps.rangeWithScores(1, 6);
    // 在分数区间下,按分数排序,同时返回value和score
    Set<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;
}

 

SessionCallback和RedisCallback 接口

    和sql同样,每次咱们调用一个操做就会创建一条连接,好比

redisTemplate.opsForValue().set("key1", "value1");
redisTemplate.opsForHash().put("hash", "field", "hvalue");

    上面代码进行了两次操做,这个时候回创建两条和redis的连接,这样是比较浪费资源的,为此redis推出了两个接口。它们的做用是让 RedisTemplate 进行回调,经过它们能够在同一条链接下执行多个 Redis 命令。其中 SessionCallback 提供了良好的封装,对于开发者比较友好,所以在实际的开发中应该优先选择使用它;相对而言,RedisCallback 接口比较底层,须要处理的内容也比较多,可读性较差,因此在非必要的时候尽可能不选择使用它。        

// 须要处理底层的转换规则,若是不考虑改写底层,尽可能不使用它
public 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;
        }
    });
}

// 高级接口,比较友好,通常状况下,优先使用它
public 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;
        }
    });
}

 

事务

        Redis中的事务有是哪一个关联命令,watch 用于监听Redis中的几个键,而后经过multi表示开启事务(注意是开启,事务尚未被执行),而后经过exe命令执行事务。可是在事务执行以前会检查被watch监听的键是否发生变化,若是发生了变化,那么就不会执行事务。当事务执行时原子性的不会被其余客户端打断。

    

    另外,通常若是须要执行事务,都会有多个语句,因此绝大多数状况会和SessionCallback一块儿使用。

@RequestMapping("/test/translation")
    @ResponseBody
    public String testTranslation() {
        redisTemplate.execute(new SessionCallback() {
            @Override
            public Object execute(RedisOperations operations)
                    throws DataAccessException {
                // 设置要监控key1 key2
                operations.watch(Arrays.asList("key1","key2"));
                operations.multi();
                operations.opsForValue().set("key2", "value2");
                operations.opsForValue().set("key1", "value1");
                return operations.exec();
            }
        });
        return "OK";
    }

    上面代码有一个地方须要特别注意,咱们看下execute方法源码

public <T> T execute(SessionCallback<T> session) {
        Assert.isTrue(this.initialized, "template not initialized; call afterPropertiesSet() before using it");
        Assert.notNull(session, "Callback object must not be null");
        RedisConnectionFactory factory = this.getRequiredConnectionFactory();
        RedisConnectionUtils.bindConnection(factory, this.enableTransactionSupport);

        Object var3;
        try {
            var3 = session.execute(this);
        } finally {
            RedisConnectionUtils.unbindConnection(factory);
        }

        return var3;
    }

    注意到没,execute的返回值,就是SessionCallback的返回值,并且……是同步的。因此redisTamplate.execute是同步执行。

 

Pipeline

    不管是使用事务,仍是使用SessionCallback,redis仍是将命令一条一条送到服务端进行处理,这是相对比较慢的。咱们能够将全部的命令进行打包,这样就只会传输一次。

@RequestMapping("/test/pipeline")
    @ResponseBody
    public String testPipeline(){
        redisTemplate.executePipelined(new SessionCallback<Object>() {
            @Override
            public Object execute(RedisOperations redisOperations) throws DataAccessException {
                for (int i=1; i<=1000; i++) {
                    redisOperations.opsForValue().set("pipeline_" + i, "value_" + i);
                }
                return null;
            }
        });
        return "OK";
    }

        

Redis订阅发布

    我的以为这个Redis最游手好闲的功能,由于无论在使用上仍是过程当中这都和数据没有太大的直接联系。(颇有多是我没有深刻学习原理)

    首先是 Redis 提供一个渠道,让消息可以发送到这个渠道上,而多个系统能够监听这个渠道,如短信、微信和邮件系统均可以监听这个渠道,当一条消息发送到渠道,渠道就会通知它的监听者,这样短信、微信和邮件系统就可以获得这个渠道给它们的消息了,这些监听者会根据本身的须要去处理这个消息

    大概是就是这么张图

                        

        首先须要定义一个监听器,这很简单

@Component
public class RedisMessageListener implements MessageListener {
    @Override
    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);
    }
}

    而后就是经过redis注册监听。

    而后再XXXApplication中添加以下代码

@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();
        // Redis链接工厂
        container.setConnectionFactory(connectionFactory);
        // 设置运行任务池
        container.setTaskExecutor(initTaskScheduler());
        // 定义监听渠道,名称为topic1
        Topic topic = new ChannelTopic("topic1");
        // 使用监听器监听Redis的消息
        container.addMessageListener(redisMessageListener, topic);
        return container;
    }

    PS: 一些自动注入的东西这里没列出来

    这里我有个疑惑,须要提供一个返回RedisMessageListenerContainer的Bean,若是直接运行initRedisContainer的代码,没有提供bean,那么注册不会生效。

    也就是Redis内部是经过依赖注入获取RedisMessageListenerContainer对象,而后将其注册到某个地方的。

    最后咱们能够经过命令行运行 publish topic1 msg 往 topic1通道发送msg消息。

    也能够经过代码发送   

                                redisTemplate.convertAndSend(channel, message);    

 

Lua脚本

    为了加强 Redis 的计算能力,Redis 在2.6版本后提供了 Lua 脚本的支持,并且执行 Lua 脚本在 Redis 中还具有原子性,因此在须要保证数据一致性的高并发环境中,咱们也可使用 Redis 的 Lua 语言来保证数据的一致性,且 Lua 脚本具有更增强大的运算功能,在高并发须要保证数据一致性时,Lua 脚本方案比使用 Redis 自身提供的事务要更好一些。

    在 Redis 中有两种运行 Lua 的方法,一种是直接发送 Lua 到 Redis 服务器去执行,另外一种是先把 Lua 发送给 Redis,Redis 会对 Lua 脚本进行缓存,而后返回一个 SHA1 的32位编码回来,以后只须要发送 SHA1 和相关参数给 Redis 即可以执行了。这里须要解释的是为何会存在经过32位编码执行的方法。若是 Lua 脚本很长,那么就须要经过网络传递脚本给 Redis 去执行了,而现实的状况是网络的传递速度每每跟不上 Redis 的执行速度,因此网络就会成为 Redis 执行的瓶颈。若是只是传递32位编码和参数,那么须要传递的消息就少了许多,这样就能够极大地减小网络传输的内容,从而提升系统的性能。

    为了支持 Redis 的 Lua 脚本,Spring 提供了 RedisScript 接口,与此同时也有一个 DefaultRedisScript 实现类。

public interface RedisScript<T> {
     // 获取脚本的Sha1
    String getSha1();

    // 获取脚本返回值
    Class<T> getResultType();

    // 获取脚本的字符串
    String getScriptAsString();
}

    这里 Spring 会将 Lua 脚本发送到 Redis 服务器进行缓存,而此时 Redis 服务器会返回一个32位的 SHA1 编码,这时候经过 getSha1 方法就能够获得 Redis 返回的这个编码了;getResultType 方法是获取 Lua 脚本返回的 Java 类型;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;
}

    上面代码执行了一个很是简单的Lua脚本 ,就是返回Hello Redis字符串。

    redisTemplate 中,execute 方法执行脚本的方法有两种

public <T> T execute(RedisScript<T> script, List<K> keys, Object... args) 

public <T> T execute(RedisScript<T> script, RedisSerializer<?> argsSerializer, 
        RedisSerializer<T> resultSerializer, List<K> keys, Object... args)

    从参数的名称能够知道,script 就是咱们定义的 RedisScript 接口对象,keys 表明 Redis 的键,args 是这段脚本的参数。两个方法最大区别是一个存在序列化器的参数,另一个不存在。对于不存在序列化参数的方法,Spring 将采用 RedisTemplate 提供的 valueSerializer 序列化器对传递的键和参数进行序列化。这里咱们采用了第二个方法调度脚本,而且设置为字符串序列化器,其中第一个序列化器是键的序列化器,第二个是参数序列化器,这样键和参数就在字符串序列化器下被序列化了。

    下面咱们再考虑存在参数的状况。例如,咱们写一段 Lua 脚本用来判断两个字符串是否相同

redis.call('set', KEYS[1], ARGV[1]) 
redis.call('set', KEYS[2], ARGV[2]) 
local str1 = redis.call('get', KEYS[1]) 
local str2 = redis.call('get', KEYS[2]) 
if str1 == str2 then  
return 1 
end 
return 0
@RequestMapping("/lua2")
@ResponseBody
public Map<String, Object> testLua2(String key1, String key2, String value1, String value2) {
    // 定义Lua脚本
    String lua = "redis.call('set', KEYS[1], ARGV[1]) \n"
            + "redis.call('set', KEYS[2], ARGV[2]) \n"
            + "local str1 = redis.call('get', KEYS[1]) \n"
            + "local str2 = redis.call('get', KEYS[2]) \n"
            + "if str1 == str2 then  \n"
            + "return 1 \n"
            + "end \n"
            + "return 0 \n";
    System.out.println(lua);
    // 结果返回为Long
    DefaultRedisScript<Long> rs = new DefaultRedisScript<Long>();
    rs.setScriptText(lua);
    rs.setResultType(Long.class);
    // 采用字符串序列化器
    RedisSerializer<String> stringSerializer = redisTemplate.getStringSerializer();
    // 定义key参数
    List<String> keyList = new ArrayList<>();
    keyList.add(key1);
    keyList.add(key2);
    // 传递两个参数值,其中第一个序列化器是key的序列化器,第二个序列化器是参数的序列化器
    Long result = (Long) redisTemplate.execute(
        rs, stringSerializer, stringSerializer, keyList, value1, value2);
    Map<String, Object> map = new HashMap<String, Object>();
    map.put("result", result);
    return map;
}

 

在Spring中使用注解操做Redis

    Redis在web开发中最重要的做用大概就是用来做为缓存存储数据,加快查询速度。

启用缓存和CacheManager

    首先缓存处理器 CacheManager有不少的实现类,它并非为Redis特别定制的。可是因为咱们使用Redis,因此天然咱们的缓存就会选择RedisCacheManager这个实现类。

    在Spring Boot中有如下配置项能够用于CacheManager配置

# SPRING CACHE (CacheProperties)
spring.cache.cache-names= # 若是由底层的缓存管理器支持建立,以逗号分隔的列表来缓存名称
spring.cache.caffeine.spec= # caffeine 缓存配置细节
spring.cache.couchbase.expiration=0ms # couchbase 缓存超时时间,默认是永不超时
spring.cache.ehcache.config= # 配置 ehcache 缓存初始化文件路径
spring.cache.infinispan.config=  #infinispan 缓存配置文件
spring.cache.jcache.config=  #jcache 缓存配置文件
spring.cache.jcache.provider= #jcache 缓存提供者配置
spring.cache.redis.cache-null-values=true # 是否容许 Redis 缓存空值
spring.cache.redis.key-prefix= # Redis 的键前缀
spring.cache.redis.time-to-live=0ms # 缓存超时时间戳,配置为0则不设置超时时间
spring.cache.redis.use-key-prefix=true # 是否启用 Redis 的键前缀
spring.cache.type= # 缓存类型,在默认的状况下,Spring 会自动根据上下文探测

    就使用Redis来讲,咱们只须要关注这些

spring.cache.cache-names= # 若是由底层的缓存管理器支持建立,以逗号分隔的列表来缓存名称
spring.cache.redis.cache-null-values=true # 是否容许 Redis 缓存空值
spring.cache.redis.key-prefix= # Redis 的键前缀
spring.cache.redis.time-to-live=0ms # 缓存超时时间戳,配置为0则不设置超时时间
spring.cache.redis.use-key-prefix=true # 是否启用 Redis 的键前缀
spring.cache.type= # 缓存类型,在默认的状况下,Spring 会自动根据上下文探测

    对于刚开始使用,咱们先简单配置下,好比

    spring.cache.type=REDIS

    spring.cache.cache-names=redisCache

    这里的 spring.cache.type 配置的是缓存类型,为 Redis,Spring Boot 会自动生成 RedisCacheManager 对象,而 spring.cache.cache-names 则是配置缓存名称,多个名称可使用逗号分隔,以便于缓存注解的引用。

    另外为了启用缓存管理器,须要在XXXApplication中,须要添加@EnableCaching注解

Demo

    咱们使用mybatis章节中使用过的demo进行,扩展咱们的 MyBatisUserDao

@Repository
public interface MyBatisUserDao {

    // 获取单个用户
    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);
}

    而后须要在userMapper.xml中注册相关操做接口

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

    <insert id="insertUser" useGeneratedKeys="true" keyProperty="id"
            parameterType="user">
        insert into t_user(user_name, note,sex)
        values(#{userName}, #{note},#{sex})
    </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>

    经过将属性 useGeneratedKeys 设置为 true,表明将经过数据库生成主键,而将 keyProperty 设置为 POJO 的 id 属性,MyBatis 就会将数据库生成的主键回填到 POJO 的 id 属性中。   

    再而后修改咱们的MyBatisService接口和实现

@Service
public class MyBatisUserServiceImpl implements MyBatisUserService {

    @Autowired
    private MyBatisUserDao myBatisUserDao = null;

    @Override
    @Transactional
    public User getUser(Long id) {
        return myBatisUserDao.getUser(id);
    }

    @Override
    @Transactional
    public User insertUser(User user) {
        myBatisUserDao.insertUser(user);
        return user;
    }

    @Override
    @Transactional
    public User updateUserName(Long id, String userName) {
        // 此处调用 getUser 方法,该方法缓存注解失效,
        // 因此这里还会执行 SQL,将查询到数据库最新数据
        User user =this.getUser(id);
        if (user == null) {
            return null;
        }
        user.setUserName(userName);
        myBatisUserDao.updateUser(user);
        return user;
    }

    @Override
    @Transactional
    public List<User> findUsers(String userName, String note) {
        return myBatisUserDao.findUsers(userName, note);
    }

    @Override
    @Transactional
    public int deleteUser(Long id) {
        return myBatisUserDao.deleteUser(id);
    }
}

    最后修改MyBatisController

@Controller
@RequestMapping("/mybatis")
public class MyBatisController {

    @Autowired
    private MyBatisUserService myBatisUserService = null;

    @RequestMapping("/getUser")
    @ResponseBody
    public User getUser(Long id) {
        return myBatisUserService.getUser(id);
    }

    @RequestMapping("/insertUser")
    @ResponseBody
    public User insertUser(String userName, String note) {
        User user = new User();
        user.setUserName(userName);
        user.setNote(note);
        user.setSex(SexEnum.FEMALE);
        myBatisUserService.insertUser(user);
        return user;
    }

    @RequestMapping("/findUsers")
    @ResponseBody
    public List<User> findUsers(String userName, String note) {
        return myBatisUserService.findUsers(userName, note);
    }

    @RequestMapping("/updateUserName")
    @ResponseBody
    public Map<String, Object> updateUserName(Long id, String userName) {
        User user = myBatisUserService.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 = myBatisUserService.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;
    }
}

    准备工做完成,接下去就开始添加咱们的缓存。首先引入依赖,添加application.properties配置(上面有列出,这里不细说了)。

    而后再Application中添加@EnableCaching

    修改MyBatisUserServiceImpl的insert方法

// 插入用户,最后 MyBatis 会回填 id,取结果 id 缓存用户
    @Override
    @Transactional
    @CachePut(value ="redisCache", key = "'redis_user_'+#result.id")
    public User insertUser(User user) {
        userDao.insertUser(user);
        return user;
    }

    @CachePut表示将方法结果返回存放到缓存中。  value表示要存入的缓存名,着咱们在application.properties中配置了。key固然表示建值,其中的写法是Spring EL中定义的写法,好比#result表示返回值的id字段。

    而后修改getUser方法

@RequestMapping("/getUser")
    @ResponseBody
    @Cacheable(value ="redisCache", key = "'redis_user_'+#id")
    public User getUser(Long id) {
        return myBatisUserService.getUser(id);
    }

    @Cacheable 表示先从缓存中经过定义的键查询,若是能够查询到数据,则返回,不然执行该方法,返回数据,而且将返回结果保存到缓存中。

    PS:这里可能会遇到错误   DefaultSerializer requires a Serializable payload but received an object of type  缘由在于咱们的User类没法被序列化,因此User类须要继承 Serializable 接口

     修改deleteUser

@Override
    @Transactional
    @CacheEvict(value ="redisCache", key = "'redis_user_'+#id",
        beforeInvocation = false)
    public int deleteUser(Long id) {
        return userDao.deleteUser(id);
    }

    @CacheEvict 经过定义的键移除缓存,它有一个 Boolean 类型的配置项 beforeInvocation,表示在方法以前或者以后移除缓存。由于其默认值为 false,因此默认为方法以后将缓存移除。

    在 updateUserName 方法里面咱们先调用了 getUser 方法,由于是更新数据,因此须要慎重一些。通常咱们不要轻易地相信缓存,由于缓存存在脏读的可能性,这是须要注意的,在须要更新数据时咱们每每考虑先从数据库查询出最新数据,然后再进行操做。所以,这里使用了 getUser 方法。可是这里有个无解,有人任务因为getUser使用了@Cacheable注解,因此会先从缓存中读取数据,这就致使了脏数据的可能。实际上这里的@Cacheable是失效了的,由于 Spring 的缓存机制也是基于 Spring AOP 的原理,而在 Spring 中 AOP 是经过动态代理技术来实现的,这里的 updateUserName 方法调用 getUser 方法是类内部的自调用,并不存在代理对象的调用,这样便不会出现 AOP,也就不会使用到标注在 getUser 上的缓存注解去获取缓存的值了,这是须要注意的地方。

    PS 解决类内部自调用问题可使用双服务互相调用的方法克服。

缓存脏数据以及超时设置

    使用缓存可使得系统性能大幅度地提升,可是也引起了不少问题,其中最为严重的问题就是脏数据问题,好比

时  刻 动 做 1 动 做 2 备  注
T1 修改 id 为1的用户    
T2 更新数据库数据    
T3 使用 key_1 为键保存数据    
T4   修改 id 为1的用户 与动做1操做同一数据
T5   更新数据库数据 此时修改数据库数据
T6   使用 key_2 为键保存数据 这样 key_1为键的缓存就已是脏数据

      对于数据的读操做,通常而言是容许不是实时数据,如一些电商网站还存在一些排名榜单,而这个排名每每都不是实时的,它会存在延迟,其实对于查询是能够存在延迟的,也就是存在脏数据是容许的。可是若是一个脏数据始终存在就说不通了,这样会形成数据失真比较严重。通常对于查询而言,咱们能够规定一个时间,让缓存失效,在 Redis 中也能够设置超时时间,当缓存超过超时时间后,则应用再也不可以从缓存中获取数据,而只能从数据库中从新获取最新数据,以保证数据失真不至于太离谱。

    咱们能够经过设置属性 spring.cache.redis.time-to-live=600000 来设置超时时间,好比这里设置超时时间10分钟。

    对于数据的写操做,每每采起的策略就彻底不同,须要咱们谨慎一些,通常会认为缓存不可信,因此会考虑从数据库中先读取最新数据,而后再更新数据,以免将缓存的脏数据写入数据库中,致使出现业务问题。

    有时候,在自定义时可能存在比较多的配置,也能够不采用 Spring Boot 自动配置的缓存管理器,而是使用自定义的缓存管理器。

// 注入链接工厂,由Spring Boot自动配置生成
@Autowired
private RedisConnectionFactory connectionFactory = null;

// 自定义Redis缓存管理器
@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 min超时
    config = config.entryTtl(Duration.ofMinutes(10));
    // 建立缓Redis存管理器
    RedisCacheManager redisCacheManager = new RedisCacheManager(writer, config);
    return redisCacheManager;
}

    这里首先注入了 RedisConnectionFactory 对象,该对象是由 Spring Boot 自动生成的。在建立 Redis 缓存管理器对象 RedisCacheManager 的时候,首先建立了带锁的 RedisCacheWriter 对象,而后使用 RedisCacheConfiguration 对其属性进行配置,这里设置了禁用前缀,而且超时时间为 10 min;最后就经过 RedisCacheWriter 对象和 RedisCacheConfiguration 对象去构建 RedisCacheManager 对象了,这样就完成了 Redis 缓存管理器的自定义。

 

总结

    就Reids,虽然上面贴的代码不少,demo也比较依赖mybatis章节的原有demo,可是总得来讲知识点仍是相对完整的。注解不是惟一的选择,可是注解确实有不错的收益。

    Redis的注解也是经过AOP生效的。

相关文章
相关标签/搜索