抢红包案例分析以及代码实现(四)

前文回顾

抢红包案例分析以及代码实现(一)

抢红包案例分析以及代码实现(二)

抢红包案例分析以及代码实现(三 )

上面三篇博文是使用的MySql数据库来作为数据的载体数据最终会将数据保存到磁盘中,而Redis使用的是内存,内存的速度比磁盘速度肯定要快很多。

对于使用 Redis实现抢红包,首先需要知道的是Redis的功能不如数据库强大,事务也不是很完整.因此要保证数据的正确性数据的正确性可以通过严格的验证得以保证。

而 Redis的 Lua 语言是原子性的,且功能更为强大,所以优先选择使用Lua语言来实现抢红包。

但是无论如何对于数据而言,在 Redis 当中存储,始终都不是长久之计 , 因为 Redis并非一个长久储存数据的地方,更多的时候只是为了提供更为快速的缓存,所以当红包金额为 0 或者红包超时的时候(超时操作可以使用定时机制实,这里暂不讨论), 会将红包数据保存到数据库中,,这样才能够保证数据的安全性和严格性。

所以本篇博文我们将使用Redis + lua脚本来实现抢红包的功能。

实现步骤

注解方式配置 Redis

首先在类 RootConfig 上创建一个 RedisTemplate 对象,并将其装载到 Spring IoC 容器中。

/**

* 创建一个 RedisTemplate 对象

*/

@Bean(name ="redisTemplate")

publicRedisTemplateinitRedisTemplate(){

JedisPoolConfig poolConfig =newJedisPoolConfig();

// 最大空闲数

poolConfig.setMaxIdle(50);

// 最大连接数

poolConfig.setMaxTotal(100);

// 最大等待毫秒数

poolConfig.setMaxWaitMillis(20000);

// 创建Jedis链接工厂

JedisConnectionFactory connectionFactory =newJedisConnectionFactory(poolConfig);

connectionFactory.setHostName("192.168.31.66");

connectionFactory.setPort(6379);

// 调用后初始化方法,没有它将抛出异常

connectionFactory.afterPropertiesSet();

// 自定Redis序列化器

RedisSerializer jdkSerializationRedisSerializer =newJdkSerializationRedisSerializer();

RedisSerializer stringRedisSerializer =newStringRedisSerializer();

// 定义RedisTemplate,并设置连接工厂

RedisTemplate redisTemplate =newRedisTemplate();

redisTemplate.setConnectionFactory(connectionFactory);

// 设置序列化器

redisTemplate.setDefaultSerializer(stringRedisSerializer);

redisTemplate.setKeySerializer(stringRedisSerializer);

redisTemplate.setValueSerializer(stringRedisSerializer);

redisTemplate.setHashKeySerializer(stringRedisSerializer);

redisTemplate.setHashValueSerializer(stringRedisSerializer);

returnredisTemplate;

}

这样 RedisTemplate 就可以在 Spring 上下文中使用了。

注意, JedisConnectionFactory对象在最后的时候需要自行调用 afterPropertiesSet 方法,它实现了 lnitializingBean 接 口。 如果将其配置在 Spring IoC 容器中, Spring 会自动调用它,但是这里我们是自行创建的, 因此需要自行调用,否则在运用的时候会抛出异常。

lua脚本和异步持久化功能的开发

Redis 并不是一个严格的事务,而且事务的功能也是有限的 。加上 Redis 本身的命令也比较有限,功能性不强,为了增强功能性,还可以使用 Lua 语言。

Redis 中的 Lua 语言是一种原子性的操作,可以保证数据的一致性。

依据这个原理可以避免超发现象,完成抢红包的功能,而且对于性能而言, Redis 会比数据库快得多。

第一次运行 Lua 脚本的时候,先在 Redis 中编译和缓存脚本,这样就可以得到一个 SHA1字符串,之后通过 SHAl 字符串和参数就能调用 Lua 脚本了。

--缓存抢红包列表信息列表 key

locallistKey ='red_packet_list_'..KEYS[1]

--当前被抢红包 key

localredPacket ='red_packet_'..KEYS[1]

--获取当前红包库存

localstock = tonumber(redis.call('hget', redPacket,'stock'))

--没有库存,返回为 0

ifstock <= 0then

return0

end

--库存减 1

stock = stock-1

--保存当前库存

redis.call('hset', redPacket,'stock', tostring(stock))

--往链表中加入当前红包信息

redis.call('rpush', listKey, ARGV[1])

--如果是最后一个红包,则返回 2 ,表示抢红包已经结束,需要将列表中的数据保存到数据库中

ifstock == 0then

return2

end

--如果并非最后一个红包,则返回 l ,表示抢红包成功

return1

流程:

判断是否存在可抢的库存,如果己经没有可抢夺 的红包,则返回为 0,结束流程

有可抢夺的红包,对于红包的库存减1 ,然后重新设置库存

将抢红包数据保存到 Redis 的链表当中,链表的 key 为 red_packet_list_ {id}

如果当前库存为 0 ,那么返回 2,这说明可以触发数据库对 Redis 链表数据的保存,链表的 key 为 red_packet_ list_ {id},它将保存抢红包的用户名和抢的时间

如果当前库存不为 0 ,那么将返回 1,这说明抢红包信息保存成功。

当返回为 2 的时候,说明红包己经没有库存,会触发数据库对链表数据的保存, 这是一个大数据量的保存。为了不影响最后一次抢红包的响应,在实际的操作中往往会考虑使用 JMS 消息发送到别的服务器进行操作,我们这里选择一种简单的方式来实现,去创建一条新的线程去运行保存 Redis 链表数据到数据库。

那就在Service层写一个持久到数据库的服务类吧

接口

packagecom.artisan.redpacket.service;

publicinterfaceRedisRedPacketService{

/**

* 保存redis抢红包列表

*@paramredPacketId --抢红包编号

*@paramunitAmount -- 红包金额

*/

publicvoidsaveUserRedPacketByRedis(Long redPacketId, Double unitAmount);

}

实现类

packagecom.artisan.redpacket.service.impl;

importjava.sql.Connection;

importjava.sql.SQLException;

importjava.sql.Statement;

importjava.sql.Timestamp;

importjava.text.DateFormat;

importjava.text.SimpleDateFormat;

importjava.util.ArrayList;

importjava.util.List;

importjavax.sql.DataSource;

importorg.springframework.beans.factory.annotation.Autowired;

importorg.springframework.data.redis.core.BoundListOperations;

importorg.springframework.data.redis.core.RedisTemplate;

importorg.springframework.scheduling.annotation.Async;

importorg.springframework.stereotype.Service;

importcom.artisan.redpacket.pojo.UserRedPacket;

importcom.artisan.redpacket.service.RedisRedPacketService;

@Service

publicclassRedisRedPacketServiceImplimplementsRedisRedPacketService{

privatestaticfinalString PREFIX ="red_packet_list_";

// 每次取出1000条,避免一次取出消耗太多内存

privatestaticfinalintTIME_SIZE =1000;

@Autowired

privateRedisTemplate redisTemplate;// RedisTemplate

@Autowired

privateDataSource dataSource;// 数据源

@Override

// 开启新线程运行

@Async

publicvoidsaveUserRedPacketByRedis(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;

intcount =0;

List userRedPacketList =newArrayList(TIME_SIZE);

for(inti =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(intj =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 =newUserRedPacket();

userRedPacket.setRedPacketId(redPacketId);

userRedPacket.setUserId(userId);

userRedPacket.setAmount(unitAmount);

userRedPacket.setGrabTime(newTimestamp(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缓存数据.

*

*@paramuserRedPacketList

*            -- 抢红包列表

*@return抢红包插入数量.

*/

privateintexecuteBatch(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 =newSimpleDateFormat("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) {

/********* 错误处理逻辑 ********/

thrownewRuntimeException("抢红包批量执行程序错误");

}finally{

try{

if(conn !=null&& !conn.isClosed()) {

conn.close();

}

}catch(SQLException e) {

e.printStackTrace();

}

}

// 返回插入抢红包数据记录

returncount.length /2;

}

}

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

这里是每次取出 1 000 个抢红包的信息,之所以这样做是为了避免取出 的数据过大 , 导致JVM 消耗过多的内存影响系统性能。

对于大批量的数据操作,这是我们在实际操作中要注意的,最后还会删除 Redis保存的链表信息,这样就帮助 Redis 释放内存了

对于数据库的保存 ,这里采用了 JDBC的批量处理,每 1000 条批量保存一次,使用批量有助于性能的提高。

注解@Async 的前提是提供一个任务池给 Spring 环境,这个时候要在原有的基础上改写配置类 WebConfig

@EnableAsync

publicclassWebConfigextendsAsyncConfigurerSupport{

....

....

....

@Override

publicExecutorgetAsyncExecutor(){

ThreadPoolTaskExecutor taskExecutor =newThreadPoolTaskExecutor();

taskExecutor.setCorePoolSize(5);

taskExecutor.setMaxPoolSize(10);

taskExecutor.setQueueCapacity(200);

taskExecutor.initialize();

returntaskExecutor;

}

}

使用@EnableAsync表明支持异步调用,而我们实现了接口AsyncConfigurerSupport的getAsyncExecutor方法,它是获取一个任务池,当在 Spring 环境中遇到注解@Async就会启动这个任务池的一条线程去运行对应的方法,这样便能执行异步了。

Service层添加Redis抢红包的逻辑

UserRedPacketService接口新增接口方法grapRedPacketByRedis

/**

* 通过Redis实现抢红包

*

*@paramredPacketId

*            --红包编号

*@paramuserId

*            -- 用户编号

*@return0-没有库存,失败 1--成功,且不是最后一个红包 2--成功,且是最后一个红包

*/

publicLong grapRedPacketByRedis(Long redPacketId, Long userId);

实现类

@Autowired

privateRedisTemplate redisTemplate;

@Autowired

privateRedisRedPacketService redisRedPacketService;

// Lua脚本

Stringscript ="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脚本[加入这句话]

Stringsha1 =null;

@Override

publicLong grapRedPacketByRedis(Long redPacketId, Long userId) {

// 当前抢红包用户和日期信息

Stringargs = userId +"-"+ System.currentTimeMillis();

Long result =null;

// 获取底层Redis操作对象

Jedis jedis = (Jedis) redisTemplate.getConnectionFactory().getConnection().getNativeConnection();

try{

// 如果脚本没有加载过,那么进行加载,这样就会返回一个sha1编码

if(sha1 ==null) {

sha1 = jedis.scriptLoad(script);

}

// 执行脚本,返回结果

Objectres = jedis.evalsha(sha1,1, redPacketId +"", args);

result = (Long) res;

// 返回2时为最后一个红包,此时将抢红包信息通过异步保存到数据库中

if(result ==2) {

// 获取单个小红包金额

StringunitAmountStr = jedis.hget("red_packet_"+ redPacketId,"unit_amount");

// 触发保存数据库操作

Double unitAmount = Double.parseDouble(unitAmountStr);

redisRedPacketService.saveUserRedPacketByRedis(redPacketId, unitAmount);

}

}finally{

// 确保jedis顺利关闭

if(jedis !=null&& jedis.isConnected()) {

jedis.close();

}

}

returnresult;

}

这里使用了保存脚本返回 的 SHAl 字符串 ,所以只会发送一次脚本到 Redis 服务器,之后只传输 SHAl 字符串和参数到 Redis 就能执行脚本 了, 当脚本返回为 2 的时候, 表示此时所有的红包都已经被抢光了 ,那么就会触发 redisRedPacketService 的 saveUserRedPacketByRedis 方法。由于在 saveU serRedPacketByRedis 加入注解@Async , 所以 Spring 会创建一条新的线程去运行它 , 这样就不会影响最后抢一个红包用户 的响应时间了 。

Controller层新增路由方法

@RequestMapping(value ="/grapRedPacketByRedis")

@ResponseBody

publicMap grapRedPacketByRedis(Long redPacketId, Long userId) {

Map resultMap =newHashMap();

Long result = userRedPacketService.grapRedPacketByRedis(redPacketId, userId);

booleanflag = result >0;

resultMap.put("result", flag);

resultMap.put("message", flag ?"抢红包成功":"抢红包失败");

returnresultMap;

}

构造模拟数据,测试

先在 Redis 上添加红包信息

127.0.0.1:6379>HMSETred_packet_1stock20000unit_amount10

OK

初始化了一个编号为1 的大红包,其中库存为 2 万个,每个 10 元. 需要保证数据库的红包表内也有对应的记录才可以。

复制个grapByRedis.jsp,测试吧

<%@pagelanguage="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">

参数

<!-- 加载Query文件-->

src="https://code.jquery.com/jquery-3.2.0.js">

$(document).ready(function(){

//模拟30000个异步请求,进行并发

varmax =30000;

for(vari =1; i <= max; i++) {

$.post({

//请求抢id为1的红包

//根据自己请求修改对应的url和大红包编号

url:"./userRedPacket/grapRedPacketByRedis.do?redPacketId=1&userId=1",

//成功后的方法

success:function(result){

console.log("OK")

}

});

}

});

启动应用,访问 http://localhost:8080/ssm_redpacket/grapByRedis.jsp

结合前几篇的数据统计,使用Redis的方式数据一致性也得到了保证且性能远远高于乐观锁和悲观锁的方式。

代码

https://github.com/yangshangwei/ssm_redpacket

好了,抢红包案例到此就讲解完了,下面是对这一系列文章的整体总结。

总结

(全剧终)

扩展阅读

抢红包案例分析以及代码实现(三)

Reddit 如何统计每个帖子的浏览量

教你88秒插入1000万条数据到mysql数据库表

来源:https://blog.csdn.net/yangshangwei/article/details/83003658

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容