高并发抢红包案列以及使用锁,版本号,redis缓存解决,项目可运行,详细注释(三)

1redis抢红包实现

在redis中首先设置红包的数量和金额,用户抢到红包之后,在redis中计算红包数量-1,保存用户的信息,直到红包被抢完。再将用户信息批量保存到数据库中。由于redis的计算是原子性的,所以不会出现数据错误,可以理解成atomic系列

具体的环境搭建请查看

https://blog.csdn.net/zzqtty/article/details/81741603

第一的和

第二

https://blog.csdn.net/zzqtty/article/details/81740104

的篇文章的搭建。springboot版本的下下来就可以直接用,改下连接之类的,那个大佬的连接我也给了的。

2.发布失败!!!!把我刚才写的都没了 。。。微笑。。。

再写一次吧。。。

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);
        //设置缓存名称
        List<String> cacheNames = new ArrayList<String>();
        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;
        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];
                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<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 (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

井底之蛙 记录

猜你喜欢

转载自blog.csdn.net/zzqtty/article/details/81901325
今日推荐