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(10) -- zset类型命令使用

zset是有序集合,内部除了存储元素外,还会存储一个score,存储在zset中的元素会按照score的大小升序排列,不同元素的score可以重复,score相同的元素会按照元素的字典序排列。 1. zset常用命令 1.1 zadd  zadd key [NX | XX] [GT | LT]   [CH] [INCR] score member [score member ...]

高并发环境中保持幂等性

在高并发环境中保持幂等性是一项重要的挑战。幂等性指的是无论操作执行多少次,其效果都是相同的。确保操作的幂等性可以避免重复执行带来的副作用。以下是一些保持幂等性的常用方法: 唯一标识符: 请求唯一标识:在每次请求中引入唯一标识符(如 UUID 或者生成的唯一 ID),在处理请求时,系统可以检查这个标识符是否已经处理过,如果是,则忽略重复请求。幂等键(Idempotency Key):客户端在每次

Java并发编程之——BlockingQueue(队列)

一、什么是BlockingQueue BlockingQueue即阻塞队列,从阻塞这个词可以看出,在某些情况下对阻塞队列的访问可能会造成阻塞。被阻塞的情况主要有如下两种: 1. 当队列满了的时候进行入队列操作2. 当队列空了的时候进行出队列操作123 因此,当一个线程试图对一个已经满了的队列进行入队列操作时,它将会被阻塞,除非有另一个线程做了出队列操作;同样,当一个线程试图对一个空

Redis中使用布隆过滤器解决缓存穿透问题

一、缓存穿透(失效)问题 缓存穿透是指查询一个一定不存在的数据,由于缓存中没有命中,会去数据库中查询,而数据库中也没有该数据,并且每次查询都不会命中缓存,从而每次请求都直接打到了数据库上,这会给数据库带来巨大压力。 二、布隆过滤器原理 布隆过滤器(Bloom Filter)是一种空间效率很高的随机数据结构,它利用多个不同的哈希函数将一个元素映射到一个位数组中的多个位置,并将这些位置的值置

Lua 脚本在 Redis 中执行时的原子性以及与redis的事务的区别

在 Redis 中,Lua 脚本具有原子性是因为 Redis 保证在执行脚本时,脚本中的所有操作都会被当作一个不可分割的整体。具体来说,Redis 使用单线程的执行模型来处理命令,因此当 Lua 脚本在 Redis 中执行时,不会有其他命令打断脚本的执行过程。脚本中的所有操作都将连续执行,直到脚本执行完成后,Redis 才会继续处理其他客户端的请求。 Lua 脚本在 Redis 中原子性的原因

java线程深度解析(五)——并发模型(生产者-消费者)

http://blog.csdn.net/Daybreak1209/article/details/51378055 三、生产者-消费者模式     在经典的多线程模式中,生产者-消费者为多线程间协作提供了良好的解决方案。基本原理是两类线程,即若干个生产者和若干个消费者,生产者负责提交用户请求任务(到内存缓冲区),消费者线程负责处理任务(从内存缓冲区中取任务进行处理),两类线程之

java线程深度解析(四)——并发模型(Master-Worker)

http://blog.csdn.net/daybreak1209/article/details/51372929 二、Master-worker ——分而治之      Master-worker常用的并行模式之一,核心思想是由两个进程协作工作,master负责接收和分配任务,worker负责处理任务,并把处理结果返回给Master进程,由Master进行汇总,返回给客

由Lua 粘合的Nginx生态环境

转自:http://blog-zq-org.qiniucdn.com/pyblosxom/oss/openresty-intro-2012-03-06-01-13.html -- agentzh tech-club.org 演讲听录 免责聲明 Lua 粘合的 Nginx 生态环境 2.1. openresty 2.2. 配置小语言 2.3. ngx_drizzle 2.4.

深入解析秒杀业务中的核心问题 —— 从并发控制到事务管理

深入解析秒杀业务中的核心问题 —— 从并发控制到事务管理 秒杀系统是应对高并发、高压力下的典型业务场景,涉及到并发控制、库存管理、事务管理等多个关键技术点。本文将深入剖析秒杀商品业务中常见的几个核心问题,包括 AOP 事务管理、同步锁机制、乐观锁、CAS 操作,以及用户限购策略。通过这些技术的结合,确保秒杀系统在高并发场景下的稳定性和一致性。 1. AOP 代理对象与事务管理 在秒杀商品

PostgreSQL中的多版本并发控制(MVCC)深入解析

引言 PostgreSQL作为一款强大的开源关系数据库管理系统,以其高性能、高可靠性和丰富的功能特性而广受欢迎。在并发控制方面,PostgreSQL采用了多版本并发控制(MVCC)机制,该机制为数据库提供了高效的数据访问和更新能力,同时保证了数据的一致性和隔离性。本文将深入解析PostgreSQL中的MVCC功能,探讨其工作原理、使用场景,并通过具体SQL示例来展示其在实际应用中的表现。 一、