建议结合下一篇一起看

下一篇

数据结构+基础设施

数据结构

这里通过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——时间复杂度、动态数组

    O(n)不一定小于O(n^2),要具体来看,而我们说的这种时间复杂度其实是渐进时间复杂度,描述的是n趋近于无穷的情况. 动态数组的时间复杂度: 添加操作:O(n) addLast()的均摊复杂度为O( ...

  2. Indexing the World Wide Web: the Journey So Far阅读笔记

    文献文档用google搜索标题即可. term预处理:用空格切分,去除标点,去除撇号,归一化小写,去除变音符号,词干还原(?),去除停用词,挖掘词组. 索引选型工程最佳实践:term粒度.按doc分块 ...

  3. js监听dom元素内容变化

    $("#divid").bind('DOMNodeInserted', function(e) { alert('element now contains: ' + $(e.tar ...

  4. CAD安装失败怎样卸载CAD 2013?错误提示某些产品无法安装

    AUTODESK系列软件着实令人头疼,安装失败之后不能完全卸载!!!(比如maya,cad,3dsmax等).有时手动删除注册表重装之后还是会出现各种问题,每个版本的C++Runtime和.NET f ...

  5. MATLAB特殊矩阵以及矩阵转置

    特殊矩阵 通用特殊矩阵 zeros函数:产生全0矩阵,即零矩阵. ones函数:产生....1矩阵,即幺矩阵. eye函数:产生对角线为1的矩阵,当矩阵是方正时,得到单位矩阵. rand函数:产生(0 ...

  6. selinux下修改sshd端口号

    21 如果已开selinux,修改sshd配置文件  # vim /etc/ssh/sshd_config中的端口号后 重启SSH服务  # systemctl restart sshd.servic ...

  7. 37、解决 HTMLTestRunner 中文显示乱码的问题

    1.在自己的测试脚本中加入下面的代码并保存: # -.- coding:utf-8 -.- import sys reload(sys) sys.setdefaultencoding('utf-8') ...

  8. React.js 小书 Lesson7 - 组件的 render 方法

    作者:胡子大哈 原文链接:http://huziketang.com/books/react/lesson7 转载请注明出处,保留原文链接和作者信息. React.js 中一切皆组件,用 React. ...

  9. return break continue区别

    return:1.跳出整个方法体 2.返回值 function(a){return a=2}; break:跳出当前循环, continue:跳出当前判断继续执行

  10. 基于HTTP协议之WEB消息实时推送技术原理及实现

    很早就想写一些关于网页消息实时推送技术方面的文章,但是由于最近实在忙,没有时间去写文章.本文主要讲解基于 HTTP1.1 协议的 WEB 推送的技术原理及实现.本人曾经在工作的时候也有做过一些用到网页 ...