目录
1. pojo层
2. dao层和映射文件
3. service接口和实现类
4. controller层
5. 超发现象
1.pojo层
RedPacket 红包类
// 实现序列化接口,这样就可以序列化对象
// 红包pojo
public class RedPacket implements Serializable {
private Long id; // 红包编号
private Long userId; // 发红包用户
private Double mount; // 红包金额
private Date sendDate; // 发红包时间
private Integer total; // 小红包总数
private Double unitAmount; // 单个小红包金额
private Integer stock; // 剩余小红包个数
private Integer version; // 版本
private String note; // 备注
/**setter and getter **/
}
UserRedPacket 用户抢红包类
// 用户抢红包表
public class UserRedPacket implements Serializable {
private Long id; // 编号
private Long redPacketId; // 用户编号
private Long userId; // 抢用户编号
private Double amount; // 抢红包金额
private Date grabTime; // 抢红包时间
private String note; // 备注
/** setter and getter **/
}
2.dao层和映射文件
RedPacketDao
// 红包DAO
@Repository
public interface RedPacketDao {
/**
* 获取红包信息
* @param id 红包id
* @return 红包具体信息
*/
RedPacket getRedPacket(Long id);
/**
* 扣减抢红包数
* @param id 红包id
* @return 更新记录条数
*/
int decreaseRedPacket(Long id);
}
其中两个方法,一个是查询红包,另一个是扣减红包库存。
抢红包逻辑: 先查询红包信息,看其是否拥有存量可以扣减。如果有存量,就扣减它,否则不扣减
UserRedPacketDao
@Repository
public interface UserRedPacketDao {
/**
* 插入抢红包信息
* @param userRedPacket 抢红包信息
* @return 影响记录数
*/
int grapRedPacket(UserRedPacket userRedPacket);
}
RedPacket.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.whc.dao.RedPacketDao">
<!--查询红包具体信息-->
<select id="getRedPacket" parameterType="long" resultType="cn.whc.pojo.RedPacket">
select id, user_id as userId, amount, send_date as sendDate, total, unit_amount as unitAmount, stock,
version, note from T_RED_PACKET where id = #{id}
</select>
<!--扣减抢红包库存-->
<update id="decreaseRedPacket">
update T_RED_PACKET set stock = stock - 1 where id = #{id}
</update>
</mapper>
UserRedPacket.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.whc.dao.UserRedPacketDao">
<!--使用了useGenerateKeys和keyProperty,返回数据库生成的主键信息-->
<insert id="grapRedPacket" parameterType="cn.whc.pojo.UserRedPacket" useGeneratedKeys="true" keyProperty="id">
insert into T_USER_RED_PACKET(red_packet_id, user_id, amount, grab_time, note)
values (#{redPacketId}, #{userId}, #{amount}, now(), #{note})
</insert>
</mapper>
3.service接口和实现类
UserRedPacketService
public interface UserRedPacketService {
/**
* 保存抢红包信息
* @param redPacketId 红包编号
* @param userId 抢红包用户编号
* @return 影响记录数
*/
int grapRedPacket(Long redPacketId, Long userId);
}
UserRedPacketServiceImpl
@Service("userRedPacketService")
public class UserRedPacketServiceImpl implements UserRedPacketService {
@Autowired
private UserRedPacketDao userRedPacketDao = null;
@Autowired
private RedPacketDao redPacketDao = null;
// 失败
private static final int FAILED = 0;
// 抢红包逻辑: 先获取红包信息,发现红包库存大于0,则抢红包并生成抢红包信息保存到数据库中
@Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRED)
public int grapRedPacket(Long redPacketId, Long userId) {
// 获取红包信息
RedPacket redPacket = redPacketDao.getRedPacket(redPacketId);
// 当前小红包库存大于0
if(redPacket.getStock() > 0) {
redPacketDao.decreaseRedPacket(redPacketId);
// 生成用户抢红包信息
UserRedPacket userRedPacket = new UserRedPacket();
// 设置红包编号
userRedPacket.setRedPacketId(redPacketId);
// 设置抢用户编号
userRedPacket.setUserId(userId);
// 设置抢红包金额
userRedPacket.setAmount(redPacket.getUnitAmount());
// 设置用户抢红包信息中的备注
userRedPacket.setNote("抢红包" + redPacketId);
// 往数据库中用户抢红包表中插入一条数据
int result = userRedPacketDao.grapRedPacket(userRedPacket);
return result;
}
// 失败返回
return FAILED;
}
}
grapRedPacket方法的逻辑是首先获取红包信息,如果发现红包库存大于0,则说明还有红包可抢,抢夺红包并生成抢红包的信息将其保存到数据库中。
配置事务注解@Transactional,让程序在事务中运行,以保证数据的一致性,采用读/写提交的隔离级别。
提高数据库的并发能力,传播行为采用Propagation.REQUIRED
4.controller层
UserRedPacketController
@Controller
@RequestMapping("/userRedPacket")
public class UserRedPacketController {
@Autowired
private UserRedPacketService userRedPacketService = null;
@RequestMapping(value = "/grapRedPacket")
@ResponseBody
public Map<String, Object> grapRedPacket(Long redPacketId, Long userId) {
// 抢红包
int result = userRedPacketService.grapRedPacket(redPacketId, userId);
Map<String, Object> resultMap = new HashMap<String, Object>();
boolean flag = result > 0;
resultMap.put("success", flag);
resultMap.put("message", flag ? "抢红包成功" : "抢红包失败");
return resultMap;
}
}
5.超发现象
<%@ page contentType="text/html;charset=UTF-8" language="java" isELIgnored="false" %>
<html>
<head>
<title>参数</title>
<%--加载JQuery文件--%>
<script type="text/javascript" src="${pageContext.request.contextPath}/static/js/jquery-1.7.2.js"></script>
<script type="text/javascript">
// 就绪函数,表示当前页面加载完毕后,直接执行里面的代码
$(function () {
// 模拟30000个异步请求,进行并发
var max = 30000;
for (var i = 1; i <= max; i++) {
$.ajax({
type: 'post',
url:"${pageContext.request.contextPath}/userRedPacket/grapRedPacket.do?redPacketId=1&userId=" + i,
success:function (result) {
}
})
}
})
</script>
</head>
<body>
</body>
</html>
用js异步发送请求模拟3万人同时抢红包场景,要使用火狐浏览器进行测试(Google测试的话大多请求丢失),抢夺id为1的红包,而数据库中是一个20万元的红包,一共有两万个。
注意两个问题: 数据的一致性和性能问题
-
数据一致性问题
出现了高并发的超发现象,两万个小红包,结果发出了20001个红包,现有库存为-1,超出之前的规定。
-
性能问题
最后一个红包和第一个红包的时间间隔,性能还是不错的,但是逻辑上存在超发错误,需要解决超发问题。
超发现象是由多线程下数据不一致造成的,对于解决此问题,通过悲观锁和乐观锁来处理,保证数据一致性,这两种方法的性能是不一样的