【SSM抢红包简单项目】 ---- (四) 使用Redis抢红包

目录

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>

在这里插入图片描述在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
没有出现超发情况,而且性能比乐观锁和悲观锁的情况高效

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值