使用Redis实现抢红包

使用Redis实现抢红包

1、开发环境

MyEclipse10、tomcat7、SSM、Redis、Mysql5、jdk7
在这里插入图片描述

2、使用注解方式配置Redis

在RootConfig.java中创建一个RedisTemplate对象

@Bean(name="redisTemplate")
public RedisTemplate  initRedisTemplate(){
	JedisPoolConfig poolConfig=new JedisPoolConfig();
	//最大空闲数
	poolConfig.setMaxIdle(50);
	//最大连接数
	poolConfig.setMaxTotal(100);
	//最大等待毫秒数
	poolConfig.setMaxWaitMillis(20000);
	
	//创建Jedis链接工厂
	JedisConnectionFactory connectionFactory=new JedisConnectionFactory(poolConfig);
	connectionFactory.setHostName("localhost");
	connectionFactory.setPort(6379);
	
	//调用后初始化方法,没有它将抛出异常
	connectionFactory.afterPropertiesSet();

	//自定Redis序列化器
	RedisSerializer jdkSerializationRedisSerializer=new JdkSerializationRedisSerializer();
	RedisSerializer stringRedisSerializer=new StringRedisSerializer();
	
	//定义RedisTemplate,并设置连接工程[修改为:工厂]
	RedisTemplate redisTemplate=new RedisTemplate();
	
	//设置序列化器	
	redisTemplate.setConnectionFactory(connectionFactory);
	
	redisTemplate.setDefaultSerializer(stringRedisSerializer);
	
	redisTemplate.setKeySerializer(stringRedisSerializer);
	redisTemplate.setValueSerializer(stringRedisSerializer);
	
	redisTemplate.setHashKeySerializer(stringRedisSerializer);
	redisTemplate.setHashValueSerializer(stringRedisSerializer);
	
	return redisTemplate;
}
3、数据储存设计(Lua脚本设计)
 String script = "local listKey = 'red_packet_list_'..KEYS[1] \n"   // 被抢红包列表 key
             + "local redPacket = 'red_packet_'..KEYS[1] \n"    // 当前被抢红包 key
                + "local stock = tonumber(redis.call('hget', redPacket, 'stock')) \n" // 读取当前红包库存
                + "if stock <= 0 then return 0 end \n"  // 没有库存,返回0
                + "stock = stock -1 \n"           // 库存减1
                + "redis.call('hset', redPacket, 'stock', tostring(stock)) \n"  // 保存当前库存
                + "redis.call('rpush', listKey, ARGV[1]) \n"     // 往Redis链表中加入当前红包信息
                + "if stock == 0 then return 2 end \n"     // 如果是最后一个红包,则返回2,表示抢红包已经结束,需要将Redis列表中的数据保存到数据库中
                + "return 1 \n";    // 如果并非最后一个红包,则返回1,表示抢红包成功。

​ 当返回为2的时候,说明红包已经没有库存,会触发数据库对链表数据的保存,这是一个大数据量的保存,因为有20000条记录。为了不影响最后一次抢红包的响应,在实际的操作中往往会考虑使用 JMS 消息发送到别的服务器进行操作。这里只是创建了一条新的线程去运行保存 Redis 链表数据到数据库的程序。

4、使用下面的Redis命令在Redis中初始化了一个编号为5的大红包,其中库存为2万个,每个10元
127.0.0.1:6379> hset red_packet_9 stock 20000
(integer) 1
127.0.0.1:6379> hset red_packet_9 unit_amount 10
(integer) 1
5、保存 Redis 抢红包信息到数据库的服务类(RedisRedPacketService.java)

package com.ssm.chapter22.service;

public interface RedisRedPacketService {

/**
 * 保存redis抢红包列表
 * @param redPacketId --抢红包编号
 * @param unitAmount -- 红包金额
 */
public void saveUserRedPacketByRedis(Long redPacketId,Double unitAmount);
}
6、RedisRedPacketService接口实现类(RedisRedPacketServiceImpl.java)

package com.ssm.chapter22.service.Impl;

import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Timestamp;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.List;

import javax.sql.DataSource;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.BoundListOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

import com.ssm.chapter22.pojo.UserRedPacket;
import com.ssm.chapter22.service.RedisRedPacketService;

@Service
public class RedisRedPacketServiceImpl implements RedisRedPacketService {

private static final String PREFIX="red_packet_list_";
// 每次取出100条,避免一次取出消耗太多内存
private static final int TIME_SIZE=100;

@Autowired
private RedisTemplate redisTemplate=null;

@Autowired
private DataSource dataSource=null;//数据源

@Async  // 开启新线程运行(意思是让spring自动创建另外一条线程去运行它)
public void saveUserRedPacketByRedis(Long redPacketId, Double unitAmount) {
	System.err.println("开始保存数据");
	Long start = System.currentTimeMillis();
	// 获取列表操作对象
	BoundListOperations ops = redisTemplate.boundListOps(PREFIX + redPacketId);
	Long size = ops.size();
	Long times = size % TIME_SIZE == 0 ? size / TIME_SIZE : size / TIME_SIZE + 1;
	int count = 0;
	List<UserRedPacket> userRedPacketList = new ArrayList<UserRedPacket>(TIME_SIZE);
	for (int i = 0; i < times; i++) {
		// 获取至多TIME_SIZE个抢红包信息
		List userIdList = null;
		if (i == 0) {
			userIdList = ops.range(i * TIME_SIZE, (i + 1) * TIME_SIZE);
		} else {
			userIdList = ops.range(i * TIME_SIZE + 1, (i + 1) * TIME_SIZE);
		}
		userRedPacketList.clear();
		// 保存红包信息
		for (int j = 0; j < userIdList.size(); j++) {
			String args = userIdList.get(j).toString();
			String[] arr = args.split("-");
			String userIdStr = arr[0];
			String timeStr = arr[1];
			Long userId = Long.parseLong(userIdStr);
			Long time = Long.parseLong(timeStr);
			// 生成抢红包信息
			UserRedPacket userRedPacket = new UserRedPacket();
			userRedPacket.setRedPacketId(redPacketId);
			userRedPacket.setUserId(userId);
			userRedPacket.setAmount(unitAmount);
			userRedPacket.setGrabTime(new Timestamp(time));
			userRedPacket.setNote("抢红包 " + redPacketId);
			userRedPacketList.add(userRedPacket);
		}
		// 插入抢红包信息
		count += executeBatch(userRedPacketList);
	}
	// 删除Redis列表
	redisTemplate.delete(PREFIX + redPacketId);//目的是释放内存资源
	Long end = System.currentTimeMillis();
	System.err.println("保存数据结束,耗时" + (end - start) + "毫秒,共" + count + "条记录被保存。");
}

/**
 * 使用JDBC批量处理Redis缓存数据:有助于性能的提高
 * 
 * @param userRedPacketList
 *            -- 抢红包列表
 * @return 抢红包插入数量.
 */
private int executeBatch(List<UserRedPacket> userRedPacketList){
	Connection conn = null;
	Statement stmt = null;
	int[] count = null;
	try {
		conn = dataSource.getConnection();
		conn.setAutoCommit(false);//禁止自动提交
		stmt = conn.createStatement();
		for (UserRedPacket userRedPacket : userRedPacketList) {
			String sql1 = "update T_RED_PACKET set stock = stock-1 where id=" + userRedPacket.getRedPacketId();
			DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");//日期的格式
			String sql2 = "insert into T_USER_RED_PACKET(red_packet_id, user_id, " + "amount, grab_time, note)"
					+ " values (" + userRedPacket.getRedPacketId() + ", " + userRedPacket.getUserId() + ", "
					+ userRedPacket.getAmount() + "," + "'" + df.format(userRedPacket.getGrabTime()) + "'," + "'"
					+ userRedPacket.getNote() + "')";
			
			stmt.addBatch(sql1);
			stmt.addBatch(sql2);
		}
		// 执行批量
		count = stmt.executeBatch();
		// 提交事务
		conn.commit();
	} catch (SQLException e) {
		/********* 错误处理逻辑 (自定义抛出异常)********/
		throw new RuntimeException("抢红包批量执行程序错误");
	} finally {
		try {
			if (conn != null && !conn.isClosed()) {
				conn.close();
			}
		} catch (SQLException e) {
			e.printStackTrace();
		}
	}
	// 返回插入抢红包数据记录
	return count.length / 2;
}
}

​ 这里的@Async注解表示让Spring自动创建另外一条线程去运行它,这样它便不在抢最后一个红包的线程之内,因为这个方法是一个较长时间的方法,如果在同一个线程内,那么对于最后抢红包的用户来说就需要等待相当长的时间,影响用户体验。

7、为了使用@Async注解,还需要在 Spring 中配置一个任务池(WebConfig.java):
package com.ssm.chapter22.config;
...
@EnableAsync
public class WebConfig extends AsyncConfigurerSupport { 
  ...
    @Override
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
        taskExecutor.setCorePoolSize(5);
        taskExecutor.setMaxPoolSize(10);
        taskExecutor.setQueueCapacity(200);
        taskExecutor.initialize();
        return taskExecutor;
    }
}
8、userRedPacketService接口中加入一个新方法:
/**
 * 通过Redis实现抢红包
 * @param redPacketId --红包编号
 * @param userId -- 用户编号
 * @return  
 * 0-没有库存,失败 
 * 1--成功,且不是最后一个红包
 * 2--成功,且是最后一个红包
 */
public Long grapRedPacketByRedis(Long redPacketId, Long userId);

用户抢红包逻辑:grapRedPacketByRedis接收的参数,第一个是大红包名称“red_packet_9”中的9,而userId是jsp文件中发起抢红包请求的唯一标识[0,30000]中的某一个i值。

当[0,30000]中的某一个值i发起请求后,假设 i 为 1000

  • jsp中直接异步post到url: “./userRedPacket/grapRedPacketByRedis.do?redPacketId=9&userId=” + i,然后调用grapRedPacketByRedis(Long redPacketId, Long userId)方法,其中redPacketId=9,而userId=1000。
  • 然后通过Object res = jedis.evalsha(sha1, 1, redPacketId + “”, args);执行自定义的Lua脚本,其中,1表示key的个数,args表示grapRedPacketByRedis方法的两个参数值。定义用户抢红包信息在Redis中的键listKey=red_packet_list_9,大红包在Redis中的键red_packet_9,然后查询red_packet_9中的stock,返回还剩红包的数量stock,此时假设stock=7777,则由于还有红包,于是将stock变为7776,并更新到red_packet_9的stock中,然后将键值对red_packet_list_9-ARGV[1](也就是userId),即red_packet_list_9- 1000写入Redis中。
  • 当返回结果为 2 时,说明最后一个红包已经被抢了,这个时候,jedis.hget(“red_packet_” + redPacketId, “unit_amount”);得到red_packet_9 → unit_amount 即单个小红包的金额10赋给变量unitAmount,然后saveUserRedPacketByRedis(redPacketId, unitAmount);方法将Redis中键red_packet_list的信息加上每个小红包金额信息,以及其他各种信息对应成用户抢红包数据库表定义,另开一个线程,将20000个数据库记录添加到数据库中保存起来。
9、使用Redis抢红包逻辑(UserRedPacketServiceImpl.java)
@Autowired
private RedisTemplate redisTemplate=null;

@Autowired
private RedisRedPacketService redisRedPacketService=null;

// Lua脚本
	String script = "local listKey = 'red_packet_list_'..KEYS[1] \n" 
	+ "local redPacket = 'red_packet_'..KEYS[1] \n"
			+ "local stock = tonumber(redis.call('hget', redPacket, 'stock')) \n" 
			+ "if stock <= 0 then return 0 end \n"
			+ "stock = stock -1 \n" 
			+ "redis.call('hset', redPacket, 'stock', tostring(stock)) \n"
			+ "redis.call('rpush', listKey, ARGV[1]) \n" 
			+ "if stock == 0 then return 2 end \n" 
			+ "return 1 \n";
	
	// 在缓存LUA脚本后,使用该变量保存Redis返回的32位的SHA1编码,使用它去执行缓存的LUA脚本[加入这句话]
	String sha1=null;
	
@Override
public Long grapRedPacketByRedis(Long redPacketId, Long userId) {
	// 当前抢红包用户和日期信息
			String args = userId + "-" + System.currentTimeMillis();
			Long result = null;
			// 获取底层Redis操作对象
			Jedis jedis = (Jedis) redisTemplate.getConnectionFactory().getConnection().getNativeConnection();
			try {
				// 如果脚本没有加载过,那么进行加载,这样就会返回一个sha1编码
				if (sha1 == null) {
					sha1 = jedis.scriptLoad(script);
				}
				// 执行脚本,返回结果
				Object res = jedis.evalsha(sha1, 1, redPacketId + "", args);
				result = (Long) res;
				// 返回2时为最后一个红包,此时将抢红包信息通过异步保存到数据库中
				if (result == 2) {
					// 获取单个小红包金额
					String unitAmountStr = jedis.hget("red_packet_" + redPacketId, "unit_amount");
					// 触发保存数据库操作
					Double unitAmount = Double.parseDouble(unitAmountStr);
					System.err.println("thread_name = " + Thread.currentThread().getName());
					redisRedPacketService.saveUserRedPacketByRedis(redPacketId, unitAmount);
				}
			} finally {
				// 确保jedis顺利关闭
				if (jedis != null && jedis.isConnected()) {
					jedis.close();
				}
			}
			return result;
}
10、UserRedPacketController.java使用redis实现抢红包逻辑
	//使用redis
	@RequestMapping("/grapRedPacketByRedis")
	@ResponseBody
	public Map<String, Object> grapRedPacketByRedis(Long redPacketId, Long userId) {
		// 抢红包
		Long result = userRedPacketService.grapRedPacketByRedis(redPacketId, userId);
		Map<String, Object> retMap = new HashMap<String, Object>();
		boolean flag = result > 0;
		retMap.put("success", flag);
		retMap.put("message", flag ? "抢红包成功" : "抢红包失败");
		return retMap;
	}
11、jsp页面

模拟高并发的jsp文件,其中,由于post是异步请求,所以可以模拟多个用户同时请求的情况:

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>参数</title>
<!-- 加载Query文件-->
<script type="text/javascript"
    src="https://code.jquery.com/jquery-3.2.0.js">
        </script>
<script type="text/javascript">
            $(document).ready(function () {
                  //模拟30000个异步请求,进行并发
                var max = 30000;
                for (var i = 1; i <= max; i++) {
                    //jQuery的post请求,请注意这是异步请求
                    $.post({
                        //请求抢id为1的红包
                        //根据自己请求修改对应的url和大红包编号
                        url: "./userRedPacket/grapRedPacketByRedis.do?redPacketId=9&userId=" + i,
                        //成功后的方法
                        success: function (result) {
                        }
                    });
                }
            });
        </script>
</head>
<body>
</body>
</html>
12、结果

在这里插入图片描述

在这里插入图片描述
只需要2秒就抢完所有红包,说明性能是可佳的

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值