本文主要是介绍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的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!