MyEclipse10、tomcat7、SSM、Redis、Mysql5、jdk7
在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;
}
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 链表数据到数据库的程序。
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
package com.ssm.chapter22.service;
public interface RedisRedPacketService {
/**
* 保存redis抢红包列表
* @param redPacketId --抢红包编号
* @param unitAmount -- 红包金额
*/
public void saveUserRedPacketByRedis(Long redPacketId,Double unitAmount);
}
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自动创建另外一条线程去运行它,这样它便不在抢最后一个红包的线程之内,因为这个方法是一个较长时间的方法,如果在同一个线程内,那么对于最后抢红包的用户来说就需要等待相当长的时间,影响用户体验。
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;
}
}
/**
* 通过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
@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;
}
//使用redis
@RequestMapping("/grapRedPacketByRedis")
@ResponseBody
public 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;
}
模拟高并发的jsp文件,其中,由于post是异步请求,所以可以模拟多个用户同时请求的情况:
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
参数