在redis中首先设置红包的数量和金额,用户抢到红包之后,在redis中计算红包数量-1,保存用户的信息,直到红包被抢完。再将用户信息批量保存到数据库中。由于redis的计算是原子性的,所以不会出现数据错误,可以理解成atomic系列
具体的环境搭建请查看
https://blog.csdn.net/zzqtty/article/details/81741603
第一的和
第二
https://blog.csdn.net/zzqtty/article/details/81740104
的篇文章的搭建。springboot版本的下下来就可以直接用,改下连接之类的,那个大佬的连接我也给了的。
再写一次吧。。。
RedisConfig 配置了redis的连接信息
EnableCaching 关闭了
package test814RedPacket.config;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;import redis.clients.jedis.JedisPoolConfig;
/**
* @Description
* @Author zengzhiqiang
* @Date 2018年8月13日
*/
@Configuration
//EnableCaching 表示 Spring IoC 容器启动了缓存机制
//@EnableCaching
public class RedisConfig {@Bean(name = "redisTemplate")
public RedisTemplate initRedisTemplate(){
JedisPoolConfig poolConfig = new JedisPoolConfig();
//最大空闲数
poolConfig.setMaxIdle(50);
//最大连接数
poolConfig.setMaxTotal(100);
//最大等待毫秒数
poolConfig.setMaxWaitMillis(20000);
//创建 Jedis 连接工厂
JedisConnectionFactory connectionFactory = new JedisConnectionFactory(poolConfig);
connectionFactory.setHostName("localhost");
connectionFactory.setPort(6379);
//调用后初始化方法,没有它将抛出异常
connectionFactory.afterPropertiesSet();
//自定 Redis 序列化器
RedisSerializer jdkSerializationRedisSerializer = new JdkSerializationRedisSerializer();
RedisSerializer stringRedisSerializer = new StringRedisSerializer();
//定义 RedisTemplate 并设置连接工程
RedisTemplate redisTemplate = new RedisTemplate();
redisTemplate.setConnectionFactory(connectionFactory);
//设置序列化器
redisTemplate.setDefaultSerializer(stringRedisSerializer);
redisTemplate.setKeySerializer(stringRedisSerializer);
redisTemplate.setValueSerializer(stringRedisSerializer);
redisTemplate.setHashKeySerializer(stringRedisSerializer);
redisTemplate.setHashValueSerializer(stringRedisSerializer);
return redisTemplate;
}
/* @Bean(name="redisCacheManager")
public CacheManager initcCacheManager(@Autowired RedisTemplate redisTemplate){
RedisCacheManager cacheManager = new RedisCacheManager(redisTemplate);
//设置超时时间为 10 分钟,单位为秒
cacheManager.setDefaultExpiration(600);
//设置缓存名称
ListcacheNames = new ArrayList ();
cacheNames.add("redisCacheManager");
cacheManager.setCacheNames(cacheNames);
return cacheManager;
}*/
}
相当于application.xml 文件的配置
结构图
package test814RedPacket.config;
import java.util.Properties;
import java.util.concurrent.Executor;
import javax.sql.DataSource;
import org.apache.tomcat.dbcp.dbcp.BasicDataSourceFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.mapper.MapperScannerConfigurer;
import org.springframework.beans.factory.config.PropertyPlaceholderConfigurer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import org.springframework.context.annotation.ComponentScan.Filter;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Repository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.transaction.annotation.TransactionManagementConfigurer;
/**
* @Description
* @Author zengzhiqiang
* @Date 2018年8月13日
*/
@Configuration
//定义 Spring 扫描的包
@ComponentScan(value="test814RedPacket.*",includeFilters={@Filter(type=FilterType.ANNOTATION,value={Service.class})})
//使用事务驱动管理器
@EnableTransactionManagement
//实现接口 TransactionManagementConfigurer ,这样可以配置注解驱动事务
public class RootConfig implements TransactionManagementConfigurer{
private DataSource dataSource = null;
/**
* 设置日志
* @Description 这里有个坑,log4j的配置文件得放到源文件加的更目录下,src下才起作用,放包里不起作用,找了好久的错误
* @Param
* @Return
*/
@Bean(name="PropertiesConfigurer")
public PropertyPlaceholderConfigurer initPropertyPlaceholderConfigurer(){
PropertyPlaceholderConfigurer propertyLog4j = new PropertyPlaceholderConfigurer();
Resource resource = new ClassPathResource("log4j.properties");
propertyLog4j.setLocation(resource);
return propertyLog4j;
}
@Bean(name="Executor")
public Executor getAsyncExecutor(){
ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
taskExecutor.setCorePoolSize(5);
taskExecutor.setMaxPoolSize(10);
taskExecutor.setQueueCapacity(200);
taskExecutor.initialize();
return taskExecutor;
}
/**
* 配置数据库
*/
@Bean(name="dataSource")
public DataSource initDataSource(){
if(dataSource!=null){
return dataSource;
}
Properties props = new Properties();
props.setProperty("driverClassName", "com.mysql.jdbc.Driver");
props.setProperty("url", "jdbc:mysql://localhost:3306/t_role");
props.setProperty("username","root");
props.setProperty("password", "123456");
props.setProperty("maxActive", "200");
props.setProperty("maxIdle", "20");
props.setProperty("maxWait", "30000");
try {
dataSource = BasicDataSourceFactory.createDataSource(props);
} catch (Exception e) {
e.printStackTrace();
}
return dataSource;
}
/**
* 配置 SqlSessionFactoryBean,这里引入了spring-mybatis的jar包,是两个框架的整合
*/
@Bean(name="sqlSessionFactory")
public SqlSessionFactoryBean initSqlSessionFactory(){
SqlSessionFactoryBean sqlSessionFactory = new SqlSessionFactoryBean();
sqlSessionFactory.setDataSource(dataSource);
//配置 MyBatis 配置文件
Resource resource = new ClassPathResource("test814RedPacket/config/mybatis-config.xml");
sqlSessionFactory.setConfigLocation(resource);
return sqlSessionFactory;
}
/**
* 通过自动扫描,发现 MyBatis Mapper 接口
*/
@Bean
public MapperScannerConfigurer initMapperScannerConfigurer(){
MapperScannerConfigurer msc = new MapperScannerConfigurer();
//扫描包
msc.setBasePackage("test814RedPacket.*");
msc.setSqlSessionFactoryBeanName("sqlSessionFactory");
//区分注解扫描
msc.setAnnotationClass(Repository.class);
return msc;
}
/**
* 实现接口方法,注册注解事务 当@Transactonal 使用的时候产生数据库事务
*/
@Override
public PlatformTransactionManager annotationDrivenTransactionManager() {
DataSourceTransactionManager transactionManager = new DataSourceTransactionManager();
transactionManager.setDataSource(initDataSource());
return transactionManager;
}}
RedisRedPacketServiceimpl
从redis中拿数据保存到数据库中逻辑,数据持久化,先给代码后讲解下
package test814RedPacket.service.impl;
import io.netty.handler.codec.http.HttpHeaders.Values;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Timestamp;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.List;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 test814RedPacket.pojo.UserRedPacket;
import test814RedPacket.service.inf.RedisRedPacketService;/**
* @Description
* 解@Async 表示让 Spring 自动创建另外 条线程去运行它
* 这里是每次取出 1000
抢红包的信息,之所以这样做是为了避免取出 的数据过 导致 jvM 消耗过多的内存影响
系统性能。对于大批量的数据操作,这是我 在实际操作中要注意的,最后还会 redis
保存的链表信息,这样就帮助 Redis 释放内存了。对于数据库的保存,这里采用了 JDBC
的批量处理,每 1000 条批量保存1 次,使用 量有助于性能的提高
* @Author zengzhiqiang
* @Date 2018年8月16日
*/
@Service
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;
@Autowired
private DataSource dataSource;
@Override
@Async
public void saveUserRedPacketByRedis(int 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;
ListuserRedPacketList = new ArrayList ();
for(int i=0;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;jString args = userIdList.get(j).toString();
String[] arr = args.split("-");
String userIdStr = arr[0];
String timeStr = arr[1];
int userId = Integer.parseInt(userIdStr);
Long time = Long.parseLong(timeStr);
//生产抢红包信息
UserRedPacket userRedPacket = new UserRedPacket();
userRedPacket.setRedPacketId(redPacketId);
userRedPacket.setUserId(userId);
userRedPacket.setAmount(unitAmount);
userRedPacket.setGrabTime(new Timestamp(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 批量处理 Red is 缓存数据.
*/private int executeBatch(List
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 (Exception e2) {
e2.printStackTrace();
}
}
//返回插入抢红包数据记录
return count.length/2;
}
@Override
public Long grapRedPacketByRedis(int redPacketId, int userId) {
return null;
}
}
在redis中执行抢红包的逻辑和计算
package test814RedPacket.service.impl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.filter.ShallowEtagHeaderFilter;import redis.clients.jedis.Jedis;
import sun.font.Script;
import test814RedPacket.dao.RedPacketMapper;
import test814RedPacket.dao.UserRedPacketMapper;
import test814RedPacket.pojo.RedPacket;
import test814RedPacket.pojo.UserRedPacket;
import test814RedPacket.service.inf.RedisRedPacketService;
import test814RedPacket.service.inf.UserRedPacketService;/**
* @DescriptiongrapRedPacket 方法的逻辑是首先获取红包信息,如果发现红包库存大于 ,则说明
有红包可抢,抢夺红包并生成抢红包的信息将其保存到数据库中。要注意的是,数据库事
务方面的设置,代码中使用注解@Transactional 说明它会在 个事务中运行,这样就能够
保证所有的操作都是在-个事务中完成的。在高井发中会发生超发的现象,后面会看到超
发的实际测试。
* @Author zengzhiqiang
* @Date 2018年8月15日
*/
@Service
public class UserRedPacketServiceimpl implements UserRedPacketService{
@Autowired
private UserRedPacketMapper userRedPacketMapper;
@Autowired
private RedPacketMapper redPacketMapper;
private static final int FAILED = 0;
@Override
@Transactional(isolation=Isolation.READ_COMMITTED,propagation=Propagation.REQUIRED)
public int grabRedPacket(int redPacketId, int userId) {
//获取红包信息
// RedPacket redPacket = redPacketMapper.getRedPacket(redPacketId);
RedPacket redPacket = redPacketMapper.getRedPacketForUpdate(redPacketId);
//当前红包数大于0
if(redPacket.getStock()>0){
redPacketMapper.decreaseRedPacket(redPacketId);
//生成抢红包信息
UserRedPacket userRedPacket = new UserRedPacket();
userRedPacket.setRedPacketId(redPacketId);
userRedPacket.setUserId(userId);
userRedPacket.setAmount(redPacket.getUnitAmount());
userRedPacket.setNote("抢红包"+redPacketId);
//插入抢红包信息
int result = userRedPacketMapper.grapRedPacket(userRedPacket);
return result ;
}
return FAILED;
}
@Override
@Transactional(isolation=Isolation.READ_COMMITTED,propagation=Propagation.REQUIRED)
public int grabRedPacketForVersion(int redPacketId, int userId) {
// long start = System.currentTimeMillis();
// while(true){
// long end = System.currentTimeMillis();
// if(end-start>100){
// return FAILED;
// }
for (int i = 0; i < 3; i++) {
//获取红包信息,
RedPacket redPacket = redPacketMapper.getRedPacket(redPacketId);
//当前红包数大于0
if(redPacket.getStock()>0){
//再次传入线程保存的 version 旧值给 SQL 判断,是否有其他线程修改过数据
int update = redPacketMapper.decreaseRedPacketForVersion(redPacketId,redPacket.getVersion());
//如果没有数据更新,说明其他线程已经更新过数据,本次抢红包失败
if(update==0){
return FAILED;
}
/**
* version 开始就保存到了对象中,当扣减的时候,再次传递给 SQL ,让 SQl 对数
据库的 version 和当前线程的旧值 version 进行比较。如果 插入抢红包的数据,否则
就不进行操作。
*/
//生成抢红包信息
UserRedPacket userRedPacket = new UserRedPacket();
userRedPacket.setRedPacketId(redPacketId);
userRedPacket.setUserId(userId);
userRedPacket.setAmount(redPacket.getUnitAmount());
userRedPacket.setNote("抢红包"+redPacketId);
//插入抢红包信息
int result = userRedPacketMapper.grapRedPacket(userRedPacket);
return result ;
}
}
return FAILED;
}
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private RedisRedPacketService redisRedPacketService;
//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 位的 SHAl 编码,使用它去执行缓存的
//Lua 脚本
String shal = null;
@Override
public Long grapRedPacketByRedis(int redPacketId, int userId) {
///当前抢红包用户和日期信息
String args = userId+"-" +System.currentTimeMillis();
Long result = null;
///获取底层 Red is 操作对象
Jedis jedis = (Jedis) redisTemplate.getConnectionFactory().getConnection().getNativeConnection();
try{
///如果脚本没有加载过 那么进行加载,这样就会返回 shal 编码
if(shal==null){
shal = jedis.scriptLoad(script);
}
///执行脚本,返回结果
Object res = jedis.evalsha(shal,1,redPacketId+"",args);
result = (Long) res;
//返回 2为最后 1个红包,此时将 红包信息 过异步保存到数据库
if(result==2){
///获取单个 红包金额
System.out.println("红包被抢完了,准备保存到数据库了。。。。。。。。。。。。。。。。。。。。。。。。");
String unitAmountStr = jedis.hget("red_packet_"+redPacketId,"unit_amount");
///触发保存数据库操作
Double unitAmount = Double.parseDouble(unitAmountStr);
System.out.println("thread_name="+Thread.currentThread().getName());
redisRedPacketService.saveUserRedPacketByRedis(redPacketId, unitAmount);
}
}finally{
///确保 jedis 关闭
if(jedis!=null&&jedis.isConnected()){
jedis.close();
}
}
return result;
}
}
讲解下吧
//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";
这个就是逻辑。
把用户信息保存到red_packet_list_5(5表示大红包的编号,比如我抢的是第5个红包)这个集合中
red_packet_5 红包信息
之后再初始化的时候会设置redis中的值
hset red_packet_5 stock 2000 红包额个数
hset red_packet_5 unit_amount 1 每个多说钱
如果抢完了就返回2 ,表示结束
在保存的时候
private static final String PREFIX = "red_packet_list_";
//获取列表操作对象
BoundListOperations ops = redisTemplate.boundListOps(PREFIX+redPacketId);
表示的就是刚才redis脚本中的
把用户信息保存到red_packet_list_5(5表示大红包的编号,比如我抢的是第5个红包)这个集合中
注释中可以理解
当然你可以不用脚本,每次取出来就行逻辑判断,哪个大佬就是这么做的。可以自己研究下
flushall
hset red_packet_14 stock 2000
hset red_packet_14 unit_amount 1
hget red_packet_14 stock
井底之蛙 记录