建议结合下一篇一起看

下一篇

数据结构+基础设施

数据结构

这里通过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();
}
}

通过redis实现的一个抢红包流程,仅做模拟【上】的更多相关文章

  1. 优化通过redis实现的一个抢红包流程【下】

    上一篇文章通过redis实现的抢红包通过测试发现有严重的阻塞的问题,抢到红包的用户很快就能得到反馈,不能抢到红包的用户很久(10秒以上)都无法获得抢红包结果,起主要原因是: 1.用了分布式锁,导致所有 ...

  2. Activity 学习(二) 搭建第一个Activity流程框架

    本次示例使用的IDER测试完成 测试背景 : xx饿了去饭店吃饭  需要先和服务员点餐  点完餐后服务员将菜品传递给厨师制作  制作完成后吃饱 一 :创建流程图 创建上一篇测试成功出现的BpmnFil ...

  3. 曹工说Redis源码(6)-- redis server 主循环大体流程解析

    文章导航 Redis源码系列的初衷,是帮助我们更好地理解Redis,更懂Redis,而怎么才能懂,光看是不够的,建议跟着下面的这一篇,把环境搭建起来,后续可以自己阅读源码,或者跟着我这边一起阅读.由于 ...

  4. ***Redis hash是一个string类型的field和value的映射表.它的添加、删除操作都是O(1)(平均)。hash特别适合用于存储对象

    http://redis.readthedocs.org/en/latest/hash/hset.html HSET HSET key field value   (存一个对象的时候key存) 将哈希 ...

  5. 给定一个字符串,仅由a,b,c 3种小写字母组成。

    package com.boco.study; /** * 题目详情 给定一个字符串,仅由a,b,c 3种小写字母组成. 当出现连续两个不同的字母时,你可以用另外一个字母替换它,如 有ab或ba连续出 ...

  6. Redis深入学习笔记(一)Redis启动数据加载流程

    这两年使用Redis从单节点到主备,从主备到一主多从,再到现在使用集群,碰到很多坑,所以决定深入学习下Redis工作原理并予以记录. 本系列主要记录了Redis工作原理的一些要点,当然配置搭建和使用这 ...

  7. 使用Redis List简单实现抢红包

    在这里不讨论抢红包的算法,只用redis简单尝试解决抢红包.借助redis单线程和List的POP方法. static void Main(string[] args) { IRedisHelper ...

  8. redis学习笔记——命令执行流程

    基础知识部分 如果需要掌握Redis的整个命令的执行过程,那么必须掌握一些基本的概念!否则根本看不懂,下面我就一些在我看来必备的基础知识进行总结,希望能为后面命令的整个执行过程做铺垫. 事件 Redi ...

  9. Redis hash 是一个 string 类型的 field 和 value 的映射表.它的添加、删除操作都是 O(1)(平均)。

    2.3 hashes 类型及操作 Redis hash 是一个 string 类型的 field 和 value 的映射表.它的添加.删除操作都是 O(1)(平均).hash 特别适合用于存储对象.相 ...

随机推荐

  1. java——字典树 Trie

    字典树是一种前缀树 package Trie; import java.util.TreeMap; public class Trie { private class Node{ public boo ...

  2. linux 命令(Ubuntu)

    1. 我们可以使用下列的命令压缩一个目录: # zip -r archive_name.zip directory_to_compress 2. 下面是如果解压一个zip文档: # unzip arc ...

  3. shell 命令下载软件 安装软件

    下载命令:wget URL地址 wget http://mirrors.163.com/centos/6/os/x86_64/Packages/yum-3.2.29-81.el6.centos.noa ...

  4. RTT学习之BSP

    ---恢复内容开始--- 一 根据相近型号的demo BSP进行修改制作自己的BSP https://github.com/RT-Thread/rt-thread/blob/master/bsp/st ...

  5. 浅谈soa之RESTful

    今晚打算花点时间整理一下面向服务的架构oap.1传统中小型项目架构一般是这样的:(java)html+servlet+jdbc.和(.net)html+handler+ado.net都是在一台应用来. ...

  6. B P5 第十三届北航程序设计竞赛预赛

    https://buaacoding.cn/contest-ng/index.html#/188/problems 其实这题挺简单的. 注意到答案的大小最多是22 二分,check长度是mid的不同子 ...

  7. (转)TCP连接的11种状态变迁

    自:http://blog.csdn.net/engrossment/article/details/8104482 http://blog.csdn.net/xiaofei0859/article/ ...

  8. pat1094. The Largest Generation (25)

    1094. The Largest Generation (25) 时间限制 200 ms 内存限制 65536 kB 代码长度限制 16000 B 判题程序 Standard 作者 CHEN, Yu ...

  9. [转]c#匿名类

    首先让我们看一个例子, 假设我们并没有Person类, 并且我们关心的属性只有Name和Age. 下面的代码演示了我们如何在没有声明类型的情况下来构建一个对象的: 1: var tom = new { ...

  10. Java中的阻塞队列-SynchronousQueue

    SynchronousQueue是一个不存储元素的阻塞队列.每一个put操作必须等待一个take操作,否则不能继续添加元素.SynchronousQueue可以看成是一个传球手,负责把生产者线程处理的 ...