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