建议结合下一篇一起看

下一篇

数据结构+基础设施

数据结构

这里通过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. my25_Mysql操作技巧汇总

    1. drop database 在数据量很大的情况下,最好先对表进行truncate,然后再drop database:不然会卡住很长的时间. 2. 数据的逻辑导入导出 如果数据量大,又需要进行逻辑 ...

  2. SQL SERVER LEFT JOIN, INNER JOIN, RIGHT JOIN

    JOIN: 如果表中有至少一个匹配,则返回行 LEFT JOIN: 即使右表中没有匹配,也从左表返回所有的行 RIGHT JOIN: 即使左表中没有匹配,也从右表返回所有的行 FULL JOIN: 只 ...

  3. Eclipse代码规范工具-Checkstyle安装和使用

    您首先可以参考这里:http://www.ibm.com/developerworks/cn/java/j-ap01117/index.html 那么首先您应该下载CheStyle: http://s ...

  4. Javascript兼容性问题汇总

    一.属性相关 我们通常把特征(attribute)和属性(property)统称为属性,但是他们确实是不同的概念, 特征(attribute)会表现在HTML文本中,对特征的修改一定会表现在元素的ou ...

  5. PHP函数的引用传递(地址传递)

    PHP中的引用: 在PHP中,变量名和变量内容是不一样的,因此同样的内容可以有不同的名字.在PHP中引用意味着用不同的名字访问同一个变量的内容. 比如:$a = 'hello world'; $b = ...

  6. 修改mysql表的字符集

    ALTER TABLE logtest CONVERT TO CHARACTER SET utf8 COLLATE utf8_general_ci; 修改数据库字符集: 代码如下: ALTER DAT ...

  7. Java 之 Serializable 序列化和反序列化的概念,作用的通俗易懂的解释

    遇到这个 Java Serializable 序列化这个接口,我们可能会有如下的问题a,什么叫序列化和反序列化b,作用.为啥要实现这个 Serializable 接口,也就是为啥要序列化c,seria ...

  8. C#中事件的一些总结

    事件中  sender和e  事件中不同的对象 object  sender  是事件的对象 eventages e   是事件对象传递过来的参数对象 如 private button_click(O ...

  9. JavaScript 数组(Array)

    //声明方式 //调用 Array构造函数创建并赋值 var users = new Array(); //new 可以省略 ); //new 可以省略 var users3 = new Array( ...

  10. springboot2.x如何配置全局自定义异常

    为什么要捕获异常? 我们开发中,经常运行时,代码会报错,这时候我们有可能抛出异常,而不是用try..catch来解决.而且现在前后端分离,如果不捕获异常的话,前端那边的人估计会被报的错搞得焦头烂额的. ...