博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
如何伪装成一个服务端开发(八) -- Redis
阅读量:6715 次
发布时间:2019-06-25

本文共 25599 字,大约阅读时间需要 85 分钟。

hot3.png

目录

 

前言

    如果你想在网上再找一个这么详细的入门 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

org.springframework.boot
spring-boot-starter-data-redis
io.lettuce
lettuce-core
redis.clients
jedis

    引入上面的依赖,对于redis的依赖我们选择了jedis而spring默认使用的是lettuce。至于jedis就类似于jdbc这种,相当于连接redis的驱动。redis类似一个数据库,c++连接它需要自己的封装,java当然也要自己的封装,这就是jedis了。

    查看网上的各种讲解和例子,一般都是使用jedis,所以当然跟随大众科技了。

 

第一个入门demo

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

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

#配置连接池属性spring.redis.jedis.pool.min-idle=5spring.redis.jedis.pool.max-active=10spring.redis.jedis.pool.max-idle=10spring.redis.jedis.pool.max-wait=2000#配置Redis服务器属性spring.redis.port=6379spring.redis.host=xxx.xxx.xxx.xxx#spring.redis.password=123456#Redis连接超时时间,单位毫秒spring.redis.timeout=1000

    然后修改XXApplication

@SpringBootApplicationpublic 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
redisTemplate( RedisConnectionFactory redisConnectionFactory) throws UnknownHostException { RedisTemplate
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
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")@ResponseBodypublic Map
testZset() { Set
> typedTupleSet = new HashSet<>(); for (int i=1; i<=9; i++) { // 分数 double score = i*0.1; // 创建一个TypedTuple对象,存入值和分数 TypedTuple
typedTuple = new DefaultTypedTuple
("value" + i, score); typedTupleSet.add(typedTuple); } // 往有序集合插入元素 stringRedisTemplate.opsForZSet().add("zset1", typedTupleSet); // 绑定zset1有序集合操作 BoundZSetOperations
zsetOps = stringRedisTemplate.boundZSetOps("zset1"); // 增加一个元素 zsetOps.add("value10", 0.26); Set
setRange = zsetOps.range(1, 6); // 按分数排序获取有序集合 Set
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
setLex = zsetOps.rangeByLex(range); // 删除元素 zsetOps.remove("value9", "value2"); // 求分数 Double score = zsetOps.score("value8"); // 在下标区间下,按分数排序,同时返回value和score Set
> rangeSet = zsetOps.rangeWithScores(1, 6); // 在分数区间下,按分数排序,同时返回value和score Set
> scoreSet = zsetOps.rangeByScoreWithScores(1, 6); // 按从大到小排序 Set
reverseSet = zsetOps.reverseRange(2, 8); Map
map = new HashMap
(); 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 execute(SessionCallback
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() {            @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

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

@Componentpublic 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
{ // 获取脚本的Sha1 String getSha1(); // 获取脚本返回值 Class
getResultType(); // 获取脚本的字符串 String getScriptAsString();}

    这里 Spring 会将 Lua 脚本发送到 Redis 服务器进行缓存,而此时 Redis 服务器会返回一个32位的 SHA1 编码,这时候通过 getSha1 方法就可以得到 Redis 返回的这个编码了;getResultType 方法是获取 Lua 脚本返回的 Java 类型;getScriptAsString 是返回脚本的字符串.

    

@RequestMapping("/lua")@ResponseBodypublic Map
testLua() { DefaultRedisScript
rs = new DefaultRedisScript
(); // 设置脚本 rs.setScriptText("return 'Hello Redis'"); // 定义返回类型。注意:如果没有这个定义,Spring 不会返回结果 rs.setResultType(String.class); RedisSerializer
stringSerializer = redisTemplate.getStringSerializer(); // 执行 Lua 脚本 String str = (String) redisTemplate.execute( rs, stringSerializer, stringSerializer, null); Map
map = new HashMap
(); map.put("str", str); return map;}

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

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

public 
T execute(RedisScript
script, List
keys, Object... args) public
T execute(RedisScript
script, RedisSerializer
argsSerializer, RedisSerializer
resultSerializer, List
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")@ResponseBodypublic Map
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
rs = new DefaultRedisScript
(); rs.setScriptText(lua); rs.setResultType(Long.class); // 采用字符串序列化器 RedisSerializer
stringSerializer = redisTemplate.getStringSerializer(); // 定义key参数 List
keyList = new ArrayList<>(); keyList.add(key1); keyList.add(key2); // 传递两个参数值,其中第一个序列化器是key的序列化器,第二个序列化器是参数的序列化器 Long result = (Long) redisTemplate.execute( rs, stringSerializer, stringSerializer, keyList, value1, value2); Map
map = new HashMap
(); 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

@Repositorypublic interface MyBatisUserDao {    // 获取单个用户    User getUser(Long id);    // 保存用户    int insertUser(User user);    // 修改用户    int updateUser(User user);    // 查询用户,指定MyBatis的参数名称    List
findUsers(@Param("userName") String userName, @Param("note") String note); // 删除用户 int deleteUser(Long id);}

    然后需要在userMapper.xml中注册相关操作接口

    
insert into t_user(user_name, note,sex) values(#{userName}, #{note},#{sex})
update t_user
user_name =#{userName},
note =#{note}
where id = #{id}
delete from t_user where id = #{id}

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

    再然后修改我们的MyBatisService接口和实现

@Servicepublic 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
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
findUsers(String userName, String note) { return myBatisUserService.findUsers(userName, note); } @RequestMapping("/updateUserName") @ResponseBody public Map
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
deleteUser(Long id) { int result = myBatisUserService.deleteUser(id); boolean flag = result == 1; String message = flag? "删除成功" : "删除失败"; return resultMap(flag, message); } private Map
resultMap(boolean success, String message) { Map
result = new HashMap
(); 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自动配置生成@Autowiredprivate 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

你可能感兴趣的文章
2017.10.1
查看>>
洛谷——P1187 3D模型
查看>>
温度传感器,ds18b20
查看>>
ecshop为什么删不掉商品分类
查看>>
bzoj1941[Sdoi2010]Hide and Seek
查看>>
IT兄弟连 Java Web教程 经典面试题2
查看>>
利用setTimeoutc处理javascript ajax请求超时
查看>>
三、Java基础工具(1)_常用类——字符串
查看>>
文献管理与信息分析》第二讲作业
查看>>
java 遍历arrayList的四种方法
查看>>
根据不同的产品id获得不同的下拉选项 (option传多值)
查看>>
css3新增属性:多列(column)
查看>>
redis 主从配置和集群配置
查看>>
手机3D游戏开发:自定义Joystick的相关设置和脚本源码
查看>>
java 数组偶数排在奇数前面
查看>>
window.frames["detailFrm"].isSubmitting = true;//?起什么作用
查看>>
ASCII表
查看>>
idea之debug
查看>>
什么是真正的流程管理?流程管理的是与不是。
查看>>
SEO实践:SEO友好的URL结构
查看>>