建议结合下一篇一起看
数据结构+基础设施
数据结构
这里通过spring-data-jpa+mysql实现DB部分的处理,其中有lombok的参与
@MappedSuperclass
@Data
@NoArgsConstructor
@AllArgsConstructor
public class BaseEntity {//公共基础实体字段
@Id //标识主键 公用主键
@GeneratedValue //递增序列
private Long id;
@Column(updatable = false) //不允许修改
@CreationTimestamp //创建时自动赋值
private Date createTime;
@UpdateTimestamp //修改时自动修改
private Date updateTime;
}
@Entity //标识这是个jpa数据库实体类
@Table
@Data //lombok getter setter tostring
@ToString(callSuper = true) //覆盖tostring 包含父类的字段
@Slf4j //SLF4J log
@Builder //biulder模式
@NoArgsConstructor //无参构造函数
@AllArgsConstructor //全参构造函数
@JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
public class RedPacketInfo extends BaseEntity implements Serializable {//红包信息表
private String red_packet_id;//红包ID
private int total_amount;//总金额
private int total_packet;//总红包数
private int remaining_amount;//剩余金额
private int remaining_packet;//剩余红包数
private String user_id;//发红包用户ID
}
@Entity //标识这是个jpa数据库实体类
@Table
@Data //lombok getter setter tostring
@ToString(callSuper = true) //覆盖tostring 包含父类的字段
@Slf4j //SLF4J log
@Builder //biulder模式
@NoArgsConstructor //无参构造函数
@AllArgsConstructor //全参构造函数
@JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
public class RedPacketRecord extends BaseEntity implements Serializable {//抢红包记录表
private int amount;
private String red_packet_id;
private String user_id;
}
REDIS数据结构
REDIS对于一个红包存储3部分信息:
1、KEY:红包ID+_TAL_PACKET VALUE:红包剩余数量
2、KEY:红包ID+_TOTAL_AMOUNT VALUE:红包剩余金额
3、KEY:红包ID+_lock VALUE:红包分布式锁
操作REDIS基础方法
private static final TimeUnit SECONDS = TimeUnit.SECONDS;
private static final long DEFAULT_TOMEOUT = 5;
private static final int SLEEPTIME = 50; /**
* 获取分布式锁 2019
* @param lockKey
* @param timeout
* @param unit
*/
public boolean getLock(String lockKey, String value, long timeout, TimeUnit unit){
boolean lock = false;
while (!lock) {
//设置key自己的超时时间
lock = redisTemplate.opsForValue().setIfAbsent(lockKey, value,timeout,unit);
if (lock) { // 已经获取了这个锁 直接返回已经获得锁的标识
return lock;
}
try {
//暂停50ms,重新循环
Thread.sleep(SLEEPTIME);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return lock;
} /**
* 按照默认方式获得分布式锁 2019
* @param lockKey
* @return
*/
public boolean getLock(String lockKey){
return getLock(lockKey,String.valueOf(new Date().getTime()),DEFAULT_TOMEOUT,SECONDS);
}
/**
* 获取指定 key 的值
*
* @param key
* @return
*/
public String get(String key) {
return redisTemplate.opsForValue().get(key);
} /**
* 设置指定 key 的值
*
* @param key
* @param value
*/
public void set(String key, String value) {
redisTemplate.opsForValue().set(key, value);
}
DAO
public interface RedPacketInfoRepository extends JpaRepository<RedPacketInfo, Long> {
@Query("select o from RedPacketInfo o where o.red_packet_id=:redPacketId")
public RedPacketInfo findByRedPacketId(@Param("redPacketId") String redPacketId);
}
public interface RedPacketRecordRepository extends JpaRepository<RedPacketRecord,Long> {
}
配置
@Component
@EnableAsync//开启异步注解,回写处
public class RedPacketConfig implements ApplicationRunner {
//启动自动发一个红包
@Autowired
RedPacketService redPacketService;
@Override
public void run(ApplicationArguments args) throws Exception {
String userId = "001";
redPacketService.handOut(userId,10000,20);
} /**
* 引入随机数组件
* @return
*/
@Bean
public RandomValuePropertySource randomValuePropertySource(){
return new RandomValuePropertySource("RedPackeRandom");
}
}
发红包
发红包通常没有特别需要处理高并发的点
/**
* 发红包
* @param userId
* @param total_amount 单位为分,不允许有小数点
* @param tal_packet
* @return
*/
public RedPacketInfo handOut(String userId,int total_amount,int tal_packet){
RedPacketInfo redPacketInfo = new RedPacketInfo();
redPacketInfo.setRed_packet_id(genRedPacketId(userId));
redPacketInfo.setTotal_amount(total_amount);
redPacketInfo.setTotal_packet(tal_packet);
redPacketInfo.setRemaining_amount(total_amount);
redPacketInfo.setRemaining_packet(tal_packet);
redPacketInfo.setUser_id(userId);
redPacketInfoRepository.save(redPacketInfo); redisUtil.set(redPacketInfo.getRed_packet_id()+TAL_PACKET, tal_packet+"");
redisUtil.set(redPacketInfo.getRed_packet_id()+TOTAL_AMOUNT, total_amount+""); return redPacketInfo;
}
/**
* 组织红包ID
* @return
*/
private String genRedPacketId(String userId){
String redpacketId = userId+"_"+new Date().getTime()+"_"+redisUtil.incrBy("redpacketid",1);
return redpacketId;
}
抢红包
详见代码注释
/**
* 抢红包
* @param userId
* @param redPacketId
* @return
*/
public GrabResult grab(String userId, String redPacketId){
Date begin = new Date();
String msg = "红包已经被抢完!";
boolean resultFlag = false;
double amountdb = 0.00; try{
//抢红包的过程必须保证原子性,此处加分布式锁
if(redisUtil.getLock(redPacketId+"_lock")) {
RedPacketRecord redPacketRecord = new RedPacketRecord().builder().red_packet_id(redPacketId)
.user_id(userId).build();
//如果没有红包了,则返回
if (Integer.parseInt(redisUtil.get(redPacketId + TAL_PACKET)) <= 0) {
}else {
//抢红包过程
//获取剩余金额 单位分
int remaining_amount = Integer.parseInt(redisUtil.get(redPacketId + TOTAL_AMOUNT));
//获取剩余红包数
int remaining_packet = Integer.parseInt(redisUtil.get(redPacketId + TAL_PACKET));
//计算本次抢红包金额
//计算公式:remaining_amount/remaining_packet*2
//如果只剩下一个红包,则余额全由这次的人获得
int amount = remaining_amount;
if (remaining_packet != 1) {
int maxAmount = remaining_amount / remaining_packet * 2;
amount = Integer.parseInt(randomValuePropertySource.getProperty("random.int[0," + maxAmount + "]").toString());
}
//与redis进行incrBy应该原子,并且2次与redis交互还有一定性能消耗,通过lua脚本实现更为妥当
redisUtil.incrBy(redPacketId + TAL_PACKET, -1);
redisUtil.incrByFloat(redPacketId + TOTAL_AMOUNT, -amount);
//准备返回结果
redPacketRecord.setAmount(amount);
amountdb = amount / 100.00;
msg = "恭喜你抢到红包,红包金额" + amountdb + "元!";
resultFlag = true;
//异步记账
try {
redPacketCallBackService.callback(userId, redPacketId,
Integer.parseInt(redisUtil.get(redPacketId + TAL_PACKET)),
Integer.parseInt(redisUtil.get(redPacketId + TOTAL_AMOUNT)),
amount);
} catch (Exception e) {
log.error(e.getMessage(), e);
}
}
}
}finally {
//解锁redis分布式锁
redisUtil.unLock(redPacketId+"_lock");
}
Date end = new Date();
System.out.println(msg+",剩余红包:"+redisUtil.get(redPacketId + TAL_PACKET)+"个,本次抢红包消耗:"+(end.getTime()-begin.getTime())+"毫秒");
return new GrabResult().builder().msg(msg).resultFlag(resultFlag).amount(amountdb).red_packet_id(redPacketId).user_id(userId).build(); }
异步入账
/**
* @program: redis
* @description: 回写信息
* @author: X-Pacific zhang
* @create: 2019-04-30 11:36
**/
@Service
public class RedPacketCallBackService {
@Autowired
private RedPacketInfoRepository redPacketInfoRepository; @Autowired
private RedPacketRecordRepository redPacketRecordRepository;
/**
* 回写红包信息表、抢红包表
*/
@Async
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void callback(String userId,String redPacketId,int remaining_packet,int remaining_amount,int amount) throws Exception {
//校验
RedPacketInfo redPacketInfo = redPacketInfoRepository.findByRedPacketId(redPacketId);
if(redPacketInfo.getRemaining_packet() <= 0 || redPacketInfo.getRemaining_amount() < amount){
throw new Exception("红包余额错误,本次抢红包失败!");
}
//先更新红包信息表
redPacketInfo.setRemaining_packet(remaining_packet);
redPacketInfo.setRemaining_amount(remaining_amount);
redPacketInfoRepository.save(redPacketInfo);
//新增抢红包信息
RedPacketRecord redPacketRecord = new RedPacketRecord().builder()
.user_id(userId).red_packet_id(redPacketId).amount(amount).build();
redPacketRecordRepository.save(redPacketRecord);
}
}
测试抢红包
@Test
public void testConcurrent(){
String redPacketId = "001_1556677154968_19";
// System.out.println(redPacketInfoRepository.findByRedPacketId("001_1556619425512_5"));
Date begin = new Date();
for(int i = 0;i < 200;i++) {
Thread thread = new Thread(() -> {
String userId = "user_" + randomValuePropertySource.getProperty("random.int(10000)").toString();
redPacketService.grab(userId, redPacketId);
});
thread.start();
}
Date end = new Date();
System.out.println("合计消耗:"+(end.getTime() - begin.getTime()));
try {
Thread.sleep(100000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}