目录
1. Redis配置
2. 数据存储设计
3. 使用Redis实现抢红包
使用Redis实现抢红包,Redis的功能不如数据库强大,事务也不完整,要保证数据的正确性,数据的正确性可以通过严格的验证得以保证。Redis的Lua语言是原子性的,且功能更强大。Redis并非一个长久存储数据的地方,更多的时候是为了提供更为快速的缓存,所以当红包库存量为0,会将红包数据保存到数据库中,保证数据的安全性和严格性
1.Redis配置
导入相关jar包依赖
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
<!-- spring-redis 整合包 -->
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
<version>1.7.2.RELEASE</version>
</dependency>
applicationContext.xml中配置Redis信息
<!--配置Redis连接池-->
<bean name="poolConfig" class="redis.clients.jedis.JedisPoolConfig">
<!--最大空闲数-->
<property name="maxIdle" value="50"/>
<!--最大连接数-->
<property name="maxTotal" value="100"/>
<!--最大等待毫秒数-->
<property name="maxWaitMillis" value="20000"/>
</bean>
<!--创建Jedis连接工厂-->
<bean id="connectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
<property name="hostName" value = "localhost"/>
<property name="port" value="6379"/>
<property name="poolConfig" ref="poolConfig"/>
</bean>
<!--String序列化器-->
<bean id="stringRedisSerializer" class="org.springframework.data.redis.serializer.StringRedisSerializer"/>
<!--配置RedisTemplate-->
<bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate">
<property name="connectionFactory" ref="connectionFactory"/>
<property name="keySerializer" ref="stringRedisSerializer"/>
<property name="valueSerializer" ref="stringRedisSerializer"/>
<property name="defaultSerializer" ref="stringRedisSerializer"/>
<property name="hashKeySerializer" ref="stringRedisSerializer"/>
<property name="hashValueSerializer" ref="stringRedisSerializer"/>
</bean>
2. 数据存储设计
Redis本身不是一个严格的事务,事务的功能有限。
为了增强功能性,我们可以使用Lua语言。Lua语言是一种原子性操作,可以保证数据的一致性,避免超发现象。
Lua脚本
– 缓存抢红包列表信息列表key
local listKey = ‘red_packet_list_’…KEYS[1]
– 当前被抢红包的key
local redPacket = ‘red_packet_’…KEYS[1]
– 获取当前红包库存
local stock = tonumber(redis.call(‘hget’, redPacket, ‘stock’))
– 没有库存,返回0
if stock <= 0
then
return 0
end
– 库存减1
stock = stock - 1
– 保存当前库存
redis.call(‘hset’, redPacket, ‘stock’, tostring(stock))
– 往链表中加入当前红包信息
redis.call(‘rpush’, listKey, ARGV[1])
– 如果是最后一个红包,则返回2,表示红包已经结束,需要将列表中的数据保存到数据库中
if stock == 0
then
return 2
end
– 如果并非最后一个红包,则返回1, 表示抢红包成功
return 1
流程
- 判断是否存在可抢的库存,如果没有,则返回0,结束流程
- 有可抢夺的红包,红包的库存量减少1,重新设置库存
- 将抢红包数据保存到Redis链表中,链表的key为red_packet_list_{id}
- 如果当前库存为0,则返回2,这说明可以触发数据库对Redis链表数据的保存,链表的key为red_packet_list_{id},它将保存抢红包的用户名和抢的时间
- 如果当前库存不为0,则返回1,说明抢红包信息保存成功
设计保存Redis抢红包的服务类
RedisRedPacketService
public interface RedisRedPacketService {
/**
* 保存redis抢红包列表
* @param redPacketId -- 抢红包编号
* @param unitAmount -- 红包金额
*/
void saveUserRedPacketByRedis(Long redPacketId, Double unitAmount);
}
RedisRedPacketServiceImpl
@Service("redisRedPacketService")
public class RedisRedPacketServiceImpl implements RedisRedPacketService {
private static final String PREFIX = "red_packet_list_";
// 每次取出1000条,避免一次取出消耗太多内存
private static final int TIME_SIZE = 1000;
@Autowired
private RedisTemplate redisTemplate = null; // RedisTemplate
@Autowired
private DataSource dataSource = null;// 数据源
// 开启新线程运行
@Async
public void saveUserRedPacketByRedis(Long redPacketId, Double unitAmount) {
System.out.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>();
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 Date(time));
userRedPacket.setNote("抢红包" + redPacketId);
userRedPacketList.add(userRedPacket);
}
// 插入抢红包信息
count += executeBatch(userRedPacketList);
}
// 删除Redis列表
redisTemplate.delete(PREFIX + redPacketId);
Long end = System.currentTimeMillis();
System.out.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自动创建另外一条线程去运行它,这样就不会出现在抢最后一个红包的线程之内。如果在同一个线程内,对于最后抢红包用户需要等待的时间太长,影响其体验。
- 每次取出1000个抢红包的信息,避免取出的数据过大,导致JVM消耗过多的内存影响系统性能
- 对于大批量的数据操作,最后还会删除Redis保存的链表信息,帮助Redis释放内存
- 对于数据库的保存,采用了JDBC的批量处理,每1000条批量保存一次,有助于性能的提高
用注解@Async前提提供一个任务池给Spring环境,这里提供一下xml配置方式
<!--开启注解调度支持@Async异步-->
<task:annotation-driven executor="asyncExecutor"/>
<!--最小线程数为5,最大线程数为10-->
<task:executor id="asyncExecutor" pool-size="5-10" queue-capacity="200"/>
当在Spring环境中遇到注解@Async就会启动任务池的一条线程去运行对应的方法,这样就能执行异步了
3. 使用Redis实现抢红包
抢红包逻辑: 首先编写Lua语言,然后通过对应的链接发送给Redis服务器,Redis返回一个SHA1字符串,保存它,之后的发送可以只发送这个字符和对应的参数
UserRedPacketService接口加入一个新的方法
/**
* 通过Redis实现抢红包
* @param redPacketId 红包编号
* @param userId 用户编号
* @return
* 0- 没有库存,失败
* 1- 成功,且不是最后一个红包
* 2- 成功,且是最后一个红包
*/
Long grapRedPacketByRedis(Long redPacketId, Long userId);
UserRedPacketServiceImpl
@Service("userRedPacketService")
public class UserRedPacketServiceImpl implements UserRedPacketService {
@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 \n" +
"then\n" +
" return 0\n" +
"end\n" +
"stock = stock - 1\n" +
"redis.call('hset', redPacket, 'stock', tostring(stock))\n" +
"redis.call('rpush', listKey, ARGV[1])\n" +
"if stock == 0 \n" +
"then \n" +
" return 2\n" +
"end\n" +
"return 1";
// 在缓存Lua脚本后,使用该变量保存Redis返回的32位的SHA1编码,使用它去执行缓存的Lua脚本
String sha1 = null;
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.out.println("thread_name = " + Thread.currentThread().getName());
// Long start = System.currentTimeMillis();
redisRedPacketService.saveUserRedPacketByRedis(redPacketId, unitAmount);
// Long end = System.currentTimeMillis();
// System.out.println("最后一个用户抢红包耗时:" + (end-start));
}
} finally {
// 确保jedis顺利关闭
if(jedis != null && jedis.isConnected()) {
jedis.close();
}
}
return result;
}
}
控制器UserRedPacketController
@Controller
@RequestMapping("/userRedPacket")
public class UserRedPacketController {
@Autowired
private UserRedPacketService userRedPacketService = null;
@RequestMapping(value = "/grapRedPacketByRedis")
@ResponseBody
public Map<String, Object> grapRedPacketByRedis(Long redPacketId, Long userId) {
Map<String, Object> resultMap = new HashMap<String, Object>();
Long result = userRedPacketService.grapRedPacketByRedis(redPacketId, userId);
boolean flag = result > 0;
resultMap.put("result", flag);
resultMap.put("message", flag ? "抢红包成功" : "抢红包失败");
return resultMap;
}
}
测试Redis抢红包
<%@ page contentType="text/html;charset=UTF-8" language="java" isELIgnored="false" %>
<html>
<head>
<title>参数</title>
<%--加载JQuery文件--%>
<script type="text/javascript" src="${pageContext.request.contextPath}/static/js/jquery-1.7.2.js"></script>
<script type="text/javascript">
// 就绪函数,表示当前页面加载完毕后,直接执行里面的代码
$(function () {
// 模拟30000个异步请求,进行并发
var max = 300000;
for (var i = 1; i <= max; i++) {
$.ajax({
type: 'post',
url:"${pageContext.request.contextPath}/userRedPacket/grapRedPacketByRedis.do?redPacketId=16&userId=" + i,
success:function (result) {
}
})
}
})
</script>
</head>
<body>
</body>
</html>
没有出现超发情况,而且性能比乐观锁和悲观锁的情况高效