《Spring Boot 实战派》--11.集成Redis,实现高并发
第11章 集成Redis,实现高并发
Redis是大规模互联网应用常用的内存高速缓存数据库,它的读写速度非常快,据官方 Bench-mark的数据,它读的速度能到11万次/秒,写的速度是8.1万次/秒。
本章首先介绍Redis的原理、概念' 数据类型;然后用完整的实例来帮助读者体验缓存增加、 删除、修改和查询功能,以及使用Redis实现文章缓存并统计点击量;最后讲解分布式Session的 使用。
11.1 认识 Spring Cache
在很多应用场景中通常是获取前后相同或更新不频繁的数据,比如访问产品信息数据、网页数 据。如果没有使用缓存,则访问每次需要重复请求数据库,这会导致大部分时间都耗费在数据库 查询和方法调用上,
因为数据库进行I/O操作非常耗费时间,这时就可以利用Spring Cache来 解决。
Spring Cache是Spring提供的一整套缓存解决方案。它本身并不提供缓存实现,而是提供统 —的接口和代码规范、配置、注解等,以便整合各种Cache方案,使用户不用关心Cache的细节。
Spring支持“透明”地向应用程序添加缓存,将缓存应用于方法,在方法执行前检查缓存中是 否有可用的数据。这样可以减少方法执行的次数,同时提高响应的速度。缓存的应用方式“透明”, 不会对调用者造成任何干扰。
只要通过注解@EnableCaching启用了缓存支持,Spring Boot就会 自动处理好缓存的基础配置。
Spring Cache作用在方法上。当调用一个缓存方法时,会把该方法参数和返回结果作为一个 “键值对”(key/value)存放在缓存中,下次用同样的参数来调用该方法时将不再执行该方法,
而是直接从缓存中获取结果进行返回。所以在使用Spring Cache时,要保证在缓存的方法和方法参数 相同时返回相同的结果。
11.1.1声明式缓存注解
Spring Boot提供的声明式缓存(cache)注解,见表
注 解 |
说 明 |
@EnableCaching |
开启缓存 |
@Cacheable |
可以作用在类和方法上,以键值对的方式缓存类或方法的返回值 |
@CachePut |
方法被调用,然后结果被缓存 |
@CacheEvict |
清空缓存 |
©Caching |
用来组合多个注解标签 |
1、@EnableCaching
标注在入口类上,用于开启缓存。
2、@Cacheable
可以作用在类和方法上,以键值对的方式缓存类或方法的返回值。键可以有默认策略和自定义 策略。
@Cacheable注解会先查询是否已经有缓存,如果已有则会使用缓存,如果没有则会执行方法 并进行缓存。
@Cacheable 可以指定 3 个属性 value、key 和 condition
- value :缓存的名称,在Spring配置文件中定义,必须指定至少一个。如, @Cacheable(value二 “cachel”)、@Cacheable(value={ “cachel”,“cache2” }o
- key:缓存的key可以为空,如果自定义key,贝腰按照SpEL表达式编写。可以自动按照 方法的参数组合。如,@Cacheable(value= "cachel”,key二 "#id”)。
- condition:缓存的条件可以为空,如果自定义condition,贝!]使用SpEL表达式编写,以返 回true或false值,只有返回true才进行缓存。如,@Cacheable(value= “cacheT' ’condition二"#id.length()>2” )o
@Cacheable注解的使用方法见以下代码:
<strong> @Cacheable (value = "emp" ,key = "targetclass + methodName +#p0" ) </strong> public User findUserByld( long id) ( return userRepository.findByld(id); ) |
代码解释如下:
- value是必需的,它指定了缓存存放的位置。
- key使用的是SpEL表达式。
- User实体类一定要实现序列化,否则会报“java.io.NotSerializableException”异常。序列化可以继承 Serializable,如 public class User implements Serializable。
3、@CachePut
@CachePut标注的方法在执行前不检查缓存中是否存在之前执行过的结果,而是每次都会执 行该方法,并将执行结果以键值对的形式存入指定的缓存中。
和@Cacheable不同的是, @CachePut每次都会触发真实方法的调用,比如用户更新缓存数据。
需要注意的是,该注解的value和key必须与要更新的缓存相同,即与@Cacheable相同。 具体见下面两段代码:
1 2 3 4 5 6 7 8 | @CachePut (value = "usr" , key = "targetclass + #p0" ) public User updata(User user) ( 〃省略 }<br> @Cacheable (value = "usr" , key = "targetclass +#p0" ) public User save(User user) ( 〃省略 } |
4、@CacheEvict
@CacheEvict用来标注需要清除缓存元素的方法或类。该注解用于触发缓存的清除操作。其 属性有value、key、conditionx allEntries和beforeinvocationo可以用这些属性来指定清除的 条件。使用方法如下:
@Cacheable (value = "usr" ,key = "#pO.id" ) public User save(User user) ( 〃省略 } //清除一条缓存 @CacheEvict (value= "usr" ,key= "#id" ) public void deleteByKey( int id) { 〃省略 }<br> //在方法调用后清空所有缓存 @CacheEvict (value= "accountCache" ,allEntries= true ) public void deleteAII() ( //省略<br>} |
1 2 3 4 5 | 〃在方法调用前清空所有缓存 @CacheEvict (value= "accountCache" ,beforelnvocation= true ) public void deleteAII() ( 〃省略 ) |
5、©Caching
注解©Caching用来组合多个注解标签,有3个属性:cacheable、put和evict,用于指定
@Cacheable、@CachePut 和@。3(:116£5(^。使用方法如下:
@Caching ( cacheable = { @Cacheable (value = "usr" ,key = "#p0" ), //省略},<br> put = ( @CachePut(value = "usr",key = "#p0"), //省略),<br> evict = ( @CacheEvict(value = "usr",key = "#p0"), //省略 }) public User save(User user) ( 〃省略 ) |
11.1.2实例41:用Spring Cache进行缓存管理
本实例展示Spring Cache是如何使用简单缓存(SIMPLE方式)进行缓存管理的。 本实例的源代码可以在*711/CacheDemo”目录下找到。
1.添加依赖
要想集成Spring Cache,只需要在pom.xml文件中添加以下依赖:
1 2 3 4 | <dependency> <groupld>org.springframework.boot</groupld> <artifactld>spring-boot-starter-cache</artifactld> </dependency> |
2、配置缓存管理器
在application.properties文件中配置目标缓存管理器,支持Ehcache、Generic、Redis、 Jcache 等。这里使用 SIMPLE 方式 "spring.cache.type=SIMPLE”。
3、开启缓存功能
在入口类添加注解@EnableCaching,开启缓存功能。
4、在服务实现里编写缓存业务逻辑
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | package com.example.demo.service.impl; <strong> @CacheConfig (cacheNames = "user" ) ©Service</strong> public class UserServicelmpI implements UserService ( <br> @Autowired private UserRepository userRepository;<br> //查找用户 <strong> @Override @Cacheable (key = "#id" )</strong> public User findUserByld( long id) ( User user = userRepository.findUserByld(id); return user; }<br> //新增用户 <strong> ©Override @CachePut (key = "#user.id" )</strong> public User insertUser(User user) ( user = this .userRepository.save(user); return user; }<br> //修改用户 <strong> @Override @CachePut (key = "#user.id" )</strong> public User updateUserByld(User user) ( return userRepository.save(user); }<br> //删除用户 ©Override <strong> @CacheEvict (key = "#id" )</strong> public void deleteUserByld( long id) ( userRepository.deleteByld(id); } } |
从上述代码可以看出,查找用户的方法使用了注解@Cacheable来开启缓存:
修改和添加方法 使用了注解@CachePuto它是先处理方法,然后把结果进行缓存的。
要想删除数据,则需要使用 注解@CacheEvict来清空缓存。
5.控制器里调用缓存服务
这里和本书前面讲解的控制器写法差不多,见以下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | @RestController @RequestMapping ( "user" ) <br> public class UserController {<br> @Autowired private UserService userService;<br> //添加用户 @<strong>PostMapping</strong>('/") public void insertUser() throws Exception ( User user = new User(); user.setUsemame( "zhonghua" ); userService.insertUser(user); }<br> //查找用户 @<strong>GetMapping</strong>( "/(id}" ) public void findUserByld( @PathVariable long id) throws Exception ( User user = userService.findUserByld(id); System.out.println(user.getld() + user.getUsemame()); }<br> //修改用户 @<strong>PutMapping</strong>( "/(id}" ) public User updateUserByld(User user) ( return userService.updatellserByld(user); }<br> //删除用户 @<strong>DeleteMapping</strong>( "/{id}" ) public void deleteUserByld( @PathVariable ( "id" ) long id) { userService.deleteUserByld(id); } } |
至此,项目已经编写完成。接着运行项目,多次访问相应URL:体验缓存效果。主要观察数据库是否进行了操作,如果数据库没有操作数据而正常返回数据,则代表缓存成功。
11.1.3 整合 Ehcache
Spring Boot支持多种不同的缓存产品。在默认情况下使用的是简单缓存,不建议在正式环境 中使用。我们可以配置一些更加强大的缓存,比如Ehcache。
Ehcache是一种广泛使用的开源Java分布式缓存,它具有内存和磁盘存储、缓存加载器、缓 存扩展、缓存异常处理' GZIP缓存、Servlet过滤器,以及支持REST和SOAP API等特点。
使用Ehcache,要先添加如下依赖:
1 2 3 4 | <dependency> <groupld>net.sf.ehcache</groupld> <br> <artifactld>ehcache</artifactld> </dependency> <dependency> <br> <groupld>org.springframework.boot</groupld> <br> <artifactld>spring-boot-starter-cache</artifactld> <br></dependency> |
具体使用方法见如下代码:
1 2 3 4 5 6 7 | <strong> @CacheConfig (cacheNames = ( "userCache" }) </strong> public class UserServicelmpI implements UserService { <br><strong> @Cacheable (key = "targetclass + methodName +#p0" ) </strong> public List<User> findAIILimit( int num) { return userRepository.findAIILimit(num); } } |
11.1.4整合 Caffeine
Caffeine是使用Java 8对Guava缓存的重写版本。它基于LRU算法实现,支持多种缓存过 期策略。
要使用它,需要在pom.xml文件中增加Caffeine依赖,这样Spring Boot就会自动用 Caffeine替换默认的简单缓存。
增加Caffeine依赖的代码如下:
1 2 3 4 | <dependency> <groupld>com.github.ben-manes.caffeine</groupld> <artifactld>caffeine</artifactld> </dependency> |
然后配置参数,见以下代码:
spring.cache.type=caffeine spring.cache.cache-names=myCaffeine spring.cache.caffeine.spec=maximumSize= 1 ,<br>expireAfterAccess=5s |
代码解释如下。
- spring.cache.type:指定使用哪个缓存供应商。
- spring.cache.cache-names:在启动时创建缓存名称(即前面的cacheNames )。如果 有多个名称,则用逗号进行分隔。
- spring.cache.caffeine.spec:这是 Caffeine 缓存的专用配置。
- maximumSize=1:最大缓存数量。如果超岀最大缓存数量,则保留后进(最新)的,最开 始的缓存会被清除。
- expireAfterAccess=5s:缓存5 s,即缓存在5 s之内没有被使用,就会被清除。在默认情 况下,缓存的数据会一直保存在内存中。
有些数据可能用一次后很长时间都不会再用,这样 会有大量无用的数据长时间占用内存,我们可以通过配置及时清除不需要的缓存。
注: Ehcache和Caffeine与Spring Boot的简单缓存用法一样,可以查看11.1.2节。
11.2 认识Redis
11.2.1 对比 Redis 与 Memcached
Cache可以和Redis —起用,Spring Boot支持把Cache存到Redis里。如果是单服务器, 则用Cache、Ehcache或Caffeine,性能更高也能满足需求。如果拥有服务器集群,则可以使用 Redis,这样性能更高。
1、Redis
Redis是目前使用最广泛的内存数据存储系统之一。它支持更丰富的数据结构,支持数据持久 化、事务、HA (高可用High Available)、双机集群系统、主从库。
Redis B key-value存储系统。它支持的value类型包括String、List、Set、Zset (有序集 合)和Hasho这些数据类型都支持push/pop、add/remove,以及取交集、并集、差集或更丰富 的操作,而且这些操作都是原子性的。
在此基础上,Redis支持各种不同方式的排序和算法。
Redis会周期性地把更新后的数据写入磁盘,或把修改操作写入追加的记录文件中(RDB和 AOF两种方式),并且在此基础上实现了 master-slave (主从)同步。机器重启后,能通过持久 化数据自动重建内存。
如果使用Redis作为Cache,则机器宕机后热点数据不会丢失。
丰富的数据结构加上Redis兼具缓存系统和数据库等特性,使得Redis拥有更加丰富的应 用场景。
Redis可能会导致的问题:
- 缓存和数据库双写一致性问题。
- 缓存雪崩问题。
- 缓存击穿问题。
- 缓存的并发竞争问题。
Redis为什么快:
- 纯内存操作。
- 单线程操作,避免了频繁的上下文切换。
- 采用了非阻塞I/O, 多路复用机制。
2、Memcached
Memcached的协议简单,它基于Libevent的事件处理,内置内存存储方式。Memcached 的分布式不互相通信,即各个Memcached不会互相通信以共享信息,分布策略由客户端实现。
它 不会对数据进行持久化,重启Memcached 重启操作系统都会导致全部数据消失。
Memcached常见的应用场景是:
存储一些读取频繁但更新较少的数据,如静态网页、系统 配置及规则数据、活跃用户的基本数据和个性化定制数据、实时统计信息等。
3、比较 Redis 与 Memcached
(1)关注度。
近年来,Redis越来越火热,从图11-1中可以看出:人们对Redis的关注度越来越高;对 Memcached关注度比较平稳,且有下降的趋势。
(2) 性能。
两者的性能都比较高。
(3) 数据类型。
Memcached的数据结构单一。
Redis非常丰富。
(4) 内存大小。
Redis在2.0版本后增加了自己的VM特性,突破物理内存的限制。
Memcached可以修改最大可用内存的大小来管理内存,采用LRU算法。
(5) 可用性。
Redis依赖客户端来实现分布式读写,在主从复制时,每次从节点重新连接主节点都要依赖整 个快照,无增量复制。Redis不支持自动分片(sharding ),如果要实现分片功能,则需要依赖程序 设定一致的散列(hash)机制。
Memcached采用成熟的hash或环状的算法,来解决单点故障引起的抖动问题,其本身没有 数据冗余机制。
(6) 持久化。
Redis依赖快照、AOF进行持久化。但AOF在增强可靠性的同时,对性能也有所影响。
Memcached不支持持久化,通常用来做缓存,以提升性能。
(7) value数据大小。
Redis的value的最大限制是1GB。
Memcached只能保存1MB以内的数据。
(8)数据一致性(事务支持)
Memcached在并发场景下用CAS保证一致性。
Redis对事务支持比较弱,只能保证事务中的每个操作连续执行。
(9)应用场景。
Redis:适合数据量较少、性能操作和运算要求高的场景。
Memcached:适合提升性能的场景。适合读多写少,如果数据量比较大,则可以采用分片的 方式来解决。
11.2.2 Redis的适用场景
1、高并发的读写
Redis特别适合将方法的运行结果放入缓存,以便后续在请求方法时直接去缓存中读取。对执 行耗时,且结果不频繁变动的SQL查询的支持极好。
在高并发的情况下,应尽量避免请求直接访问数据库,这时可以使用Redis进行缓冲操作,让 请求先访问Redis
2、计数器
电商网站(APP)商品的浏览量、视频网站(APP)视频的播放数等数据都会被统计,以便用 于运营或产品分析。为了保证数据实时生效,每次浏览都得+1,这会导致非常高的并发量。
这时可 以用Redis提供的incr命令来实现计数器功能,这一切在内存中操作,所以性能非常好,非常适用 于这些计数场景。
3、排行榜
可以利用Redis提供的有序集合数据类,实现各种复杂的排行榜应用。如京东、淘宝的销量榜 单,商品按时间' 销量排行等。
4、分布式会话
在集群模式下,一般都会搭建以Redis等内存数据库为中心的Session (会话)服务,它不再 由容器管理,而是由Session服务及内存数据库管理。
5、互动场景
使用Redis提供的散列、集合等数据结构,可以很方便地实现网站(APP)中的点赞、踩、关 注共同好友等社交场景的基本功能。
6、最新列表
Redis可以通过LPUSH在列表头部插入一个内容ID作为关键字,LTRIM可用来限制列表的 数量,这样列表永远为/V个ID,无须查询最新的列表,直接根据ID查找对应的内容即可。
11.3 Redis的数据类型
Redis有5种数据类型,见表
数据类型 |
存储的值 |
读写能力 |
string (字符串) |
可以是字符串、整数或浮点数 |
对整个字符串或字符串的其中一部分执行操作;对对象和 浮点数执行自增(increment)或自减(decrement)操作 |
list (列表) |
一个链表,链表上的每个节点都 包含了一个字符串 |
从链表的两端推入或弹出元素;根据偏移量对链表逬行修 剪(trim);读取单个或多个元素;根据值来查找或移除元素 |
set (集合) |
包含字符串的无序收集器 (unorderedcollection ), 并且被 包含的每个字符串都是独一无二 的,各不相同 |
添加、获取、移除单个元素;检查一个元素是否存在于某 个集合中;计算交集、并集、差集;从集合里随机获取元素 |
hash (散列) |
包含键值对的无序散列表 |
添加、获取、移除单个键值对;获取所有键值对 |
zset (有序集合, sorted set) |
字符串成员(member)与浮点 数分值(score)之间的有序映射, 元素的排列顺序由分值的大小决定 |
添加、获取、删除单个元素;根据分值范围(「ange)或 成员来获取元素 |
1、字符串(string)
Redis字符串可以包含任意类型的数据、字符、整数、浮点数等。
一个字符串类型的值的容量有512MB,代表能存储最大512MB的内容。可以使用INCR ( DECR、INCRBY)命令来把字符串当作原子计数器使用。
使用APPEND命令在字符串后添加内容。
应用场景:计数器。
2、列表(list)
Redis列表是简单的字符串列表,按照插入顺序排序。可以通过LPUSH. RPUSH命令添加 一个元素到列表的头部或尾部。
—个列表最多可以包含“232-1" ( 4294967295 )个元素。
应用场景:取最新/V个数据的操作、消息队列、删除与过滤' 实时分析正在发生的情况、数据 统计与防止垃圾邮件(结合Set )=
3、集合(set)
Redis集合是一个无序的、不允许相同成员存在的字符串合集。
支持一些服务器端的命令从现有的集合岀发去进行集合运算,如合并(并集:union)、求交(交 集:intersection )、差集,找出不同元素的操作(共同好友' 二度好友)。
应用场景:Unique操作,可以获取某段时间内所有数据“排重值”,比如用于共同好友、二度 好友、统计独立旧、好友推荐等。
4、散列(hash )
Redis hash是字符串字段和字符串值之间的映射,主要用来表示对象,也能够存储许多元素。
应用场景:存储、读取、修改用户属性。
5、有序集合(sorted sets zset)
Redis有序集合和Redis集合类似,是不包含相同字符串的合集。每个有序集合的成员都关联 着一个评分,这个评分用于把有序集合中的成员按最低分到最高分排列(排行榜应用,取TOP N 操作)。
使用有序集合,可以非常快捷地完成添加、删除和更新元素的操作。元素是在插入时就排好序 的,所以很快地通过评分(score )或位次(position )获得一个范围的元素。
应用场景:排行榜应用、取TOP N、需要精准设定过期时间的应用(时间戳作为Score)、带 有权重的元素(游戏用户得分排行榜)、过期项目处理、按照时间排序等。
11.4 用RedisTemplate操作Redis的5种数据类型
11.4.1认识opsFor方法
Spring封装了 RedisTemplate来操作Redis,它支持所有的Redis原生的API。在 RedisTemplate中定义了对5种数据结构的操作方法。
- opsForValue():操作字符串。
- opsForHash():操作散列。
- opsForList():操作列表。
- opsForSet():操作集合。
- opsForZSet():操作有序集合。
下面通过实例来理解和应用这些方法。这里需要特别注意的是,运行上述方法后要对数据进行 清空操作,否则多次运行会导致数据重复操作。
11.4.2实例42:操作字符串
字符串(string )是Redis最基本的类型ostring的一个“key”对应一个“value”,即key-value 键值对。string是二进制安全的,可以存储任何数据(比如图片或序列化的对象)。
值最大能存储512MB的数据。一般用于一些复杂的计数功能的缓存。RedisTemplate提供以 下操作string的方法。
本实例的源代码可以在“/ll/Redis”目录下找到。
1、set void set(K key, V value);
get V get(Object key)
具体用法见以下代码:
1 2 3 4 5 6 7 8 9 10 11 | @Autowired private RedisTemplate redisTemplate; @Test public void string() { redisT emplate.opsForValue().setC'num", 123 ); redisTemplate.opsForValue().set( "string" , "some strings" ); Object s = redisTemplate.opsForValue().get( "num" ); Object s2 = redisTemplate.opsForValue().get( "string" ); System.out.println(s); System.out.println(s2); } |
输出结果如下:
1 2 | 123 some strings |
2、set void set(K key, V value, long timeout, TimeUnit unit)
以下代码设置3 s失效。3 s之内查询有结果,3 s之后查询则返回为nullo具体用法见以下 代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | @Test public void string2() {<br> 〃设置的是3s失效,3s之内查询有结果,3s之后返回为 null redisTemplate.opsForValueO.setC'num ", " 123XYZ”, 3 , TimeUnit.SECONDS); <br> try ( Object s = redisTemplate.opsForValue().get( "num" ); System.out.println(s); Thread.currentThread().sleep( 2000 );<br> Object s2 = redisTemplate.opsForValue().get( "num" ); System.out.println(s2); Thread.currentThread().sleep( 5000 );<br> Object s3 = redisTemplate.opsForValue().get( "num" ); System.out.println(s3); } catch (InterruptedException ie) ( ie.printStackTraceQ; }<br>} |
运行测试,输岀如下结果:
1 2 | 123XYZ 123XYZ null |
TimeUnit是java.uti 1.concurrent包下面的一个类,表示给定单元粒度的时间段,常 的颗粒度有:
- 天(TimeUnit.DAYS )
- 小时(TimeUnit.HOURS )
- 分钟(TimeUnit.MINUTES )
- 秒(TimeUnit. SECONDS )
- 毫秒(TimeUnit.MILLISECONDS )
3、set void set(K key, V value, long offset)
给定key所存储的字符串值,从偏移量offset开始。具体用法见以下代码:
1 2 3 4 5 6 | @Test public void string3() { //重写(overwrite)给定key所存储的字符串值,从偏移量offset开始 redisTemplate.opsForValue().set( "key" , "hello world" , 6 ); System.out.println( redisTemplate.opsForValue().get( "key" )); } |
运行测试,输岀如下结果:
1 | hello |
4、getAndSet V getAndSet(K key, V value)
设置键的字符串值,并返回其旧值。具体用法见以下代码:
1 2 3 4 5 6 | @Test public void string4() { //设置键的字符串值并返回其旧值 <br> redisTemplate.opsForValue().set("getSetTest","test"); System.out.println(redisTemplate.opsForValue().getAndSet( "getSetTest" , "test2" ) <br><br> System.out.println(redisTemplate.opsForValue().get( "getSetTest" )); } |
运行测试,输出如下结果:
1 2 | test test2 |
5、append Integer append(K key, String value)
如果key已经存在,并且是一个字符串,则该命令将该值追加到字符串的末尾。如果key不存 在,则它将被创建并设置为空字符串,因此append在这种特殊情况下类似于set。用法见以下 代码:
1 2 3 4 5 6 | @Test public void string5() { redisTemplate.opsForValue().append( "k" , "test" ); System.out.println(redisTemplate.opsForValue().get( "k" )); <br> redisTemplate.opsForValue().append(',k "," test2"); System.out.println(redisTemplate.opsForValue().get( "k" )); } |
运行测试,输出如下结果:
1 2 | test testtest2 |
这里一定要注意反序列化配置,否则会报错。
6、size Long size(K key)
返回key所对应的value值的长度,见以下代码:
1 2 3 4 5 | @Test public void string6() { redisTemplate.opsForValue().set( "key" , "1" ); System.out.println(redisTemplate.opsForValue().size( "key" )); ) |
运行测试,输出如下结果:
1 | 3 |
11.4.3 实例43:操作散列
Redis hash (散列)是一个string类型的field和value的映射表,hash特别适合用于存储 对象。value中存放的是结构化的对象。
利用这种数据结构,可以方便地操作其中的某个字段。
比 如在“单点登录”时,可以用这种数据结构存储用户信息。以Cookield作为key,设置30分钟为 缓存过期时间,能很好地模拟岀类似Session的效果。
本实例的源代码可以在711/Redis"目录下找到。
下面介绍具体用法。
1、void putAll(H key, Map<? extends HK, ? extends HV> m)
用m中提供的多个散列字段设置到key对应的散列表中,用法见以下代码:
1 2 3 4 5 6 7 8 | @Test public void hash1() { Map<String,Object> testMap = new HashMap(); testMap.put( "name" , "zhonghua" ); testMap.put( "sex" , "male" );<br> redisTemplate.opsForHash().putAII( "Hash" ,testMap); System.out.println(redisTemplate.opsForHash0.entries( "Hash" )); } |
运行测试,输岀如下结果:
1 | {sex=male, name=zhonghua} |
2、void put(H key, HK hashKey, HV value)
设置hashKey的值,用法见以下代码:
1 2 3 4 5 | @Test public void hash2() { redisTemplate.opsForHash().put('TedisHash ", " name ", " hongwei"); redisTemplate.opsForHash().put( "redisHash" , "sex" , "male" ); System.out.println(redisTemplate.opsForHash().entries( "redisHash" )); <br>} |
运行测试,输出如下结果:
1 | (name=hongwei, sex=male} |
3、List<HV> values(H key)
根据密钥获取整个散列存储的值,用法见以下代码:
1 2 3 | @Test public void hash2() {<br> redisTemplate.opsForHash().put( "redisHash" , "name" , "hongwei" ); <br> redisTemplate.opsForHash().put( "redisHash" , "sex" , "male" ); System.out.println(redisTemplate.opsForHash().values( "redisHash" ));<br>} |
运行测试,输出如下结果:
1 | [hongwei, male] |
4、MapCHK, HV> entries(H key)
根据密钥获取整个散列存储,用法见以下代码:
1 2 3 4 | @Test public void hash2() { redisTemplate.opsForHash().put('TedisHash ", " name ", " hongwei ");<br> redisTemplate.opsForHash().put(" redisHash ", " sex ", " male ");<br> System.out.println(redisTemplate.opsForHash().entries(" redisHash")); ) |
运行测试,输岀如下结果:
1 | {name=hongwei, sex=male) |
5、Long delete(H key, Object... hashKeys)
删除给定的hashKeys,用法见以下代码:
1 2 3 4 5 6 7 | @Test public void hash3() { redisTemplate.opsForHash().put( "redisHash" , "name" , "hongwei" ); redisTemplate.opsForHash().put( "redisHash" , "sex" , "male" ); System.out.println(redisTemplate.opsForHash().delete( "redisHash" , "name" )); System.out.p^intln(redisTemplate.opsForHash().entries( "redisHash" )); } |
运行测试,输出如下结果:
1 2 | 1 (sex=male) |
6、Boolean hasKey(H key, Object hashKey)
确定hashKey是否存在,用法见以下代码:
1 2 3 4 5 6 7 | @Test public void hash4() { redisTemplate.opsForHash().put( "redisHash" , "name" , "hongwei" ); redisTemplate.opsForHash().put( "redisHash" , "sex" , "male" ); System.out.println(redisTemplate.opsForHash().hasKey( "redisHash" , "name" )); System.out.println(redisTemplate.opsForHash().hasKey( "redisHash" , "age" )); } |
运行测试,输出如下结果:
1 2 | true fMse |
7、HV get(H key, Object hashKey)
从键中的散列获取给定hashKey的值,用法见以下代码:
1 2 3 4 5 | @Test public void hash7() { redisTemplate.opsForHash().put( "redisHash" , "name" , "hongwei" ); redisTemplate.opsForHash().put( "redisHash" , "sex" , "male" ); System.out.println(redisTemplate.opsForHash().get( "redisHash" , "name" )); <br>} |
运行测试,输出如下结果:
1 | hongwei |
8、Set<HK> keys(H key)
获取key所对应的key的值,用法见以下代码:
1 2 3 4 5 6 7 | @Test 〃获取key所对应的key的值 public void hash8() { redisTemplate.opsForHash().putC'redisHash ", " name ", " hongwei"); redisTemplate.opsForHash().put( "redisHash" , "sex" , "male" ); System.out.println(redisTemplate.opsForHash().keys( "redisHash" )); } |
运行测试,输岀如下结果:
1 | [sex, name] |
9、Long size(H key)
获取key所对应的散列表的大小个数,用法见以下代码:
1 2 3 | @Test public void hash9() { <br> redisTemplate.opsForHash().putCTedisHash ", " name ", " hongwei "); <br> redisTemplate.opsForHash().put(" redisHash ", " sex ", " male "); <br> System.out.println(redisTemplate.opsForHash().size(" redisHash")); } |
运行测试,输出如下结果:
2
11.4.4实例44:操作列表
Redis列表是简单的字符串列表,按照插入顺序排序。可以添加一个元素到列表的头部(左边) 或尾部(右边)
使用list数据结构,可以做简单的消息队列的功能。还可以利用Irange命令,做基于Redis的分页功能,性能极佳。
而使用SQL语句做分页功能往往效果极差。
本实例的源代码可以在“/ll/Redis”目录下找到。
下面介绍具体用法。
1、Long leftPushAII(K key, V... values)
leftPushAII表示把一个数组插入列表中,用法见以下代码:
1 2 3 4 5 6 | @Test public void Iist1() { String[] strings = new String[]{ "T'," 2 "," 3 "}; redisTemplate.opsForList().leftPushAll( "list" ,strings); System.out.println(redisTemplate.opsForList().range( "list" , 0 , - 1 ); ) |
运行测试,输出如下结果:
[3,2,1]
2、Long size(K key)
返回存储在键中的列表的长度。如果键不存在,贝IJ将其解释为空列表,并返回0。如果key存 储的值不是列表,则返回错误。用法见以下代码:
1 2 3 4 5 6 | @Test public void list2() { String[] strings = new String[]{ "1" , "2,,," 3 "}; redisTemplate.opsForList().leftPushAII( "list" ,strings); System.out.println(redisTemplate.opsForList().size( "list" )); ) |
运行测试,输出如下结果:
3
3、Long leftPush(K key, V value)
将所有指定的值插入在键的列表的头部。如果键不存在,则在执行推送操作之前将其创建为空 列表(从左边插入)。用法见以下代码:
1 2 3 4 5 6 7 8 9 | @Test public void list3() { redisTemplate.opsForList().leftPush( "list" ,"r'); System.out.println(redisTemplate.opsForList().size( "list" )); redisTemplate.opsForList().leftPush( "list" , "2" ); System.out. println(redisTemplate.opsForList().size( "list" ));<br> redisTemplate.opsForList().leftPush( "list" , "3" ); System.out.println(redisTemplate.opsForList().size( "list" )); } |
返回的结果为推送操作后的列表的长度。运行测试,输岀如下结果:
1 2 3 | 1 2 3 |
4、Long rightPush(K key, V value)
将所有指定的值插入存储在键的列表的头部。如果键不存在,则在执行推送操作之前将其创建 为空列表(从右边插入)。用法见以下代码:
1 2 3 4 5 6 7 8 9 | @Test public void list4() { redisTemplate.opsForList().rightPush( "listRight" , "1" ); System.out.println(redisTemplate.opsForList().size( "listRight" ));<br> redisTemplate.opsForList().rightPush( "listRight" , "2" ); System.out.println(redisTemplate.opsForList().size( "listRight" ));<br> redisTemplate.opsForList().rightPush( "listRight" , "3" ); System.out.println(redisTemplate.opsForList().size( "listRight" )); } |
运行测试,输岀如下结果:
1 2 3 | 1 2 3 |
5、Long rightPushAII(K key, V... values)
通过rightPushAII方法向最右边批量添加元素,用法见以下代码:
1 2 3 4 5 | @Test public void list5() { String[] strings = new String[]( "1" ,,, 2 ,,, "3n};<br> redisTemplate.opsForList().rightPushAll(" list",strings); System.out.println(redisTemplate.opsForList().range( "list" , 0 , - 1 )); } |
运行测试,输出如下结果:
1 | [ 1 , 2 , 3 ] |
6、void set(K key, long index, V value)
在列表中index的位置设置value,用法见以下代码:
1 2 3 4 5 6 7 8 | @Test public void list6() { String[] strings = new String[]{” 1 ”, "2" , "3" }; redisTemplate.opsForList().rightPushAll( "list6" , strings); System.out.println(redisTemplate.opsForList().range( "list6" , 0 , - 1 ));<br> redisTemplate.opsForList().set( "list6" , 1 , "值" ); System.out.println(redisTemplate.opsForList().range( "list6" , 0 , - 1 )); } |
运行测试,输岀如下结果:
1 2 | [ 1 , 2 , 3 ] [ 1 ,值, 3 ] |
7、Long remove(K key, long count, Object value)
从存储在键中的列表,删除给定“count”值的元素的第1个计数事件。其中,参数count的 含义如下。
- count":删除等于value的所有元素。
- count>0:删除等于从头到尾移动的值的元素。
- count<0:删除等于从尾到头移动的值的元素。
以下代码用于删除列表中第一次出现的值。
1 2 3 4 5 6 7 | @Test public void list7() { String[] strings = new String[]( "1" , "2" , "3" ); <br> redisTemplate.opsForList().rightPushAll( "list7" , strings); System.out.println(redisTemplate.opsForList().range( "list7" , 0 , - 1 )); <br><br> redisTemplate.opsForList().remove( "list7" , 1 , "2" ); //将删除列表中第一次出现的2 System.out.println(redisTemplate.opsForList().range( "list7" , 0 , - 1 )); ) |
运行测试,输出如下结果:
1 2 | [ 1 , 2 , 3 ] [ 1 , 3 ] |
8、V index(K key, long index)
根据下标获取列表中的值(下标从0开始),用法见以下代码:
1 2 3 4 5 6 | @Test public void list8() { String[] strings = new Stnng[]{ "1" , ” 2 ”, "3" }; <br> redisTemplate.opsForList().rightPushAII( "list8" , strings); System.out.println(redisTemplate.opsForList().range( "list8" , 0 ,- 1 )); System.out.println(redisTemplate.opsForList().index( "list8" , 2 )); ) |
运行测试,输出如下结果:
1 2 | [ 1 , 2 , 3 ] 3 |
9、V leftPop(K key)
弹出最左边的元素,弹出之后该值在列表中将不复存在,用法见以下代码:
1 2 3 4 5 6 7 | @Test public void list9() { String[] strings = new String[]{ "l" , "2" , "3" }; <br> redisTemplate.opsForList().rightPushAII( "list9" , strings);<br> System.out.println(redisTemplate.opsForList().range( "list9" , 0 ,- 1 )); System.out.println(redisTemplate.opsForList().leftPop( "list9" )); System.out.println(redisTemplate.opsForList().range(,,list9", 0 , - 1 )); } |
运行测试,输出如下结果:
1 2 3 | [ 1.2 . 3 ] 1 [ 2.3 ] |
10、V rightPop(K key)
弹出最右边的元素,弹岀之后该值在列表中将不复存在,用法见以下代码:
1 2 3 4 5 6 7 8 | @Test public void list10() { StringD strings = new String[]{ "1" , "2" , "H3" ); redisTemplate.opsForList().rightPushAll( "list10" , strings); System.out.println(redisTemplate.opsForList().range( "list10" , 0 , - 1 )); System.out.println(redisTemplate.opsForList().rightPop( "list10" )); System.out.println(redisTeinplate.opsForList().range( "list10" , 0 , - 1 )); ) |
运行测试,输出如下结果:
1 2 3 | [ 1 , 2 , 3 ] 3 [ 1 , 2 ].. |
11.4.5实例45:操作集合
set是存放不重复值的集合。利用set可以做全局去重的功能。还可以进行交集' 并集、差集等 操作,也可用来实现计算共同喜好、全部的喜好' 自己独有的喜好等功能。
Redis的set是string类型的无序集合,通过散列表实现。
本实例的源代码可以在“/ll/Redis”目录下找到。
下面介绍具体用法。
1、Long add(K key, V... values)
在无序集合中添加元素,返回添加个数,用法见以下代码:
1 2 3 4 5 6 7 | @Test public void Set1 () { String[] strs = new String[]{ "str1" , "str2" }; System.out.println(redisTemplate.opsForSet().add( "Set1" , strs)); //也可以直接在add中添加多个值 System.out.println(redisTemplate.opsForSet().add( "Set1" , "1" , "2" , "3" ); } |
运行测试,输出如下结果:
1 2 | 2 3 |
2、Long remove(K key, Object... values)
移除集合中一个或多个成员,用法见以下代码:
1 2 3 4 5 6 | @Test public void Set2() { String[] strs= new String[]{ "strl" , "str2" ); System.out.println(redisTemplate.opsForSet().add( "Set2" , strs)); System.out.println(redisTemplate.opsForSet().remove( "set2" ,strs)); } |
运行测试,输出如下结果:
1 2 | 2 0 |
3、V pop(K key)
移除并返回集合中的一个随机元素,用法见以下代码:
1 2 3 4 5 6 7 | @Test public void Set3() { String[] strs= new String[]( "str1" , "str2" }; System.out.println(redisTemplate.opsForSet().add( "Set3" , strs)); System.out.println(redisTemplate.opsForSet().pop( "Set3" )); System.out.println(redisTemplate.opsForSet().members( "Set3" )); } |
运行测试,输岀如下结果:
1 2 3 | 2 strl [str2] |
4、Boolean move(K key, V value, K destKey)
将member元素移动,用法见以下代码:
1 2 3 4 5 6 7 8 | @Test public void Set4() { String[] strs 二 new String[]( "str1" , "str2" }; System.out.println(redisTemplate.opsForSet().add( "Set4" , strs));<br> redisTemplate.opsForSet().move( "Set4" , "str2" , "Set4to2" ); System.out.println(redisTemplate.opsForSet().members( "Set4" )); System.out.println(redisTemplate.opsForSet().members( "Set4to2" )); } |
运行测试,输出如下结果:
1 2 3 4 5 | 2 [strl] [str2] |
5、Long size(K key)
获取无序集合的大小长度,用法见以下代码:
1 2 3 4 5 6 | @Test public void Set5() { String[] strs = new String[]{ "str1" , "str2" ); System.out.println(redisTemplate.opsForSet().add( "Set5" , strs)); System.out.println(redisTemplate.opsForSet().size( "Set5" )); } |
运行测试,输出如下结果:
1 2 | 2 2 |
6、Set<V> members(K key)
返回集合中的所有成员,用法见以下代码:
1 2 3 4 5 6 | @Test public void Set6() { String[] strs = new String[]{ "strl" , "str2" ); System.out.println(redisTemplate.opsForSet().add( "Set6" , strs)); System.out.println(redisTemplate.opsForSet().members( "Set6" )); } |
运行测试,输出如下结果:
1 2 | 2 [strl, str2] |
7、Cursor<V> scan(K key, ScanOptions options)
遍历Set,用法见以下代码:
1 2 3 4 5 6 7 8 | @Test public void Set7() { String[] strs = new Sfring[]{ "str1" , "str2" }; System.out.println(redisTemplate.opsForSet().add( "Set7" , strs)); Cursor<Object> curosr = redisTemplate.opsForSet().scan( "Set7" , ScanOptions.NONE); <br> while (curosr.hasNext()){ System.out.println(curosr.next()); } } |
运行测试,输出如下结果:
1 2 | strl str2 |
11.4.6实例46:操作有序集合
zset ( sorted set,有序集合)也是string类型元素的集合,且不允许重复的成员。每个元素 都会关联一个double类型的分数。可以通过分数将该集合中的成员从小到大进行排序。
zset的成员是唯一的,但权重参数分数(score )却可以重复。集合中的元素能够按score进 行排列。它可以用来做排行榜应用、 取TOP N、延时任务、范围查找等。
本实例的源代码可以在711/Redis"目录下找到。
下面介绍具体用法。
1、Long add(K key, Set<TypedTuple<V» tuples)
新增一个有序集合,用法见以下代码:
1 2 3 4 5 6 7 8 9 | @Test public void Zset1() { ZSetOperations.TypedTuple<Object> objectTypedTuplel = new DefaultTypedTuple<>( "zset-1" , 9.6 ); <br> ZSetOperations.TypedTuple<Object> objectTypedTuple2 = new DefaultTypedTuple<>( "zset-2" , 9.9 ); SeKZSetOperations.TypedTuple<Object>> tuples = new HashSet<ZSetOperations.TypedTuple<Object>>(); tuples.add(objectTypedTuplel); tuples.add(objectTypedTuple2);<br> System.out.println(redisTemplate.opsForZSet().add("zsetT,,tuples)); System.out.pnntln(redisTemplate.opsForZSet().range( "zset1" , 0 ,- 1 )); } |
运行测试,输岀如下结果:
1 2 | 2 [zset- 1 , zset- 5 , zset- 2 , zset- 6 ] |
2、Boolean add(K key, V value, double score)
新增一个有序集合,如果存在则返回false,如果不存在则返回true。用法见以下代码:
1 2 3 4 | @Test public void Zset2() { System.out.println(redisTemplate.opsForZSet().add( "zset2" , "zset-1" , 1.0 )); System.out.println(redisTemplate.opsForZSet().add( "zset2" , "zset-1" , 1.0 )); <br>} |
运行测试,输出如下结果:
True
false
3、Long remove(K key, Object... values)
从有序集合中移除一个或多个元素,用法见以下代码:
1 2 3 4 5 | @Test public void Zset3() { System.out.println(redisTemplate.opsForZSet()-add( "zset3" , "zset-1" , 1.0 )); <br> System.out.println(redisTemplate.opsForZSet().add( "zset3" , "zset-2" , 1.0 )); <br> System.out.println(redisTemplate.opsForZSet()-range( "zset3" , 0 - 1 )); <br> System.out.println(redisTemplate.opsForZSet().remove( "zset3" , "zset-2" )); System.out.println(redisTemplate.opsForZSet0.range( "zset3" , 0 - 1 )); ) |
运行测试,输出如下结果:
1 2 3 4 5 6 7 8 9 | true true [zset- 1 , zset- 2 ] 1 [zset-I] |
4、Long rank(K key, Object o)
返回有序集中指定成员的排名,按分数值递增排列,用法见以下代码:
1 2 3 4 5 | @Test public void Zset4() { System.out.println(redisTemplate.opsForZSet().add( "zset4" , "zset-1" , 1.0 )); <br> System.out.println(redisTemplate.opsForZSet().add( "zset4" , "zset-2" , 1.0 )); System.out.println(redisTemplate.opsForZSet().range( "zset4" , 0 ,- 1 )); <br> System.out.println(redisTemplate.opsForZSet().rank( "zset4" ,"zset- 1 ')); ) |
运行测试,输出如下结果:
1 2 3 4 5 6 7 | true true [zset- 1 , zset- 2 ] 0 |
结果中的0表示排名第一。
5、Set<V> range(K key, long start, long end)
通过索引区间返回有序集合指定区间内的成员,按分数值递增排列,用法见以下代码:
1 2 3 4 5 6 7 8 9 | @Test public void Zset5() { ZSetOperations.TypedTuple<Object> objectTypedTuplel = new DefaultTypedTuple<>( "zset-1" , 9.6 ); ZSetOperations.TypedTuple<Object> objectTypedTuple2 = new DefaultTypedTuple<>( "zset-2" , 9.1 ); <br> <br> Set<ZSetOperations.TypedTuple<Object» tuples = new HashSet<ZSetOperations.TypedTuple<Object»(); tuples.add(objectTypedTuplel); tuples.add(objectTypedTuple2); System.out.println(redisTemplate.opsForZSet().add( "zset5" ,tuples)); System.out.println(redisTemplate.opsForZSet().range( "zset5" , 0 ,- 1 )); ) |
运行测试,输出如下结果:
1 2 3 | 0 [zset- 2 , zset- 1 ] |
6、Long count(K key, double min, double max)
通过分数返回有序集合指定区间内的成员个数,用法见以下代码:
1 2 3 4 5 6 7 8 9 10 11 12 | @Test public void Zset6() { ZSetOperations.T ypedT uple<Object> objectTypedTuplel = new DefaultTypedTuple<>( "zset-1" , 3.6 ); ZSetOperations.TypedTuple<Object> obje'ctTypedTuple2 = new DefaultTypedTuple<>( "zset-2" , 4.1 ); ZSetOperations.T ypedT uple<Object> objectTypedT uple3 = new DefaultTypedTupleO( "zset-3" , 5.7 ); <br><br> Set<ZSetOperations.TypedTuple<Object» tuples = new HashSet<ZSetOperations.TypedTuple<Object»(); tuples.add(objectTypedTuplel); tuples.add(objectTypedTuple2); tuples.add(objectTypedTuple3); System.out.println(redisTemplate.opsForZSet0.add( "zset6" ,tuples)); System.out.println(redisTemplate.opsForZSet().rangeByScore( "zset6" , 0 , 9 )); System.out.println(redisTemplate.opsForZSet().count( "zset6" , 0 , 5 )); } |
运行测试,输出如下结果:
1 2 3 4 5 | 1 [zset- 1 , zset- 2 , zset- 3 ] 2 |
7、Long size(K key)
获取有序集合的成员数,用法见以下代码:
1 2 3 4 5 6 7 8 9 10 | @Test public void Zset7() { ZSetOperations.TypedTuple<Object> objectTypedTuplel = new DefaultTypedTuple<>( "zset-1 " , 3.6 ); ZSetOperations.TypedTuple<Object> objectTypedTuple2 = new DefauItTypedTuple<>( "zset-2" , 4.1 ); ZSetOperations.T ypedT uple<Object> objectTypedTuple3 = new DefaultTypedTupleO( "zset-3" , 5.7 ); <br><br> Set<ZSetOperations.TypedTuple<Object» tuples = new HashSeKZSetOperations.TypedTuple <Object»(); tuples.add(objectTypedTuplel); tuples.add(objectTypedTuple2); tuples.add(objectTypedTuple3); System.out.println(redisTemplate.opsForZSet().add( "zset7" ,tuples)); System.out.println(redisTemplate.opsForZSet().size( "zset7" )); |
运行测试,输出如下结果:
8、Double score(K key, Object o)
获取指定成员的score值,用法见以下代码:
1 2 3 4 5 6 7 8 | @Test public void Zset8() ( ZSetOperations.TypedTuple<Object> objectTypedTuplel = new DefaultTypedTupleO( "zset-1" , 3.6 ); <br><br> ZSetOperations.TypedT uple<Object> objectTypedTuple2 = new DefauItT ypedTuple<>( "zset-2" , 4.1 ); <br> ZSetOperations.TypedTuple<Object> objectTypedTuple3 = new DefaultTypedTuple<>C'zset- 3 ", 5.7 ); <br> Set<ZSetOperations.TypedTuple<Object» tuples = new HashSet<ZSetOperations.TypedTuple <Object»(); tuples.add(objectTypedT uplel); tuples.add(objectTypedT uple2); tuples.add(objectTypedTuple3); System.out.pnntln(redisTeiYiplate.opsForZSet().add( "zset7" ,tuples)); System.out.println(redisTemplate.opsForZSet().score( "zset7" , "zset-3" ));<br>} |
运行测试,输岀如下结果:
5.7
9、Long removeRange(K key, long start, long end)
移除指定索引位置的成员,有序集成员按分数值递增排列,用法见以下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | @Test public void Zset9() ( ZSetOperations.TypedTuple<Object> objectTypedTuplel = new DefaultTypedTupleO( "zset-1" , 3.6 ); ZSetOperations.TypedTuple<Object> objectTypedTuple2 = new DefaultTypedTuple<>( "zset-2" , 5.1 ); ZSetOperations.TypedTuple<Object> objectTypedTuple3 = new Default?ypedTuple<>( "zset-3" , 2.7 ); Set<ZSetOperations.TypedTuple<Object» tuples = new HashSet<ZSetOperations.TypedTuple <Object»(); tuples.add(objectTypedTuplel); tuples.add(objectTypedTuple2); tuples.add(objectTypedTuple3); System.out.println(redisTemplate.opsForZSet().add( "zset9" ,tuples)); System.out.println(redisTemplate.opsForZSet().range( "zset9" , 0 ,- 1 )); System.out.println(redisTemplate.opsForZSet().removeRange( "zset9" , 1 , 2 )); System.out.println(redisTemplate.opsForZSet().range( "zset9" , 0 - 1 )); } |
运行测试,输出如下结果:
2
[zset-3, zset-1, zset-2]
2
[zset-3]
10、Cursor<TypedTuple<V» scan(K key, ScanOptions options)
遍历zset,用法见以下代码:
1 2 3 4 5 6 7 8 9 10 11 | @Test public void Zset10() { ZSetOperations.TypedTuple<Object> objectTypedTuplel = new DefaultTypedTuple<>( "zset-1" , 3.6 ); <br> ZSetOperations.TypedTuple<Object> objectTypedTuple2 = new DefaultTypedTuple<>( "zset-2" , 5.1 ); <br> ZSetOperations.TypedTuple<Object> objectTypedTuple3 = new DefaultTypedTuple<>( "zset-3" , 2.7 ); <br> Set<ZSetOperations.TypedTuple<Object» tuples = new HashSet<ZSetOperations.TypedTuple <Object»(); tuples.add(objectTypedTuplel); tuples.add(objectTypedTuple2); tuples.add(objectTypedTuple3); System.out.println(redisTemplate.opsForZSet().add( "zset10" , tuples)); Cursor<ZSetOperations.TypedTuple<Object» cursor = redisTemplate.opsForZSet().scan( "zset10" , ScanOptions.NONE); while (cursor.hasNext()) { ZSetOperations.TypedTuple<Object> item = cursor.next(); System.out.println(item.getValue() + ":" + item.getScore());<br> }<br>} |
运行测试,输出如下结果:
1 2 3 4 5 6 7 | 3 zset- 3 : 2.7 zset-l: 3.6 zset- 2 : 5.1 |
除使用opsForXXX方法外,还可以使用Execute方法。opsForXXX方法的底层,是通过调 用Execute方法来实现的。opsForXXX方法实际上是封装了 Execute方法,定义了序列化,以便 使用起来更简单便捷。
11.4.7 比较 RedisTemplate 和 StringRedisTemplate
StringRedisTemplate继承于RedisTemplate,两者的数据是不相通的。
- StringRedisTemplate 只能管理 StringRedisTemplate 中的数据。
- RedisTemplate 只能管理 RedisTemplate 中的数据。
StringRedisTemplate默认采用的是string的序列化策略,RedisTemplate默认采用的是 JDK的序列化策略。
11.5实例47:用Redis和MyBatis完成缓存数据的增加、删除、 修改、查询功能
本实例使用Redis、MyBati和MySQL来实现数据的增加、删除、修改和查询功能。 善® O爆本实例的源代码可以在"/11/RedisCURD"目录下找到。
11.5.1 在 Spring Boot 中集成 Redis
(1)完成配置基础项。
添加 Redis、MySQL、MyBatis 依束负。
(2) 配置MySQL、Redis服务器,可以直接在application.properties文件中进行配置,具 体配置方法见以下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | spring.datasource.url=jdbc:mysql: //127.0.0.1/book?useUnicode=true&characterEncoding=utf-8&serverTi mezone=UTC&useSSL=true spring.datasource.usemame=root spring.datasource.password=root spring.datasource.driver- class -name=com.mysql.jdbc.Driver<br> spring.jpa.properties.hibernate.hbm2ddl.auto=update spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5lnnoDBDialect spring.jpa.show-sql= true <br> #Redis数据库索引(默认为 0 ) spring.redis.database= 0 #Redis服务器地址 spring.redis.host= 127.0 . 0.1 #Redis服务器连接端口 spring.redis.port= 6379 #Redis服务器连接密码(默认为空) spring.redis.password= #连接池最大连接数(使用负值表示没有限制) spring.redis.pool.max-active= 8 #连接池最大阻塞等待时间(使用负值表示没有限制) spring.redis.pool.max-wait=- 1 #连接池中的最大空闲连接 spring.redis.pool.max-idle= 8 #连接池中的最小空闲连接 spring.redis.pool.min-idle= 0 #连接超时时间(ms) spring.redis.timeout= 5000 |
(3) 在入口类加上@EnableCaching注解,开启缓存支持。
11.5.2 配置 Redis 类
要想启用Spring缓存支持,需创建一个CacheManager的Bean。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | ©Configuration public class RedisConfig extends CachingConfigurerSupport { //在缓存对象集合中,缓存是以key-value形式保存的 //如果没有指定缓存的key,则Spring Boot会使用SimpleKeyGenerator生成key<br> @Bean public KeyGenerator keyGenerator() { return new KeyGenerator() {<br><br> @Override //定义缓存数据key的生成策略 public Object generate(Object target, Method method, Object... params) { <br> StringBuilder sb = new StringBuilder(); <br> sb.append(target.getClass().getName()); <br> sb.append(method.getName()); <br> for (Object obj: params) { sb.append(obj.toString()); } return sb.toStringO; } ); }<br><br> @SuppressWarnings ( "rawtypes" ) @Bean //缓存管理器2.x版本 public CacheManager cacheManager(RedisConnectionFactory connectionFactory) {<br> RedisCacheManager cacheManager 二 RedisCacheManager.create(connectionFactory); <br> return cacheManager; } <br> /** @Bean 1 .x版本,Spring Bootl.x版本请用下面的缓存管理器启用支持 public CacheManager cacheManager(@SuppressWarnings("rawtypes") RedisTemplate redisTemplate) { return new RedisCacheManager(redisTemplate); }*/ <br> //注册成Bean被spring管理,如果没有这个Bean,则Redis可视化工具中的中文内容(key或value)都会以二 进制存储,不易检查 @Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) { <br> RedisTemplate<String, Object> redisTemplate = new RedisTemplate<String, Object>(); <br> redisTemplate.setConnectionFactory(factory); return redisTemplate; }<br> @Bean public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory factory) { <br> StringRedisTemplate stringRedisTemplate = new StringRedisTemplate(); <br> StringRedisTemplate.setConnectionFactory(factory); return StringRedisTemplate; } } |
11.5.3创建测试实体类
创建用于数据操作的测试实体类,见以下代码:
1 2 3 4 5 6 | @Data public class User implements Serializable { private String id; private String name; private int age; } |
本实例采用MyBatis方式操作数据库。如果是读者自己输入代码,则需要手动创建好 数据库表,否则可以下载本实例,实例中会自动创建数据表,或参考8.9.2节实现建表自 动化。
11.5.4实现实体和数据表的映射关系
这里实现实体和数据表的映射关系,具体用法见以下代码:
1 2 3 4 5 6 7 8 9 10 11 | ©Repository @Mapper public interface UserMapper { @lnsert ( "insert into user(name,age) values(#(name),#{age})" ) int addUser( @Param ( "name" )String name, @Param ( "age" )Sring age);<br> @Select ( "select * from user where id =#(id}" ) User findByld( @Param ( "id" ) String id);<br> @Update ( "update user set name=#(name},age=#(age} where id=#{id}" ) <br> int updateByld(User user);<br> @Delete ( "delete from user where id=#(id}" ) void deleteByld( @Param ( "id" )String id); } |
可以看到id值需要string类型。
11.5.5创建Redis缓存服务层
创建Redis缓存服务层,即缓存在服务层工作,具体用法见以下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | <strong>©Service </strong><br><strong> @CacheConfig (cacheNames = "users" )</strong> <br> public class UserService {<br> @Autowired UserMapper userMapper; <br><br><strong> @Cacheable (key = "#p0" ) </strong><br> public User selectUser(String id){ System.out.println("selectH); return userMapper.findByld(id); }<br> <strong> @CachePut (key = "#p0" ) </strong><br> public void updataByld(User user){ System.out.println( "update" ); <br> userMapper.updateByld(user); }<br><br> //如果allEntries指定为true,则调用CacheEvict方法后将立即清空所有缓存 <strong> @CacheEvict (key = "#pO" ,allEntries= true ) </strong> public void deleteByld(String id){ System.out.println( "delete" ); userMapper.deleteByld(id); } } |
代码解释如下。
- @Cacheable:将查询结果缓存到Redis中。
- key="#p0":指定传入的第1个参数作为Redis的key。
- @CachePut:指定key,将更新的结果同步到Redis中。
- @CacheEvict:指定key,删除缓存数据。
- allEntries=true:方法调用后将立即清除缓存。
11.5.6完成增加、删除、修改和查询测试API
增加、删除、修改和查询测试API,具体用法见以下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | @RestController @RequestMapping ( "/user" ) public class RedisController { <br> @Autowired UserService userService;<br> @RequestMapping ( "/{id}" ) public User ForTest( @PathVariable ( "id" ) String id){ return userService.selectUser(id); }<br> @RequestMapping ( "/update/" ) public String update(User user)( userService.updataByld(user); return "success" ; }<br> @RequestMapping ( "/delete/{id}" ) public String delete ( @PathVariable String id)( userService.deleteByld(id); return "delete success" ; } } |
启动项目,多次访问http://localhost:8080/user/1 ;第一次时控制台会出现select信息,代表 对数据库进行了查询操作。
后面再访问时则不会出现提示,表示没有对数据库执行操作,而是使用 Redis中的缓存数据。
11.6实例48:用Redis和JPA实现缓存文章和点击量
本实例用Redis、JPA和MySQL实现缓存文章和点击量。
本实例的源代码可以在"/H/JpaArticleRedisDemo”目录下找到。
11.6.1实现缓存文章
(1) 实现服务层的缓存设置,用法见以下代码:
1 2 3 4 5 6 7 8 9 10 | ©Service <strong> @CacheConfig (cacheNames = "articleservice" ) </strong><br> public class ArticleServicelmpI implements ArticleService { <br> @Autowired private ArticleRepository articleRepository;<br> @Override <strong> @Cacheable (key = "#p0" ) </strong> public Article findArticleByld( long id) { return articleRepository.findByld(id); } } |
(2) 实现控制器,用法见以下代码:
1 2 3 4 5 6 7 8 | @Autowired private ArticleService articleService;<br> @RequestMapping ( "/{id}" ) public ModelAndView testPathVanable( @PathVariable ( "id" ) Integer id) { Article articles = articleService.findArticleByld(id); ModelAndView mav 二 new ModelAndView( "web/article/show" ); <br> mav.addObject( "article" , articles); return mav; } |
(3) 在入口类开启注解@EnableCaching,支持缓存。
11.6.2实现统计点击量
如果要实时更新文章的点击量,对数据库进行修改操作,则会导致读写频繁。所以,一般采取 Redis缓存,每访问一次都是HRedis中増加1次,待到某个时刻再同步到MySQL数据库中。
可以在控制器中加入以下代码来实现。
1 | stringRedisTemplate.boundValueOps( "name::" + id).increment( 1 ); //val +1 |
控制器的最终代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | /** * Description:根据id获取文章对象 */ @GetMapping ( "/{id}" ) public ModelAndView getArticle( @PathVariable ( "id" ) Integer id) throws Exception { Article articles = articleService.findArticleByld(id); if (articles.getView() > 0 ) { //val +1 stringRedisTemplate.boundValueOps( "name::" + id).increment(articles.getView() + 1 ); } else {<br> //val +1 <br> stringRedisTemplate.boundValueOps("name::" + id).increment(l); } <br> ModelAndView mav = new ModelAndView( "article/show" ); mav.addObject( "article" , articles); return mav; } |
下面来编写定时任务,在特定的时间点完成点击量的Redis和MySQL数据库同步。
11.6.3实现定时同步
点击量平时都是存储在Redis中的,需要在某个时间点更新到MySQL数据库。我们可以通过 实现一个定时任务来完成。使用定时任务需要开启支持,
请在入口类加上注解 @EnableScheduling。同步的具体实现见以下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | ©Component public class CountScheduledTasks { <br> @Autowired private ArticleRepository articleRepository;<br> @Autowired private StringRedisTemplate stringRedisTemplate;<br> @Scheduled (cron = ” 0 00 2 ? * * ”)<br> //每天 4 : 30 执行 public void syncPostViews() { Long startTime = System.nanoTime(); List dtoList = new ArrayList<>(); Set<String> keySet = stringRedisTemplate.keys( "name::*" ); for (String key: keySet) { String views = stringRedisTemplate.opsForValue().get(key); String sid = key.replaceAII( "name::" , "" );<br> long lid = Long.parseLong(sid); <br> long lviews = Long.parseLong(views); //批量更新可以用Collection<?>实现 <br> articleRepository.updateArticleViewByld(lviews, lid); <br> stringRedisTemplate.delete(key); } } } |
11.7 实例49:实现分布式Session
在分布式环境中,我们经常会遇到这样的场景:用户的登录请求经过Nginx转发到A服务器(会 员服务器)进行验证登录,下一步的操作(如查看新闻)转发到了 B服务器(新闻服务器)进行请 求。
如果没做Session共享,则用户信息只存储在A服务器上的Web容器中,B服务器是识别不 了这个用户的,这会需要用户重新登录。
这种场景下就需要进行Session共享,以便不用重复登录, 使用Redis来实现是非常好的方式。
实例的源代码可以在“/11/RdiesSession”目录下找到。
11.7.1 用 Redis 实现 Session 共享
Spring Boot封装了 Redis下使用分布式Session的功能,可以直接来使用。下面是实现步骤。
(1)在入口类中添加@EnableRedisHttpSession (见以下代码),以开启分布式Session支 持。或在Redis配置类中启用。
1 2 3 4 | ©Configuration <strong> @EnableRedisHttpSession </strong> public class RedisSessionConfig ( } |
(2)添加 Spring Boot 提供的 spring-session-data-redis 依赖,支持 Redis 实现 Session 共享。依赖见以下代码:
1 2 3 4 5 6 7 | <dependency> <groupld>org.springframework.boot</groupld> <br> <artifactld>spring-boot-starter-data-redis</artifactld> </dependency> <dependency> <groupld>org.springframework.session</groupld> <artifactld>spring-session-data-redis</artifactld> </dependency> |
上面两个依赖都需要添加,第1个支持Redis,第2个是用Spring Boot实现Redis下的 Session分布式共享。
(3) 编写测试控制器,见以下代码:
1 2 3 4 5 6 | @GetMapping ( "/session" ) public Map<String, Object> sessionTest(HttpServletRequest request) { Map<String, Object> map = new HashMap<>(); map.put( "sessionld" , request.getSession().getld()); return map; ) |
(4) 配置集群服务器。
读者可参考本书的4.3.3节多环境配置中的方法,配置两个配置文件,把服务器端口分别改为 8080和8081,然后把Spring Boot项目打包之后(打包参考本书的3.2.4节的相关介绍),运行 两个服务器端。运行代码如下:
1 2 | java -jar name.jar -spring.profiles.active=dev java -jar name.jar —spring.profiles.active=pro |
再访问 http://localhost:8080/session 和 http://localhost:8081/session o
可以看到,两个URL地址的Session -样了。当然,在实际生产环境中域名端口通常是一样 的,这里使用不同端口是为了在本机模拟分布式环境测试。
11.7.2配置Nginx实现负载均衡
11.7.1节已经实现了分布式Session共享,但使用了不同的端口。
在生产环境中,会使用不同 IP地址来区别集群中的服务器,所以,需要配置Nginx服务器,以达到无缝切换,让客户感受不到 切换到了不同的服务器。
要配置Nginx的服务器集群,只需要修改Nginx的配置文件。具体配置见以下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | #服务器集群配置 upstream eg.com { #服务器集群名字 server 127.0 . 0.1 : 18080 weight= 1 ; #服务器配置weight是权重的意思,权重越大,分配的概率越大 server 127.0 . 0.1 : 8081 weight= 2 ; }<br> #Nginx的配置 server { listen 80 ; #监听 80 端口,可以改成其他端口 server_name localhost; #当前服务的域名 location /{ proxy_pass http: //eg.com; proxy_redirect default ; }<br> error_page 500 502 503 504 /50x.html;<br> location = /50x.html { root html; } |
在生产环境中,可能需要更进一步的配置。配置完成之后,可以登录会员系统测试效果。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!