16、数据共享(session共享)
既然Redis能持久化数据,就可以用它来实现模块间的数据共享;SpringBoot Session 利用的这个机制来实现 Session 共享;
-
-
依赖 <dependency> <groupId>org.springframework.session</groupId> <artifactId>spring-session-data-redis</artifactId> </dependency>
-
开启session共享 @Configuration @EnableRedisHttpSession public class RedisSessionConfig { }
测试代码
package com.ehang.redis.controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import javax.servlet.http.HttpServletRequest; import java.util.Enumeration; import java.util.HashMap; import java.util.Map; /** * @author session 共享 * @title: RedisSessionController * @projectName ehang-spring-boot * @description: TODO * @date 2022/8/5 15:58 */ @RestController @RequestMapping("session") public class RedisSessionController { /** * 设置session的值 * @param request * @return */ @GetMapping("set") public Map set(HttpServletRequest request) { String id = request.getSession().getId(); Map<String, String> vas = new HashMap<>(); String key = "key"; String value = "value"; vas.put("id", id); vas.put(key, value); // 自定义session的值 request.getSession().setAttribute(key, value); return vas; } /** * 获取session的值 * @param request * @return */ @GetMapping("get") public Map get(HttpServletRequest request) { Map<String, Object> vas = new HashMap<>(); // 遍历所有的session值 Enumeration<String> attributeNames = request.getSession().getAttributeNames(); while (attributeNames.hasMoreElements()) { String k = attributeNames.nextElement(); Object va = request.getSession().getAttribute(k); vas.put(k, va); } vas.put("id", request.getSession().getId()); return vas; } }
- 测试
- 开启两个服务,分别接听8080和8081,8080调用赋值接口,8081调用获取接口,如下图,可以看到,两个服务共享了一份Session数据;
-
Redis中保存的数据
127.0.0.1:6379> keys spring:* 1) "spring:session:sessions:expires:6f1d7d53-fe01-4e80-9e6a-5ff54fffa92a" 2) "spring:session:expirations:1659688680000" 3) "spring:session:sessions:6f1d7d53-fe01-4e80-9e6a-5ff54fffa92a"
17、商品筛选
商城类的应用,都会有类似于下图的一个商品筛选的功能,来帮用户快速搜索理想的商品;
假如现在iphone 100 、华为mate 5000 已发布,在各大商城上线;下面就通过 Redis 的 set 来实现上述的商品筛选功能;
功能所需命令
-
SADD key member [member …]:添加一个或多个元素 -
SINTER key [key …]:返回给定所有集合的交集
Redis-cli 客户端测试
# 将iphone100 添加到品牌为苹果的集合 127.0.0.1:6379> sadd brand:apple iphone100 (integer) 1 # 将meta5000 添加到品牌为苹果的集合 127.0.0.1:6379> sadd brand:huawei meta5000 (integer) 1 # 将 meta5000 iphone100 添加到支持5T内存的集合 127.0.0.1:6379> sadd ram:5t iphone100 meta5000 (integer) 2 # 将 meta5000 添加到支持10T内存的集合 127.0.0.1:6379> sadd ram:10t meta5000 (integer) 1 # 将 iphone100 添加到操作系统是iOS的集合 127.0.0.1:6379> sadd os:ios iphone100 (integer) 1 # 将 meta5000 添加到操作系统是Android的集合 127.0.0.1:6379> sadd os:android meta5000 (integer) 1 # 将 iphone100 meta5000 添加到屏幕为6.0-6.29的集合中 127.0.0.1:6379> sadd screensize:6.0-6.29 iphone100 meta5000 (integer) 2 # 筛选内存5T、屏幕在6.0-6.29的机型 127.0.0.1:6379> sinter ram:5t screensize:6.0-6.29 1) "meta5000" 2) "iphone100" # 筛选内存10T、屏幕在6.0-6.29的机型 127.0.0.1:6379> sinter ram:10t screensize:6.0-6.29 1) "meta5000" # 筛选内存5T、系统为iOS的机型 127.0.0.1:6379> sinter ram:5t screensize:6.0-6.29 os:ios 1) "iphone100" # 筛选内存5T、屏幕在6.0-6.29、品牌是华为的机型 127.0.0.1:6379> sinter ram:5t screensize:6.0-6.29 brand:huawei 1) "meta5000"
18、购物车
商品缓存
电商项目中,商品消息,都会做缓存处理,特别是热门商品,访问用户比较多,由于商品的结果比较复杂,店铺信息,产品信息,标题、描述、详情图,封面图;为了方便管理和操作,一般都会采用 Hash 的方式来存储(key为商品ID,field用来保存各项参数,value保存对于的值)
购物车
当商品信息做了缓存,购物车需要做的,就是通过Hash记录商品ID,以及需要购买的数量(其中key为用户信息,field为商品ID,value用来记录购买的数量) ;
功能所需命令
-
HSET key field value : 将哈希表 key 中的字段 field 的值设为 value ; -
HMSET key field1 value1 [field2 value2 ] :同时将多个 field-value (域-值)对设置到哈希表 key 中。 -
HGET key field:获取存储在哈希表中指定字段的值。 -
HGETALL key :获取在哈希表中指定 key 的所有字段和值 -
HINCRBY key field increment :为哈希表 key 中的指定字段的整数值加上增量 increment 。 -
HLEN key:获取哈希表中字段的数量
Redis-cli 客户端测试
# 购物车添加单个商品 127.0.0.1:6379> HSET sc:u1 c001 1 (integer) 1 # 购物车添加多个商品 127.0.0.1:6379> HMSET sc:u1 c002 1 coo3 2 OK # 添加商品购买数量 127.0.0.1:6379> HINCRBY sc:u1 c002 1 (integer) 2 # 减少商品的购买数量 127.0.0.1:6379> HINCRBY sc:u1 c003 -1 (integer) 1 # 获取单个的购买数量 127.0.0.1:6379> HGET sc:u1 c002 "2" # 获取购物车的商品数量 127.0.0.1:6379> HLEN sc:u1 (integer) 3 # 购物车详情 127.0.0.1:6379> HGETALL sc:u1 1) "c001" 2) "1" 3) "c002" 4) "2" 5) "coo3" 6) "2"
19、定时取消订单(key过期监听)
电商类的业务,一般都会有订单30分钟不支付,自动取消的功能,此时就需要用到定时任务框架,Quartz、xxl-job、elastic-job 是比较常用的 Java 定时任务;我们也可以通过 Redis 的定时过期、以及过期key的监听,来实现订单的取消功能;
-
Redis key 过期提醒配置修改 redis 相关事件配置。找到 redis 配置文件 redis.conf,查看 notify-keyspace-events 配置项,如果没有,添加 notify-keyspace-events Ex,如果有值,则追加 Ex,相关参数说明如下: -
K
:keyspace 事件,事件以 keyspace@ 为前缀进行发布 -
E
:keyevent 事件,事件以 keyevent@ 为前缀进行发布 -
g
:一般性的,非特定类型的命令,比如del,expire,rename等 -
$
:字符串特定命令 -
l
:列表特定命令 -
s
:集合特定命令 -
h
:哈希特定命令 -
z
:有序集合特定命令 -
x
:过期事件,当某个键过期并删除时会产生该事件 -
e
:驱逐事件,当某个键因 maxmemore 策略而被删除时,产生该事件 -
A
:g$lshzxe的别名,因此”AKE”意味着所有事件
-
-
添加RedisKeyExpirationListener的监听 import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.listener.RedisMessageListenerContainer; @Configuration public class RedisListenerConfig { @Bean RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory) { RedisMessageListenerContainer container = new RedisMessageListenerContainer(); container.setConnectionFactory(connectionFactory); return container; } }
KeyExpirationEventMessageListener
接口监听所有 db 的过期事件keyevent@*:expired"
-
package com.ehang.redis.config; import lombok.extern.slf4j.Slf4j; import org.springframework.data.redis.connection.Message; import org.springframework.data.redis.listener.KeyExpirationEventMessageListener; import org.springframework.data.redis.listener.RedisMessageListenerContainer; import org.springframework.stereotype.Component; /** * 监听所有db的过期事件__keyevent@*__:expired" * * @author 一行Java * @title: RedisKeyExpirationListener * @projectName ehang-spring-boot * @description: TODO * @date 2022/8/5 16:36 */ @Component @Slf4j public class RedisKeyExpirationListener extends KeyExpirationEventMessageListener { public RedisKeyExpirationListener(RedisMessageListenerContainer listenerContainer) { super(listenerContainer); } /** * 针对 redis 数据失效事件,进行数据处理 * * @param message * @param pattern */ @Override public void onMessage(Message message, byte[] pattern) { // 获取到失效的 key,进行取消订单业务处理 // 由于这里是监听了所有的key,如果只处理特定数据的话,需要做特殊处理 String expiredKey = message.toString(); log.info("过期的Key:{}", expiredKey); } }
测试
为了快速验证效果,这里 将过期时间调整为2秒;
注意,由于过期之后,Redis中的Key已经不存在了,因此,一定要将订单号作为key,不能作为值保存,否则监听到过期Key之后,将拿不到过期的订单号;
-
不推荐使用基于这一套机制,确实能够实现订单的超时取消,但是还是不太建议使用,这里仅作为一个思路;原因主要有以下几个: -
redis 的过期删除策略是采用定时离线扫描,或者访问时懒性检测删除,并没有办法保证时效性,有可能key已经到期了,但Redis并没有扫描到,导致通知的延迟; -
消息发送即忘(fire and forget),并不会保证消息的可达性,如果此时服务不在线或者异常,通知就再也收不到了;
-
20物流信息(时间线)
寄快递、网购的时候,查询物流信息,都会给我们展示xxx时候,快递到达什么地方了,这就是一个典型的时间线列表;
数据库的做法,就是每次变更就插入一条带时间的信息记录,然后根据时间和ID(ID是必须的,如果出现两个相同的时间,单纯时间排序,会造成顺序不对),来排序生成时间线;
我们也可以通过 Redis 的 List 来实现时间线功能,由于 List 采用的是双向链表,因此升序,降序的时间线都能正常满足;
-
RPUSH key value1 [value2]:在列表中添加一个或多个值,(升序时间线) -
LPUSH key value1 [value2]:将一个或多个值插入到列表头部(降序时间线) -
LRANGE key start stop:获取列表指定范围内的元素
Redis-cli 客户端测试
升序
127.0.0.1:6379> RPUSH time:line:asc 20220805170000 (integer) 1 127.0.0.1:6379> RPUSH time:line:asc 20220805170001 (integer) 2 127.0.0.1:6379> RPUSH time:line:asc 20220805170002 (integer) 3 127.0.0.1:6379> LRANGE time:line:asc 0 -1 1) "20220805170000" 2) "20220805170001" 3) "20220805170002"
降序
127.0.0.1:6379> LPUSH time:line:desc 20220805170000 (integer) 1 127.0.0.1:6379> LPUSH time:line:desc 20220805170001 (integer) 2 127.0.0.1:6379> LPUSH time:line:desc 20220805170002 (integer) 3 127.0.0.1:6379> LRANGE time:line:desc 0 -1 1) "20220805170002" 2) "20220805170001" 3) "20220805170000"
好了,关于Redis 的妙用,就介绍到这里;有了这些个场景的运用,下次再有面试官问你,Redis除了缓存还做过什么,相信聊上个1把小时,应该不成问题了。
文章评论