分布式商城秒杀实现

版权声明:个人收集,转载注明,谢谢支持 https://blog.csdn.net/qq_42629110/article/details/84839963

秒杀工程的演变,在一致性要求下面对高并发和高速读写

秒杀

页面倒计时

<script th:inline="javascript">
    //抢购URL
    function kill(){
        location.href="/kill/gokill?id=" + [[${kill.id}]] + "&gnumber=1";
    }

    //js的定时器
    // setTimeout(function(){}, 1000);  隔1秒后,执行该方法一次,仅仅只会执行一次
    // setInterval(function(){}, 1000); 每隔1秒都会执行一次
	//使用setInterval+递归解决用户第一秒显示00的问题
	
    //实现倒计时
    function time(){
        //秒杀开始的时间
        var begin = new Date([[${kill.starttime}]]);
        //获得当前时间
        var now = new Date();//获取时间服务器 - ajax

        //计算相差多久
        var howtime = begin - now;

        if(howtime > 0){
            //计算倒计时
            var day = format(parseInt(howtime/1000/60/60/24));
            var hour = format(parseInt(howtime/1000/60/60%24));
            var min = format(parseInt(howtime/1000/60%60));
            var second = format(parseInt(howtime/1000%60));

            var showtime = day + "天" + hour + "时" + min + "分" + second + "秒";
            $("#span_id").html(showtime);

            setTimeout(function(){
                time();
            }, 1000);

        } else {
            //秒杀开始
            // alert("秒杀开始");
            $("#btn1").attr("disabled",false);
        }
    }
    //调用方法
    time();
    
    // setInterval(function(){
    //     time();
    // }, 1000);
    //格式化数字显示为 00:00:00
    function format(number){
        if(number < 10){
            return "0" + number;
        }
        return number;
    }
</script>

秒杀核心service的演变

一.简单的service逻辑:
1.查询kill库存
2.根据库存判断是否秒杀成功
3.成功则生成订单

@Override
@Transactional
public int kill(Integer id, Integer gnumber, int uid) {

    //先查询kill库存
    Kill kill = killDao.queryKillById(id);
    //库存不足直接返回0
    if(kill.getSave() <= 0){
        return 0;
    }
    //库存足够
    //减库存
    killDao.updateSave(id,gnumber);
    //插入订单
    Orders orders = new Orders();
    orders.setOrdertime(new Date());
    orders.setOrderid(UUID.randomUUID().toString());
    orders.setUid(uid);

    orderDao.insertOrder(orders);
    return 1;
}

存在问题

秒杀一个高并发和高速读写的场景,我们并不能简单的操作数据库
1.解决数据一致性,因为高并发多线程访问产生的超买情况
2.解决数据库的高速读写问题,减小数据的负载

问题解决

演变:

1.业务层共享资源加锁

1.1悲观锁的思想

1.1.1 代码同步

我们直接在方法上添加synchronized,使用同步代码块,或者互斥锁,也就是简单的给业务层的共享资源上锁

确实可以保证我们的数据一致性,但是带来的是服务器处理请求能力的大幅度下降

1.1.2 数据库使用排它锁加锁

<select id="queryKillById">
    select save from t_kill where id = #{id}
</select>

我们在查询秒杀库存的时候添加for update,也就是我们的排它锁,一旦我们查询了该条数据,其他事务不可以添加任何锁,也就是我们在对库存做操作的时候只要事务没有提交一定可以保证其他事务无法访问该数据

保证了数据一致性,但仍然是锁机制肯定我们不希望得到性能下降的结果

1.2乐观锁的思想

我们需要知道乐观锁并不是锁机制
乐观锁只是一种思想,并不是锁,我们是通过version或者编码等实现
1.2.1 通过添加字段version

每次查询库存的时候携带版本号,然后每次扣减库存的时候判断版本号是否一致,一旦不一致就不允许提交

<select id="queryKillById">
    select save,version from t_kill where id = #{id}
</select>
<update id="updateSave">
  update t_kill 
	  set save = save - #{gnumber} 
	  where id = #{id} and version = #{version}
</update>
响应比悲观锁的实现肯定快一点,但是还有一个明显的缺点,导致了线程放弃购买,我们的商品肯定会出现剩余

1.2.2 通过库存判断

也是乐观锁的思想,只不过我们不需要维护version字段,只要我们在扣减库存的时候对save字段进行判断即可,因为insert,delete,update是默认自带排他锁的,我们可以保证在执行的时候该数据其他事务无法访问

<select id="queryKillById">
    select save from t_kill where id = #{id}
</select>
<update id="updateSave">
  update t_kill 
	  set save = save - #{gnumber} 
	  where id = #{id} and save >= gnumber
 </update>

引入Redis

业务逻辑

将数据库的数据先存放在redis中,然后我们直接对redis进行脚本操作,将需要添加的订单信息也存放到redis中然后判断save<=0在将缓存中的数据添加回数据库中

编写lua脚本保证原子性操作
--key=kill+id
local id = KEYS[1]
--需要购买的商品数量
local number = tonumber(ARGV[1])
--获得库存
local save = tonumber(redis.call('get','kill'..id))

if save == null or save <= 0 then
    --购买失败 库存不足或者没有该商品
    return 2
end

--此时库存的扣减
save = save - number
--设置库存回到缓存
redis.call('set','kill'..id),save)
--缓存订单数据
redis.call('rpush','order'..id,ARGV[2])
-- 秒杀成功 返回结果
if save == 0 then
    -- 0 : 表示最后一次,秒杀结束
    --此时需要将缓存中的数据写回数据库
    return 0
else
    return 1
end
缓存lua脚本
@Autowired
private RedisTemplate redisTemplate;
private RedisConnection connection;
//lua脚本的位置
private String luaPath;
private String luaKey;
@PostConstruct
public void init(){
    connection = redisTemplate.getConnectionFactory().getConnection();
    luaPath = this.getClass().getResource("/static/lua/kill.lua").getPath();
    try {
        luaKey = connection.scriptLoad(FileUtils.readFileToByteArray(new File(luaPath)));
    } catch (IOException e) {
        e.printStackTrace();
    }
}
主要业务方法
@Override
@Transactional
public int kill(Integer id, Integer gnumber, int uid) {
Orders orders = new Orders();
orders.setOrdertime(new Date());
orders.setOrderid(UUID.randomUUID().toString());
orders.setUid(uid);
//执行缓存的lua脚本
Long result = connection.evalSha(luaKey, ReturnType.INTEGER, 1,
        (id + "").getBytes(),
        (gnumber + "").getBytes(),
        new Gson().toJson(orders).getBytes());
if(result == 0){
    //秒杀结束
    //为了避免最后一个用户等待一个将数据写回数据库的时间,我们使用异步的service方法进行写回
    saveReturnData(id);
}
return result.intValue();//0,1成功 2失败
}
异步方法将redis中的数据写回数据库
启动主方法上添加@EnableAsync注解开启异步Service
@Override
@Async
@Transactional
public void saveReturnData(Integer id) {
    //通过id获得数据
    Long size = redisTemplate.opsForList().size("order" + id);
    List<String> ordersStringList = redisTemplate.opsForList().range("order" + id, 0, size);
    List<Orders> ordersList = new ArrayList<>();
    for (int i = 0; i < ordersStringList.size(); i++) {
        Orders orders = new Gson().fromJson(ordersStringList.get(i), Orders.class);
        ordersList.add(orders);
    }
    //批量插入订单
    orderDao.insertOrderList(ordersList);
    //将库存设为0
    killDao.updateSave(id,0);
    //删除redis中的数据
    redisTemplate.delete("kill" + id);
    redisTemplate.delete("order" + id);
}

猜你喜欢

转载自blog.csdn.net/qq_42629110/article/details/84839963