1. 程式人生 > >高併發搶紅包案列以及使用鎖,版本號,redis快取解決,專案可執行,詳細註釋(三)

高併發搶紅包案列以及使用鎖,版本號,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

井底之蛙 記錄