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.xsd
       http://www.springframework.org/schema/aop 
       http://www.springframework.org/schema/aop/spring-aop.xsd
       http://www.springframework.org/schema/tx 
       http://www.springframework.org/schema/tx/spring-tx.xsd
       http://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");
	
	@Autowired
	private RedisTemplate redisTemplate = null;
	
	@Autowired
	private DataSource dataSource = null;
	
	@Override
	//开启新线程运行
	@Async
	public void saveUserRedPacketByRedis(Long redPacketId, Double unitAmount) {
		// TODO Auto-generated method stub
		
		System.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 stub
		Connection conn = null;
		Statement stmt = null;
		int[] count = null;
		try {
			conn = dataSource.getConnection();
			conn.setAutoCommit(false);
			stmt = conn.createStatement();
			for(UserRedPacket userRedPacket : listUserRedPacket) {
				//更新库存sql
				String sql1 = "update T_RED_PACKET set stock = stock-1 where id=" + userRedPacket.getRedPacketId();
				
				DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
				
				//插入抢红包用户信息sql
				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();
			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);

实现类:


	@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) {
		// 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")
	@ResponseBody
	public 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

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值