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

目录

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

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

如何伪装成一个服务端开发(三) 

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

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

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

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

 

前言

    如果你想在网上再找一个这么详细的入门 Spirng Boot + redis的项目,那你可得费点力气了……因为我就尝试过……

NoSQL

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

    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 提供了序列化器的机制,并且实现了几个序列化器

                            09b47e5fe12e9d7092576e12894e84dc1c7.jpg

    而上面这个奇怪的字符串就是因为String对象通过了JdkSerializationRedisSerializer序列化之后存入的。但是我们的key "username" 为什么没有变得奇怪呢?因为我们在XXApplication主动设置了序列化的接口 StringRedisSerializer。

    RedisTemplate可以设置以下序列化器

属  性描   述备  注
defaultSerializer默认序列化器如果没有设置,则使用 JdkSerializationRedisSerializer
keySerializerRedis 键序列化器如果没有设置,则使用默认序列化器
valueSerializerRedis 值序列化器如果没有设置,则使用默认序列化器
hashKeySerializerRedis 散列结构 field 序列化器如果没有设置,则使用默认序列化器
hashValueSerializerRedis 散列结构 value 序列化器如果没有设置,则使用默认序列化器
stringSerializer字符串序列化器

RedisTemplate 自动赋值为 StringRedisSerializer 对象

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

    我们通过redisTemplate进行操作(也可以通过stringRedisTemplate,区别就是stringRedisTemplate 相当于redisTemplate<String,String>),首先redisTemplate获取redis连接,然后进行操作,然后关闭连接(上面有提到)。

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

    PS 最新版本还有一种和分布式相关的 ClusterOperations 这里我们暂且不表。如有需要可以看这里

操 作 接 口功  能备  注获取接口方法连续操作接口获取连续操作接口
GeoOperations地理位置操作接口使用不多,本书不再介绍redisTemplate.opsForGeo();BoundGeoOperationsredisTemplate.boundGeoOps("geo");
HashOperations散列操作接口 redisTemplate.opsForHash();BoundHashOperationsredisTemplate.boundHashOps("hash");
HyperLogLogOperations基数操作接口使用不多,本书不再介绍redisTemplate.opsForHyperLogLog();  
ListOperations列表(链表)操作接口 redisTemplate.opsForList();BoundListOperationsredisTemplate.boundListOps("list");
SetOperations集合操作接口 redisTemplate.opsForSet();BoundSetOperationsredisTemplate.boundSetOps("set");
ValueOperations字符串操作接口 redisTemplate.opsForValue();BoundValueOperationsredisTemplate.boundValueOps("string");
ZSetOperations有序集合操作接口 redisTemplate.opsForZSet();BoundZSetOperationsredisTemplate.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监听的键是否发生变化,如果发生了变化,那么就不会执行事务。当事务执行时原子性的不会被其他客户端打断。

    abf45e6e3ebf5cbe584c9c4b5ad5281331f.jpg

    另外,一般如果需要执行事务,都会有多个语句,所以绝大多数情况会和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 提供一个渠道,让消息能够发送到这个渠道上,而多个系统可以监听这个渠道,如短信、微信和邮件系统都可以监听这个渠道,当一条消息发送到渠道,渠道就会通知它的监听者,这样短信、微信和邮件系统就能够得到这个渠道给它们的消息了,这些监听者会根据自己的需要去处理这个消息

    大概是就是这么张图

                        3799b58ad175f667c64f7b01301abad36ac.jpg

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

@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生效的。

 

 

 

转载于:https://my.oschina.net/zzxzzg/blog/3000345

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值