上面几次的超发现象,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