使用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 userRedPacketList = new ArrayList(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 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 { ...@Overridepublic 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")@ResponseBodypublic Map grapRedPacketByRedis(Long redPacketId, Long userId) {// 抢红包Long result = userRedPacketService.grapRedPacketByRedis(redPacketId, userId);Map retMap = new HashMap();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"%>
参数
12、结果
只需要2秒就抢完所有红包,说明性能是可佳的
本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!
