使用SpringData Redis处理秒杀业务中的过期数据

149 阅读5分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第17天,点击查看活动详情

作者平台:

| CSDN:blog.csdn.net/qq_4115394…

| 掘金:juejin.cn/user/651387…

| 知乎:www.zhihu.com/people/1024…

| GitHub:github.com/JiangXia-10…

| 微信公众号:1024笔记

本文一共2438字,预计阅读7分钟

前言

最近在一个业务中遇到一个限时秒杀的场景,就是规定时间内抢名额的问题。秒杀场景在很多公司或者系统中都有使用。就是对于某个业务场景的时间限制,比如,填写了一个问卷、下了一个订单,超过一定的时间内没有提交,那么就会失效。这种场景业务也是比较常见的一个业务之一了。

解决方案

这种情况有两种解决方案,一种是使用定时任务,定时扫描数据库中过期的数据,然后进行修改,但是为了数据的精确性和对时间的控制,这样扫描的间隔是比较短的,从而就会导致对数据库的频繁访问,造成了数据库压力。所以这种方案在一定的业务背景下不是很可取,因为一般来说设置过期时间的场景很多都是类似于秒杀的业务,这样的需要之一对于数据库的要求肯定是比较大的。

那么另一种情况就是使用消息通知,如果数据失效就会发送消息,程序接受到了消息就会对数据库进行修改,比如逻辑删除或者直接物理删除过期数据,这种情况对于数据库的访问压力就比较小了。

这里需要使用到的是redis的订阅与发布。Redis订阅发布一种消息通信模式:发送者发送消息,订阅者接收消息。Redis客户端可以订阅任意数量的频道。当有新消息通过PUBLISH命令发送给频道时, 这个消息就会被发送给订阅它的几个客户端。

正文

使用redis解决过期问题,这里主要涉及到的API是Jedis和RedisTemplate。

Jedis是Redis官方推出的一款面向Java的客户端,提供了很多供Java语言调用的接口。

SpringData Redis是Spring官方推出的,可以理解是Spring框架集成了Redis操作的一个子框架,并且封装了很多Redis的命令,所以可以很方便的使用Spring操作Redis数据库。

image.png

image.png

首先需要引入Spring相关的依赖,具体如下:

                <!-- spring data redis相关依赖-->
		<dependency>
			<groupId>redis.clients</groupId>
			<artifactId>jedis</artifactId>
			<version>2.6.2</version>
		</dependency>
		<dependency>
			<groupId>org.apache.commons</groupId>
			<artifactId>commons-pool2</artifactId>
			<version>2.2</version>
		</dependency>
		<dependency>
			<groupId>org.springframework.data</groupId>
			<artifactId>spring-data-redis</artifactId>
		</dependency>

		<!-- spring 相关依赖-->
		<dependency>
			<groupId>org.aspectj</groupId>
			<artifactId>aspectjweaver</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-aop</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-context</artifactId>
		</dependency>
		
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-orm</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-beans</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-core</artifactId>
		</dependency>
		 <!-- mybatis相关依赖-->
		<dependency>
			<groupId>org.mybatis</groupId>
			<artifactId>mybatis</artifactId>
			<version>3.4.5</version>
		</dependency>
		
		<!-- mybatis整合spring -->
		<dependency>
                   <groupId>org.mybatis</groupId>
                   <artifactId>mybatis-spring</artifactId>
                   <version>1.3.1</version>
                </dependency>
                
		<!-- 数据库相关依赖-->
		<dependency>
			<groupId>com.alibaba</groupId>
			<artifactId>druid</artifactId>
		</dependency>
		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
		</dependency>
               

然后新建一个xml文件,配置SpringData Redis,可以参考如下配置

<bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate">
  <property name="connectionFactory" ref="connectionFactory"></property>
  <property name="keySerializer">

<bean class="org.springframework.data.redis.serializer.StringRedisSerializer"></bean>	
   </property>
   <property name="valueSerializer">

<bean class="org.springframework.data.redis.serializer.StringRedisSerializer"></bean>
		</property>
	</bean>
	
	<!-- jedis连接工厂 -->
	<bean id="connectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
		<property name="hostName" value="127.0.0.1"></property>
		<property name="port" value="6379"></property>
		<property name="database" value="0"></property>
		<property name="poolConfig" ref="poolConfig"></property>
	</bean>
	
	<!-- jedis连接池的配置信息 -->
	<bean id="poolConfig" class="redis.clients.jedis.JedisPoolConfig">
		<property name="maxIdle" value="5"></property>
		<property name="maxTotal" value="10"></property>
		<property name="testOnBorrow" value="true"></property>
	</bean>

写一个测试类,测试项目是否创建成功:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations="classpath:applicationContext-data-redis.xml")
public class RedisTest {

	private static Logger log = Logger.getLogger(RedisTest.class);
	
	@Autowired
	private RedisTemplate<String, String> redisTemplate;
	
	@Test
	public void testSend() {
		redisTemplate.opsForValue().set("itcast","very good!");
		String value = redisTemplate.opsForValue().get("itcast");
		log.info("从redis中获取的数据:"+value);
	}
}

结果如下:

image.png

redis数据库查询结果:

image.png

说明项目创建成功,能够从redis中获取数据。

因为是使用redis进行消息的通知,所以接下来需要做的就是监听redis的消息。在代码中需要自定义处理redis消息的监听器,从而监听redis的主题消息,具体代码如下:

public class RedisMessageListener implements MessageListener {
	public void onMessage(Message message, byte[] pattern) {
		 System.out.println("从频道为" + new String(message.getChannel())
	                + "中获取了一条新的消息,消息内容:" + new String(message.getBody()));
	}

}

上述代码主要是实现了MessageListener接口,并且实现了onMessage方法,当从redis中获取消息后,打印主题名称和基本的消息。这样就自定义好了一个redis消息的监听器,当订阅的频道有一条新的消息发送过来之后,通过此监听器中的onMessage方法处理。接下来还需要在springData redis的配置文件中添加监听器以及订阅的频道主题,我们测试订阅的频道为JIANGXIA,配置如下:

<!-- 配置处理消息的消息监听适配器 -->
	<bean class="org.springframework.data.redis.listener.adapter.MessageListenerAdapter" id="messageListener">
		<!-- 构造方法注入:自定义的消息监听 -->
	    <constructor-arg>
	        <bean class="cn.jiangxia.redis.listener.RedisKeyExpiredMessageDelegate"/>
	    </constructor-arg>
	</bean>
	
	<!-- 消息监听者容器:对所有的消息进行统一管理 -->
	<bean class="org.springframework.data.redis.listener.RedisMessageListenerContainer" id="redisContainer">
	    <property name="connectionFactory" ref="connectionFactory"/>
	    <property name="messageListeners">
	    	<map>
	    		<!-- 配置频道与监听器
	    			将此频道中的内容交由此监听器处理
	    			key-ref:监听,处理消息
	    			ChannelTopic:订阅的消息频道
	    		 -->
	            <entry key-ref="messageListener">
	                <list>
	                    <bean class="org.springframework.data.redis.listener.ChannelTopic">
	                        <constructor-arg value="JIANGXIA"></constructor-arg>
	                    </bean>
	                </list>
	            </entry>
		    </map>
		 </property>
	</bean>

接下来需要做的就是结合redis key的失效机制和消息通信机制完成过期数据的处理。

核心逻辑是当用户获取到订单是会将该订单数据存入到redis中,并且设置有效期,当该key超过有效期时,就会想定制的频道发送一条消息,如果监听到此消息就获取数据库中的di,然后修改该对应数据即可(修改状态或者直接物理删除)。

核心方法如下:

模拟获取订单并存入数据库和redis缓存中

public void testSave() {
		
		Date now = new Date();
		int timeOut = 1;//设置的失效时间一分钟
		
		Order order = new Order();
		order.setName("测试");//设置名称
		order.setMoney(BigDecimal.ONE);//设置金额
		order.setCouponDesc("苹果手机1000块");//设置说明
		order.setCreateTime(now);//设置获取时间
		//设置超时时间:有效期1分钟后超时
		order.setExpireTime(DataUtils.addTime(now, timeOut));
		//设置状态:0-可用 1-已失效 2-已使用
		order.setState(0);
		orderMapper.save(order);
		
		redisTemplate.opsForValue().set("order:"+order.getId(), order.getId()+"", (long)timeOut, TimeUnit.MINUTES);
	}

在OnMessage中接收redis中key失效消息,然后完成过期数据的处理:

public void onMessage(Message message, byte[] pattern) {
		//1.获取失效的key
		String key = new String(message.getBody());
		//判断是否是失效通知
		if(key.startsWith("order")){
			//2.从key中分离出失效订单的id
			String redisId = key.split(":")[1];
			//3.查询订单信息
			Order order = orderMapper.selectOrderById(Long.parseLong(redisId));
			//4.修改订单状态
			order.setState(1);
			//5.更新数据库
			orderMapper.updateOrder(order);
		}
	}

总结

以上就是如何使用SpringData Redis的key失效机制和消息通信机制,完成对于数据库中过期数据的处理问题,主要适用于秒杀或者限时业务场景中数据的处理。

相关推荐