SSM+Redis高并发抢红包之-Lua+Redis

2024-04-20 06:48
文章标签 ssm redis 并发 lua 抢红包

本文主要是介绍SSM+Redis高并发抢红包之-Lua+Redis,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

 

 

 

上面几次的超发现象,SSM+Redis高并发抢红包之-悲观锁,SSM+Redis高并发抢红包之-乐观锁关于抢红包解决并发问题,都是基于数据库方面。这次我们换个非关系型数据库来解决,它就是redis。这里我们利用redis缓存数据,用Lua语言来保证操作的原子性,这样就保证了数据的一致性,从而避免前面的超发现象了。等到达临界点再将相关数据写入mysql数据库中,这样就提高了程序的运行效率。主要流程图大概内容就是下面那个了。

 

                                                  

 

这里面主要用到redis 数据结构就是 hash,加上一点Lua语言。

下面给出代码:

 

1.添加相关依赖

    <!-- Redis-Java --><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>2.1.6.RELEASE</version></dependency>

 

2.添加spring-redis的配置文件

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx"xmlns:context="http://www.springframework.org/schema/context"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsdhttp://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsdhttp://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"><bean id="poolConfig" class="redis.clients.jedis.JedisPoolConfig"><!--最大空闲数 --><property name="maxIdle" value="50" /><!--最大连接数 --><property name="maxTotal" value="100" /><!--最大等待时间 --><property name="maxWaitMillis" value="20000" /></bean><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><bean id="jdkSerializationRedisSerializer"class="org.springframework.data.redis.serializer.JdkSerializationRedisSerializer" /><bean id="stringRedisSerializer"class="org.springframework.data.redis.serializer.StringRedisSerializer" /><!-- 序列化器 -->	<bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate"><property name="connectionFactory" ref="connectionFactory" /><property name="defaultSerializer" ref="stringRedisSerializer" /><property name="keySerializer" ref="stringRedisSerializer" /><property name="valueSerializer" ref="stringRedisSerializer" /></bean>
</beans>

3.因为在里面我们用到了@Async注解,所以也要在spring-service.xml里注册

 <!-- 异步线程调用注解 --><task:executor id="myexecutor" pool-size="5" /><task:annotation-driven executor="myexecutor"/>

4.新建一个RedisRedPacketService服务类

public interface RedisRedPacketService {/** 保存redis抢红包列表* @param redPacketId --抢红包编号* @param unitAmount --红包金额*/public void saveUserRedPacketByRedis( Long redPacketId, Double unitAmount);}

 

RedisRedPacketService接口实现类

import java.sql.Connection;
import java.sql.Date;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Timestamp;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Logger;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.pojo.UserRedPacket;
import com.service.RedisRedPacketService;@Service
public class RedisRedPacketServiceImpl implements RedisRedPacketService {private static final String PREFIX = "red_packet_list_";//每次取出1000条,避免一次取出消耗太多内存private static final int TIME_SIZE = 1000;private static Logger logger = Logger.getLogger("RedisRedPacketServiceImpl.class");@Autowiredprivate RedisTemplate redisTemplate = null;@Autowiredprivate DataSource dataSource = null;@Override//开启新线程运行@Asyncpublic void saveUserRedPacketByRedis(Long redPacketId, Double unitAmount) {// TODO Auto-generated method stubSystem.out.println("开始保存数据");Long start = System.currentTimeMillis();//获取列表操作对象BoundListOperations ops = redisTemplate.boundListOps(PREFIX + redPacketId);Long size = ops.size();System.out.println("size:"+size);Long times = size % TIME_SIZE == 0 ? size/TIME_SIZE:size/TIME_SIZE+1;System.out.println("times等于:"+times);int count = 0;List<UserRedPacket> listUserRedPacket = new ArrayList<UserRedPacket>(TIME_SIZE);System.out.println("集合大小为:"+listUserRedPacket.size());logger.info("start");for(int i = 0;i < times;i++) {//获取至多TIME_SIZE个抢红包信息System.out.println("zzz");List listUserId = null;if(i == 0) {System.out.println("开始了没");listUserId = ops.range(i*TIME_SIZE, (i+1)*TIME_SIZE);System.out.println("结束了没");}else {listUserId = ops.range(i*TIME_SIZE+1, (i+1)*TIME_SIZE);}System.out.println("listUserId 等于:"+listUserId);listUserRedPacket.clear();//保存红包信息for(int j = 0;j < listUserId.size();j++) {String args = listUserId.get(j).toString();String[] arr = args.split("-");String userIdStr = arr[0];String timeStr = arr[1];System.out.println("timeStr="+timeStr);Long userId = Long.parseLong(userIdStr);Long time = Long.parseLong(timeStr);System.out.println("time is "+ time);//生成红包信息UserRedPacket userRedPacket = new UserRedPacket();userRedPacket.setRedPacketId(redPacketId);userRedPacket.setUserId(userId);userRedPacket.setAmount(unitAmount);userRedPacket.setGrabTime(new Timestamp(time));userRedPacket.setNote("抢红包"+redPacketId);listUserRedPacket.add(userRedPacket);}//插入抢红包信息count += executeBatch(listUserRedPacket);System.out.println("count = "+count);}//删除Redis 列表,释放Redis内存redisTemplate.delete(PREFIX + redPacketId);Long end = System.currentTimeMillis();System.out.println("保存数据结束,耗时"+ (end - start) +"毫秒,共" + count + "条记录被保存。");}/** 使用JDBC批量处理Redis 缓存数据* @param listUserRedPacket 抢红包列表* @return 抢红包插入数量*/private int executeBatch(List<UserRedPacket> listUserRedPacket) {// TODO Auto-generated method stubConnection conn = null;Statement stmt = null;int[] count = null;try {conn = dataSource.getConnection();conn.setAutoCommit(false);stmt = conn.createStatement();for(UserRedPacket userRedPacket : listUserRedPacket) {//更新库存sqlString sql1 = "update T_RED_PACKET set stock = stock-1 where id=" + userRedPacket.getRedPacketId();DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");//插入抢红包用户信息sqlString 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();System.out.println(count);//提交事务conn.commit();}catch(SQLException e) {e.printStackTrace();}finally {try {if(conn != null && !conn.isClosed()) {conn.close();}}catch(Exception e) {e.printStackTrace();}}//返回插入红包数据记录return count.length/2;}}

 

5.实现抢红包的逻辑,这里我们使用Lua语言,通过发送对应的连接给Redis服务器,然后Redis会返回一个SHA1字符串,我们保存它,之后发送就可以只发送这个字符和对于的参数了。所以我们在UserRedPacketService接口中加入一个新的方法:

/** 通过Redis 实现抢红包* @param redPacketId 红包编号* @param userId 用户编号* @return * * 0 - 没有库存,失败* 1 - 成功,且不是最后一个红包* 2 - 成功,且是最后一个红包*/public Long grapRedPacketByRedis(Long redPacketId, Long userId);

实现类:

@Autowiredprivate RedisTemplate redisTemplate = null;@Autowiredprivate 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;@Overridepublic Long grapRedPacketByRedis(Long redPacketId, Long userId) {// TODO Auto-generated method stub//当前抢红包用户和日期String args = userId + "-" + System.currentTimeMillis();Long result = null;//获取底层Redis 操作对象Jedis jedis = (Jedis) redisTemplate.getConnectionFactory().getConnection().getNativeConnection();try {//如果脚本没有加载过,那么进行加载,这样就会返回一个shal编码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("userId:"+userId);System.out.println("Thread_name  = "+Thread.currentThread().getName());redisRedPacketService.saveUserRedPacketByRedis(redPacketId, unitAmount);}}finally {//确保jedis顺利关闭if(jedis != null && jedis.isConnected()) {jedis.close();}}return result;}

这里的Lua脚本,大概意思是这样的

判断是否存在可抢的库存,如果没有,就返回0,结束流程

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

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

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

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

 

6、新建一个controller方法

	@RequestMapping("/grapRedPacketByRedis")@ResponseBodypublic Map<String, Object> grapRedPacketByRedis(Long redPacketId, Long userId){//抢红包Long result = userRedPacketService.grapRedPacketByRedis(redPacketId, userId);Map<String,Object> res = new HashMap<String,Object>();boolean flag = result > 0;res.put("success", flag);res.put("message", flag?"抢红包成功":"抢红包失败");return res;}

 

7、在Redis上添加红包信息

 

8、数据库中 在t_red_packet里插入200000元,20000份红包,库存也是20000

 

count = 20000
保存数据结束,耗时10904毫秒,共20000条记录被保存。

10S,比乐观锁,悲观锁快的多。这里只是初步了解redis的概念,redis常用的技术和相关操作都需要继续学习。

详细代码可以在我GitHub上进行查看   ssm-redis

这篇关于SSM+Redis高并发抢红包之-Lua+Redis的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/919554

相关文章

Redis 的 SUBSCRIBE命令详解

《Redis的SUBSCRIBE命令详解》Redis的SUBSCRIBE命令用于订阅一个或多个频道,以便接收发送到这些频道的消息,本文给大家介绍Redis的SUBSCRIBE命令,感兴趣的朋友跟随... 目录基本语法工作原理示例消息格式相关命令python 示例Redis 的 SUBSCRIBE 命令用于订

sky-take-out项目中Redis的使用示例详解

《sky-take-out项目中Redis的使用示例详解》SpringCache是Spring的缓存抽象层,通过注解简化缓存管理,支持Redis等提供者,适用于方法结果缓存、更新和删除操作,但无法实现... 目录Spring Cache主要特性核心注解1.@Cacheable2.@CachePut3.@Ca

Web服务器-Nginx-高并发问题

《Web服务器-Nginx-高并发问题》Nginx通过事件驱动、I/O多路复用和异步非阻塞技术高效处理高并发,结合动静分离和限流策略,提升性能与稳定性... 目录前言一、架构1. 原生多进程架构2. 事件驱动模型3. IO多路复用4. 异步非阻塞 I/O5. Nginx高并发配置实战二、动静分离1. 职责2

Redis实现高效内存管理的示例代码

《Redis实现高效内存管理的示例代码》Redis内存管理是其核心功能之一,为了高效地利用内存,Redis采用了多种技术和策略,如优化的数据结构、内存分配策略、内存回收、数据压缩等,下面就来详细的介绍... 目录1. 内存分配策略jemalloc 的使用2. 数据压缩和编码ziplist示例代码3. 优化的

redis-sentinel基础概念及部署流程

《redis-sentinel基础概念及部署流程》RedisSentinel是Redis的高可用解决方案,通过监控主从节点、自动故障转移、通知机制及配置提供,实现集群故障恢复与服务持续可用,核心组件包... 目录一. 引言二. 核心功能三. 核心组件四. 故障转移流程五. 服务部署六. sentinel部署

基于Redis自动过期的流处理暂停机制

《基于Redis自动过期的流处理暂停机制》基于Redis自动过期的流处理暂停机制是一种高效、可靠且易于实现的解决方案,防止延时过大的数据影响实时处理自动恢复处理,以避免积压的数据影响实时性,下面就来详... 目录核心思路代码实现1. 初始化Redis连接和键前缀2. 接收数据时检查暂停状态3. 检测到延时过

Redis实现分布式锁全过程

《Redis实现分布式锁全过程》文章介绍Redis实现分布式锁的方法,包括使用SETNX和EXPIRE命令确保互斥性与防死锁,Redisson客户端提供的便捷接口,以及Redlock算法通过多节点共识... 目录Redis实现分布式锁1. 分布式锁的基本原理2. 使用 Redis 实现分布式锁2.1 获取锁

Redis中哨兵机制和集群的区别及说明

《Redis中哨兵机制和集群的区别及说明》Redis哨兵通过主从复制实现高可用,适用于中小规模数据;集群采用分布式分片,支持动态扩展,适合大规模数据,哨兵管理简单但扩展性弱,集群性能更强但架构复杂,根... 目录一、架构设计与节点角色1. 哨兵机制(Sentinel)2. 集群(Cluster)二、数据分片

Spring Security 前后端分离场景下的会话并发管理

《SpringSecurity前后端分离场景下的会话并发管理》本文介绍了在前后端分离架构下实现SpringSecurity会话并发管理的问题,传统Web开发中只需简单配置sessionManage... 目录背景分析传统 web 开发中的 sessionManagement 入口ConcurrentSess

redis数据结构之String详解

《redis数据结构之String详解》Redis以String为基础类型,因C字符串效率低、非二进制安全等问题,采用SDS动态字符串实现高效存储,通过RedisObject封装,支持多种编码方式(如... 目录一、为什么Redis选String作为基础类型?二、SDS底层数据结构三、RedisObject