Redis使用的是内存,内存的速度比磁盘速度肯定要快很多.。使用 Redis实现抢红包,需要知道的是Redis的功能不如数据库强大,事务也不是很完整.因此要保证数据的正确性,可以通过严格的验证得以保证。而 Redis的 Lua 语言是原子性的,且功能更为强大,所以优先选择使用Lua语言来实现抢红包。
在 Redis 当中存储,始终都不是长久之计 , 因为 Redis并非一个长久储存数据的地方,更多的时候只是为了提供更为快速的缓存,所以当红包金额为 0 或者红包超时的时候(超时操作可以使用定时机制实,这里暂不讨论), 会将红包数据保存到数据库中, 这样才能够保证数据的安全性和严格性。
注解方式配置 Redis
@Configuration
public class RedisHepler { @Bean(name = "redisTemplate") public RedisTemplate initRedisTemplate(LettuceConnectionFactory connectionFactory) { RedisTemplate<String, Serializable> redisTemplate = new RedisTemplate<>(); redisTemplate.setKeySerializer(new StringRedisSerializer()); redisTemplate.setValueSerializer(new StringRedisSerializer()); redisTemplate.setHashKeySerializer(new StringRedisSerializer()); redisTemplate.setHashValueSerializer(new StringRedisSerializer()); redisTemplate.setConnectionFactory(connectionFactory); return redisTemplate; } @Bean public DefaultRedisScript<String> defaultRedisScript() { DefaultRedisScript<String> defaultRedisScript = new DefaultRedisScript<>(); defaultRedisScript.setResultType(String.class); defaultRedisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("lua/demo.lua"))); return defaultRedisScript; } }
lua脚本和异步持久化功能的开发
Redis 中的 Lua 语言是一种原子性的操作,可以保证数据的一致性 。
local listKey = 'red_packet_list_'..KEYS[1] local redPacket = 'red_packet_'..KEYS[1] local stock=tonumber(redis.call('hget',redPacket,'stock')) if stock<=0 then return 0 end stock =stock-1 redis.call('hset',redPacket,'stock',tostring(stock)) redis.call('rpush',listKey,ARGV[1]) if stock==0 then return 2 end return 1
流程:
判断是否存在可抢的库存,如果己经没有可抢夺的红包,则返回为 0,结束流程
有可抢夺的红包,对于红包的库存减1 ,然后重新设置库存
将抢红包数据保存到 Redis 的链表当中,链表的 key 为 red_packet_list_ {id}
如果当前库存为 0 ,那么返回 2,这说明可以触发数据库对 Redis 链表数据的保存,链表的 key 为 red_packet_ list_ {id},它将保存抢红包的用户名和抢的时间
如果当前库存不为 0 ,那么将返回 1,这说明抢红包信息保存成功。
当返回为 2 的时候,说明红包己经没有库存,会触发数据库对链表数据的保存, 这是一个大数据量的保存。为了不影响最后一次抢红包的响应,在实际的操作中往往会考虑使用 JMS 消息发送到别的服务器进行操作
接口
package com.smart.service; public interface RedisRedPacketService { /** * 保存redis抢红包列表 * @param redPacketId * @param unitAmount */ void saveUserRedPacketByRedis(Long redPacketId, Double unitAmount); }
实现类
package com.smart.service.impl; import com.smart.model.UserRedPacket; import com.smart.service.RedisRedPacketService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; 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 javax.sql.DataSource; import java.sql.Connection; import java.sql.SQLException; import java.sql.Statement; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.List; @Service public class RedisRedPacketServiceImpl implements RedisRedPacketService { private static Logger logger = LoggerFactory.getLogger(RedisRedPacketServiceImpl.class); private static final String PREFIX = "red_packet_list_"; /** * 每次取出1000条数据 */ private static final int TIME_SIZE = 1000; @Autowired private RedisTemplate redisTemplate; @Autowired private DataSource dataSource; @Async public void saveUserRedPacketByRedis(Long redPacketId, Double unitAmount) { logger.info("----------开始保存数据----------------"); 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> userRedPackets=new ArrayList<>(TIME_SIZE); for(int i=0;i<times;i++){ 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); } userRedPackets.clear(); for(int j=0;j<userIdList.size();j++){ String values= userIdList.get(j).toString(); String[] arr = values.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.setGrabTime(new Date(time)); userRedPacket.setAmount(unitAmount); userRedPacket.setNote("抢红包 "+redPacketId); userRedPackets.add(userRedPacket); } count+=executeBatch(userRedPackets); } redisTemplate.delete(PREFIX+redPacketId); long end=System.currentTimeMillis(); System.out.println("保存数据结束,耗时" + (end - start) + "毫秒,共" + count + "条记录被保存"); } private int executeBatch(List<UserRedPacket> userRedPackets) { Connection conn=null; Statement statement=null; int[] count=null; try{ conn = dataSource.getConnection(); conn.setAutoCommit(false); statement = conn.createStatement(); for(UserRedPacket userRedPacket :userRedPackets){ 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() + "')"; statement.addBatch(sql1); statement.addBatch(sql2); } count= statement.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 自动创建另外一条线程去运行它,这样它便不在抢最后一个红包的线程之内。因为这个方法是一个较长时间的方法,在同一个线程内,那么对于最后抢红包的用户需要等待的时间太长,用户体验不好
这里是每次取出 1 000 个抢红包的信息,之所以这样做是为了避免取出 的数据过大 , 导致JVM 消耗过多的内存影响系统性能。
对于大批量的数据操作,在实际操作中要注意的,最后还会删除 Redis保存的链表信息,这样就帮助 Redis 释放内存,对于数据库的保存 ,这里采用了 JDBC的批量处理,每 1000 条批量保存一次,使用批量有助于性能的提高。
Service层添加Redis抢红包的逻辑
UserRedPacketService接口新增接口方法grapRedPacketByRedis
/** * 通过Redis实现抢红包 * * @param redPacketId --红包编号 * @param userId -- 用户编号 * @return 0-没有库存,失败 1--成功,且不是最后一个红包 2--成功,且是最后一个红包 */ public Long grapRedPacketByRedis(Long redPacketId, Long userId);
实现类
package com.smart.service.impl; import com.smart.service.RedisRedPacketService; import com.smart.service.UserRedPacketService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.connection.RedisConnection; import org.springframework.data.redis.connection.ReturnType; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.script.DefaultRedisScript; import org.springframework.stereotype.Service; import java.io.Serializable; @Service public class UserRedPacketServiceImpl implements UserRedPacketService { @Autowired RedisTemplate<String,Serializable> redisTemplate; @Autowired RedisRedPacketService redisRedPacketService; @Autowired DefaultRedisScript<String> redisScript; @Override public Long grapRedPacketByRedis(Long redPacketId, Long userId) { String args=userId+"-"+System.currentTimeMillis(); String key=String.valueOf(redPacketId); Object res = redisTemplate.execute((RedisConnection connection) -> connection.eval( redisScript.getScriptAsString().getBytes(), ReturnType.INTEGER, 1, key.getBytes(), args.getBytes())); Long result= (Long) res; if(result==2){ String unitAmountStr = (String) redisTemplate.opsForHash().get("red_packet_" + redPacketId,"unit_amount"); Double unitAmount = Double.valueOf(unitAmountStr); redisRedPacketService.saveUserRedPacketByRedis(redPacketId,unitAmount); } return result; } }
Controller层新增路由方法
package com.smart.controller; import com.smart.service.UserRedPacketService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; import java.util.HashMap; import java.util.Map; @Controller @RequestMapping("/userRedPacket") public class UserRedPacketController { @Autowired private UserRedPacketService userRedPacketService; @RequestMapping("/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 上添加红包信息
127.0.0.1:6379> HMSET red_packet_1 stock 20000 unit_amount 10
OK
grapRedPacket.html
<!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>参数</title> <!-- 加载Query文件--> <script type="text/javascript" src="https://code.jquery.com/jquery-3.2.0.js"> </script> <script type="text/javascript"> $(document).ready(function () { //模拟30000个异步请求,进行并发 var max = 30000; for (var i = 1; i <= 1; i++) { $.post({ //请求抢id为1的红包 //根据自己请求修改对应的url和大红包编号 url: "./userRedPacket/grapRedPacketByRedis?redPacketId=1&userId="+i, //成功后的方法 success: function (result) { document.write(i); if(i%1000==0){ document.write("\n"); } } }); } }); </script> </head> <body> </body> </html>
访问:http://localhost:8080/grapRedPacket.html