Java Web(1)高并发业务

  互联网无时无刻不面对着高并发问题,例如商品秒杀、微信群抢红包、大麦网抢演唱会门票等。

  当一个Web系统,在一秒内收到数以万计甚至更多的请求时,系统的优化和稳定是至关重要的。

  互联网的开发包括Java后台、NoSQL、数据库、限流、CDN、负载均衡等。

  一、互联系统应用架构基础分析

  

  防火墙的功能是防止互联网上的病毒和其他攻击,正常的请求通过防火墙后,最先到达的就是负载均衡器。

  负载均衡器的主要功能:

  • 对业务请求做初步的分析,决定分不分发请求到Web服务器,常见的分发软件比如Nginx和Apache等反向代理服务器,它们在关卡处可以通过配置禁止一些无效的请求,比如封禁经常作弊的IP地址,也可以使用Lua、C语言联合 NoSQL 缓存技术进行业务分析,这样就可以初步分析业务,决定是否需要分发到服务器
  • 提供路由算法,它可以提供一些负载均衡算法,根据各个服务器的负载能力进行合理分发,每一个Web服务器得到比较均衡的请求,从而降低单个服务器的压力,提高系统的响应能力。
  • 限流,对于一些高并发时刻,如双十一,需要通过限流来处理,因为可能某个时刻通过上述的算法让有效请求过多到达服务器,使得一些Web服务器或者数据库服务器产生宕机。当某台机器宕机后,会使得其他服务器承受更大的请求量,这样就容易产生多台服务器连续宕机的可能性,持续下去就会引发服务器雪崩。因此,在这种情况下,负载均衡器有限流的算法,对于请求过多的时刻,可以告知用户系统繁忙,稍后再试,从而保证系统持续可用。

  为了应对复杂的业务,可以把业务存储在 NoSQL 上,通过C语言或者Lua语言进行逻辑判断,它们的性能比Web服务器判断的性能要快速得多,从而降低Web服务器的压力,提高互联网系统的响应速度。

  

  二、应对无效请求

  

  在负载均衡器转发给Web服务器之前,使用C语言和Redis进行判断是否是无效请求。对于黄牛组织,可以考虑僵尸账号排除法进行应对。

  

  三、系统设计

  高并发系统往往需要分布式的系统分摊请求的压力,要尽量根据 Web 服务器的性能进行均衡分配请求。  

  划分系统可以按照业务划分,即水平划分。也可以不按照业务分,即垂直划分。

  按照业务划分可以提高开发效率以及更方便地设计数据库。但是,还要通过RPC(Remote Procedure Call Protocol)远程过程调用协议处理这些信息,例如Dubbo、Thrift和Hessian等。

  

  四、数据库设计

  为了得到高性能,可以使用分表或分库技术,从而提高系统的响应能力。

  分表是指在一个数据库内本来一张表可以保存的数据,设计成多张表去保存。例如,将每一年的交易记录分别分成交易表,而不是只有一张交易表来记录所有的交易记录。

  分库是把表数据分配在不同的数据库中,分库首先需要一个路由算法确定数据在哪个数据库上,然后才能进行查询,这里可以把用户和对应业务的数据库的信息缓存到Redis中,这样路由算法就可以通过Redis从读取的数据来决定使用哪个数据库进行查询了。

  另外,还可以考虑SQL优化,建立索引等优化,提高数据库的性能。

 

  五、动静分离技术

  

  对于互联网而言,大部分数据都是静态数据,只有少数使用动态数据,动态数据的数据包很小,不会造成网络瓶颈,而静态的数据则不一样,静态数据包含图片、CSS、JavaScript 和视频等互联网的应用,尤其是图片和视频占据的流量很大,如果都从动态服务器(比如Tomcat、WildFly和WebLogic等)获取,那么动态服务器的带宽压力会很大,这个时候应该考虑动静分离技术。

  可以使用静态HTTP服务器,例如Apache,将静态数据分离到静态HTTP服务器上,这样图片、HTML、脚本等资源都可以从静态服务器上获取,尽量使用 Cookie 等技术,让客户端缓存能够缓存数据,避免多次请求,降低服务器的压力。

  企业可以还可以使用高级的动静分离技术,例如CDN(Content Delivery Network,即内容分发网络),它允许企业将自己的静态数据缓存到网络 CDN 的节点中,对于用户的请求,直接通过CDN算法去指定的CDN节点去响应请求。

  

  六、锁和高并发

  无论区分有效请求和无效请求、水平划分还是垂直划分、动静分离技术,还是数据库分表、分库技术,都无法避免动态数据,而动态数据的请求最终也会落在一台 Web 服务器上。

  例如,发放一个总额为 20 万元的红包,拆分成 2 万个金额为 10 元 的小红包,供给网站的 3 万个会员在线抢夺,这就是一个典型的高并发的场景。

  由于会出现多个线程同时发起请求,由于线程每一步完成的顺序不一样,这样会导致数据的一致性问题。

  为了保证数据一致性,可以使用加锁的方式,但是加锁会影响并发,从而影响系统的性能,而不加锁就难以保证数据的一致性,这就是锁和高并发的矛盾。

  

  为了解决锁和高并发的矛盾,大部分企业提出了悲观锁和乐观锁的概念,

  • 对于数据库而言,如果在短时间内需要执行大量SQL,对于服务器的压力可想而知,需要优化数据库的表设计、索引、SQL语句等。
  • 还可以使用 Redis 事务和 Lua 语言所提供的原子性来取代现有的数据库技术,从而提高数据的存储响应,以应对高并发场景,但是严格来说也属于乐观锁。

  0.T_RED_PACKET为红包表,T_USER_RED_PACKET为用户抢红包表

  (0)未加锁情况下,并发导致了数据的不一致

    <!-- 查询红包具体信息 -->
    <select id="getRedPacket" parameterType="long"
        resultType="com.ssm.chapter22.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>

  业务逻辑:

    @Override
    @Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRED)
    public int grapRedPacket(Long redPacketId, Long userId) {
        // 获取红包信息
        // RedPacket redPacket = redPacketDao.getRedPacket(redPacketId);
        // 悲观锁
        RedPacket redPacket = redPacketDao.getRedPacketForUpdate(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;
    }

  

  1.使用数据库的悲观锁和乐观锁进行设计

  (1)悲观锁

  悲观锁是一种利用数据库内部机制提供的锁的方法,也就是对更新的数据加锁,这样在并发期间一旦有一个事务持有了数据库记录的锁,其他的线程将不能再对数据进行更新了。

  修改SQL语句,加入“for update”,意味着将持有对数据库记录的行更新锁(因为这里使用主键查询,所以只会对行加锁。如果使用的是非主键查询,要考虑是否对全表加锁的问题,加锁后可能引发其他查询的阻塞),那就意味着在高并发的场景下,当一条事务持有了这个更新锁后才能往下操作,其他的线程如果要更新这条记录都需要等待,这样就不会出现超发现象引发的数据不一致的问题了。

  但是,悲观锁会导致系统性能下降。对于悲观锁来说,当一条线程抢占了资源后,其他的线程将得不到资源,那么这个时候,CPU 就会将这些得不到资源的线程挂起,挂起的线程也会消耗 CPU 的资源。由于高并发环境下的频繁挂起线程和恢复线程,导致CPU频繁切换线程上下文,从而使CPU资源得到了极大的消耗,造成了性能不佳的问题。悲观锁也称独占锁。

  业务逻辑和不加锁时一致。

    <!-- 查询红包具体信息 -->
    <select id="getRedPacketForUpdate" parameterType="long"
        resultType="com.ssm.chapter22.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} for update
    </select>

  (2)乐观锁

  乐观锁是一种不会阻塞其他线程并发的机制,它不会使用数据库的锁进行实现,它的设计里面由于不阻塞其他线程,所以不会引发线程频繁挂起和恢复,这样可以提高并发能力。乐观锁也称为非阻塞锁。乐观锁使用的是CAS原理。

  

  CAS原理并不排斥并发,也不独占资源,只是在线程开始阶段就读入线程共享数据,保存为旧值。当处理完逻辑,需要更新数据的时候,会进行一次比较,即比较各个线程当前共享的数据是否和旧值保持一致,如果一致,就开始更新数据,如果不一致,就认为该数据已经被其他线程修改了,那么就不再更新数据,可以考虑重试或者放弃。有时候可以重试,这样就是一个可重入锁。

  CAS原理存在ABA问题,ABA问题是因为业务逻辑存在回退的可能性。如果加入一个非业务逻辑的属性,比如在一个数据中加入版本号,每次修改变量的数据时,强制版本号只能递增,而不会回退,即使是其他业务数据回退,它也会递增,那么就解决了ABA问题。

  对于查询来说,没有“for update”语句,避免了锁的发生,就不会造成线程阻塞。然后,增加了对版本号的判断,其次每次扣减都会对版本号加1,这样就可以避免ABA问题了。

    <!-- 通过版本号扣减抢红包 每更新一次,版本增1, 其次增加对版本号的判断 -->
    <update id="decreaseRedPacketForVersion">
        update T_RED_PACKET
        set stock = stock - 1,
        version = version + 1
        where id = #{id}
        and version = #{version}
    </update>

  在Service中使用乐观锁,无重入的代码:其中,redPacket先获取到了红包的记录,其redPacket.getVersion()表示的就是version版本值,在执行更新数据库方法时,将redPacket.getVersion()传入作为version变量,由于在判断时增加了“and version = #{version}”语句,因此,如果不相等,就不执行update SQL语句,因此update返回值就为0,表明版本号发生了变化。

    // 乐观锁,无重入
    @Override
    @Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRED)
    public int grapRedPacketForVersion(Long redPacketId, Long userId) {
        // 获取红包信息,注意version值
        RedPacket redPacket = redPacketDao.getRedPacket(redPacketId);
        // 当前小红包库存大于0
        if (redPacket.getStock() > 0) {
            // 再次传入线程保存的version旧值给SQL判断,是否有其他线程修改过数据
            int update = redPacketDao.decreaseRedPacketForVersion(redPacketId, redPacket.getVersion());
            // 如果没有数据更新,则说明其他线程已经修改过数据,本次抢红包失败
            if (update == 0) {
                return FAILED;
            }
            // 生成抢红包信息
            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;
    }

  在实际测试的情况下,经过3万次的抢夺,原来的2万个红包中,由于版本号version不一致导致了还有8千多个没有被抢到,也就是说,抢红包失败的记录太大了。

  (3)乐观锁重入机制

  为了克服这个问题,提高成功率,还会考虑使用锁重入机制。也就是一旦因为版本原因没有抢到红包,则重新尝试抢红包,但是过多的重入会造成大量的SQL执行,有两种方式:

  • 按时间戳重入,也就是在一定的时间内(例如:当前时间+100毫秒),不成功的会循环到成功为止,直至超过时间戳,不成功才会退出,返回失败。
  • 按次数重入,比如限定3次,如果超过3次尝试后还失败,那么就判定此次失败

  按时间戳重入:有时候时间戳并不是那么稳定,也会随着系统的空闲或者繁忙导致重试次数不一。

    // 乐观锁,按时间戳重入
    @Override
    @Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRED)
    public int grapRedPacketForVersion(Long redPacketId, Long userId) {
        // 记录开始时间
        long start = System.currentTimeMillis();
        // 无线循环,直到成功或者满100毫秒退出
        while (true) {
            // 获取循环当前时间
            long end = System.currentTimeMillis();
            // 如果超过了100毫秒就结束尝试
            if (end - start > 100) {
                return FAILED;
            }
... 和乐观锁部分一样
}

  按次数重入:限制重试次数,这样就能避免过多的重试导致过多的SQL被执行,从而保证数据库的性能。

    // 乐观锁,按重试次数重入
    @Override
    @Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRED)
    public int grapRedPacketForVersion(Long redPacketId, Long userId) {
        for (int i = 0; i < 3; i++) {
            // 获取红包信息,主要是version值
            RedPacket redPacket = redPacketDao.getRedPacket(redPacketId);
            // 当前小红包库存大于0
            if (redPacket.getStock() > 0) {
                // 再次传入线程保存的version旧值给SQL判断,是否有其他线程修改过数据
                int update = redPacketDao.decreaseRedPacketForVersion(redPacketId, redPacket.getVersion());
                // 如果没有数据更新,则说明其他线程已经修改过数据,则重新抢夺
                if (update == 0) {
                    continue;
                }
...
}

  

  2.使用Redis进行设计

  数据库最终会将数据保存到磁盘中,而Redis使用的是内存,内存的速度比磁盘速度快得多。

  Redis的Lua语言是原子性的,且功能更为强大,因此优先选择Lua语言实现抢红包业务。

  Redis并非是一个长久存储数据的地方,它存储的数据是非严格和安全的环境,更多的时候只是为了提供更为快速的缓存,因此当红包金额为0或者红包超时的时候,将红包数据保存到数据库中,这样才能够保证数据的安全性和严格性。  

  

  (0)表设计

  使用下面的Redis命令在Redis中初始化了一个编号为5的大红包,其中库存为2万个,每个10元

127.0.0.1:6379> hset red_packet_5 stock 20000
(integer) 1
127.0.0.1:6379> hset red_packet_5 unit_amount 10
(integer) 1

  在数据库中通过下面的SQL语句创建用户抢红包信息表:

create table T_USER_RED_PACKET 
(
   id                   int(12)                        not null auto_increment,
   red_packet_id        int(12)                        not null,
   user_id              int(12)                        not null,
   amount               decimal(16,2)                  not null,
   grab_time            timestamp                      not null,
   note                 varchar(256)                   null,
   primary key clustered (id)
);

  (1)Lua脚本设计

    // Lua脚本
    String script = "local listKey = 'red_packet_list_'..KEYS[1] \n"   // 被抢红包列表 key
             + "local redPacket = 'red_packet_'..KEYS[1] \n"    // 当前被抢红包 key
                + "local stock = tonumber(redis.call('hget', redPacket, 'stock')) \n" // 读取当前红包库存
                + "if stock <= 0 then return 0 end \n"  // 没有库存,返回0
                + "stock = stock -1 \n"           // 库存减1
                + "redis.call('hset', redPacket, 'stock', tostring(stock)) \n"  // 保存当前库存
                + "redis.call('rpush', listKey, ARGV[1]) \n"     // 往Redis链表中加入当前红包信息
                + "if stock == 0 then return 2 end \n"     // 如果是最后一个红包,则返回2,表示抢红包已经结束,需要将Redis列表中的数据保存到数据库中
                + "return 1 \n";    // 如果并非最后一个红包,则返回1,表示抢红包成功。

  当返回为2的时候,说明红包已经没有库存,会触发数据库对链表数据的保存,这是一个大数据量的保存,因为有20000条记录。为了不影响最后一次抢红包的响应,在实际的操作中往往会考虑使用 JMS 消息发送到别的服务器进行操作。这里只是创建了一条新的线程去运行保存 Redis 链表数据到数据库的程序。

  保存 Redis 抢红包信息到数据库的服务类:

package com.ssm.chapter22.service.impl;
...
@Service
public class RedisRedPacketServiceImpl implements RedisRedPacketService {

    private static final String PREFIX = "red_packet_list_";
    // 每次取出1000条,避免一次取出消耗太多内存
    private static final int TIME_SIZE = 1000;

    @Autowired
    private RedisTemplate redisTemplate = null; // RedisTemplate

    @Autowired
    private DataSource dataSource = null; // 数据源

    @Override
    // 开启新线程运行
    @Async
    public void saveUserRedPacketByRedis(Long redPacketId, Double unitAmount) {
        System.err.println("开始保存数据");
        Long start = System.currentTimeMillis();
        // 获取列表操作对象
        BoundListOperations ops = redisTemplate.boundListOps(PREFIX + redPacketId);
        Long size = ops.size();
        Long times = size % TIME_SIZE == 0 ? size / TIME_SIZE : size / TIME_SIZE + 1;
        int count = 0;
        List<UserRedPacket> userRedPacketList = new ArrayList<UserRedPacket>(TIME_SIZE);
        for (int i = 0; i < times; i++) {
            // 获取至多TIME_SIZE个抢红包信息
            List userIdList = null;
            if (i == 0) {
                userIdList = ops.range(i * TIME_SIZE, (i + 1) * TIME_SIZE);
            } else {
                userIdList = ops.range(i * TIME_SIZE + 1, (i + 1) * TIME_SIZE);
            }
            userRedPacketList.clear();
            // 保存红包信息
            for (int j = 0; j < userIdList.size(); j++) {
                String args = userIdList.get(j).toString();
                String[] arr = args.split("-");
                String userIdStr = arr[0];
                String timeStr = arr[1];
                Long userId = Long.parseLong(userIdStr);
                Long time = Long.parseLong(timeStr);
                // 生成抢红包信息
                UserRedPacket userRedPacket = new UserRedPacket();
                userRedPacket.setRedPacketId(redPacketId);
                userRedPacket.setUserId(userId);
                userRedPacket.setAmount(unitAmount);
                userRedPacket.setGrabTime(new Timestamp(time));
                userRedPacket.setNote("抢红包 " + redPacketId);
                userRedPacketList.add(userRedPacket);
            }
            // 插入抢红包信息
            count += executeBatch(userRedPacketList);
        }
        // 删除Redis列表
        redisTemplate.delete(PREFIX + redPacketId);
        Long end = System.currentTimeMillis();
        System.err.println("保存数据结束,耗时" + (end - start) + "毫秒,共" + count + "条记录被保存。");
    }

    /**
     * 使用JDBC批量处理Redis缓存数据.
     * 
     * @param userRedPacketList 抢红包列表
     * @return 抢红包插入数量.
     */
    private int executeBatch(List<UserRedPacket> userRedPacketList) {
        Connection conn = null;
        Statement stmt = null;
        int[] count = null;
        try {
            conn = dataSource.getConnection();
            conn.setAutoCommit(false);
            stmt = conn.createStatement();
            for (UserRedPacket userRedPacket : userRedPacketList) {
                String sql1 = "update T_RED_PACKET set stock = stock-1 where id=" + userRedPacket.getRedPacketId();
                DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
                String sql2 = "insert into T_USER_RED_PACKET(red_packet_id, user_id, " + "amount, grab_time, note)"
                        + " values (" + userRedPacket.getRedPacketId() + ", " + userRedPacket.getUserId() + ", "
                        + userRedPacket.getAmount() + "," + "'" + df.format(userRedPacket.getGrabTime()) + "'," + "'"
                        + userRedPacket.getNote() + "')";
                stmt.addBatch(sql1);
                stmt.addBatch(sql2);
            }
            // 执行批量
            count = stmt.executeBatch();
            // 提交事务
            conn.commit();
        } catch (SQLException e) {
            /********* 错误处理逻辑 ********/
            throw new RuntimeException("抢红包批量执行程序错误");
        } finally {
            try {
                if (conn != null && !conn.isClosed()) {
                    conn.close();
                }
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        // 返回插入抢红包数据记录
        return count.length / 2;
    }
}

  这里的@Async注解表示让Spring自动创建另外一条线程去运行它,这样它便不在抢最后一个红包的线程之内,因为这个方法是一个较长时间的方法,如果在同一个线程内,那么对于最后抢红包的用户来说就需要等待相当长的时间,影响用户体验。

  为了使用@Async注解,还需要在 Spring 中配置一个任务池:

package com.ssm.chapter22.config;
...
@EnableAsync
public class WebConfig extends AsyncConfigurerSupport { 
  ...
    @Override
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
        taskExecutor.setCorePoolSize(5);
        taskExecutor.setMaxPoolSize(10);
        taskExecutor.setQueueCapacity(200);
        taskExecutor.initialize();
        return taskExecutor;
    }
}

  用户抢红包逻辑:grapRedPacketByRedis接收的参数,第一个是大红包名称“red_packet_5”中的5,而userId是jsp文件中发起抢红包请求的唯一标识[0,30000]中的某一个i值。

  当[0,30000]中的某一个值i发起请求后,假设 i 为 1000

  • jsp中直接异步post到url: "./userRedPacket/grapRedPacketByRedis.do?redPacketId=8&userId=" + i,然后调用grapRedPacketByRedis(Long redPacketId, Long userId)方法,其中redPacketId=5,而userId=1000。
  • 然后通过Object res = jedis.evalsha(sha1, 1, redPacketId + "", args);执行自定义的Lua脚本,其中,1表示key的个数,args表示grapRedPacketByRedis方法的两个参数值。定义用户抢红包信息在Redis中的键listKey=red_packet_list_5,大红包在Redis中的键red_packet_5,然后查询red_packet_5中的stock,返回还剩红包的数量stock,此时假设stock=7777,则由于还有红包,于是将stock变为7776,并更新到red_packet_5的stock中,然后将键值对red_packet_list_5-ARGV[1](也就是userId),即red_packet_list_5 - 1000写入Redis中。
  • 当返回结果为 2 时,说明最后一个红包已经被抢了,这个时候,jedis.hget("red_packet_" + redPacketId, "unit_amount");得到red_packet_5 → unit_amount 即单个小红包的金额10赋给变量unitAmount,然后saveUserRedPacketByRedis(redPacketId, unitAmount);方法将Redis中键red_packet_list的信息加上每个小红包金额信息,以及其他各种信息对应成用户抢红包数据库表定义,另开一个线程,将20000个数据库记录添加到数据库中保存起来。
    @Autowired
    private RedisTemplate redisTemplate = null;

    @Autowired
    private RedisRedPacketService redisRedPacketService = null;

    // Lua脚本
    String script = "local listKey = 'red_packet_list_'..KEYS[1] \n" 
             + "local redPacket = 'red_packet_'..KEYS[1] \n"
                + "local stock = tonumber(redis.call('hget', redPacket, 'stock')) \n" 
                + "if stock <= 0 then return 0 end \n"
                + "stock = stock -1 \n" 
                + "redis.call('hset', redPacket, 'stock', tostring(stock)) \n"
                + "redis.call('rpush', listKey, ARGV[1]) \n" 
                + "if stock == 0 then return 2 end \n" 
                + "return 1 \n";

    // 在缓存LUA脚本后,使用该变量保存Redis返回的32位的SHA1编码,使用它去执行缓存的LUA脚本[加入这句话]
    String sha1 = null;

    @Override
    public Long grapRedPacketByRedis(Long redPacketId, Long userId) {
        // 当前抢红包用户和日期信息
        String args = userId + "-" + System.currentTimeMillis();
        Long result = null;
        // 获取底层Redis操作对象
        Jedis jedis = (Jedis) redisTemplate.getConnectionFactory().getConnection().getNativeConnection();
        try {
            // 如果脚本没有加载过,那么进行加载,这样就会返回一个sha1编码
            if (sha1 == null) {
                sha1 = jedis.scriptLoad(script);
            }
            // 执行脚本,返回结果
            Object res = jedis.evalsha(sha1, 1, redPacketId + "", args);
            result = (Long) res;
            // 返回2时为最后一个红包,此时将抢红包信息通过异步保存到数据库中
            if (result == 2) {
                // 获取单个小红包金额
                String unitAmountStr = jedis.hget("red_packet_" + redPacketId, "unit_amount");
                // 触发保存数据库操作
                Double unitAmount = Double.parseDouble(unitAmountStr);
                System.err.println("thread_name = " + Thread.currentThread().getName());
                redisRedPacketService.saveUserRedPacketByRedis(redPacketId, unitAmount);
            }
        } finally {
            // 确保jedis顺利关闭
            if (jedis != null && jedis.isConnected()) {
                jedis.close();
            }
        }
        return result;
    }

   控制器方法:

    @RequestMapping(value = "/grapRedPacketByRedis")
    @ResponseBody
    public Map<String, Object> grapRedPacketByRedis(Long redPacketId, Long userId) {
        Map<String, Object> resultMap = new HashMap<String, Object>();
        Long result = userRedPacketService.grapRedPacketByRedis(redPacketId, userId);
        boolean flag = result > 0;
        resultMap.put("result", flag);
        resultMap.put("message", flag ? "抢红包成功": "抢红包失败");
        return resultMap;
    }

  模拟高并发的jsp文件,其中,由于post是异步请求,所以可以模拟多个用户同时请求的情况:

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>参数</title>
<!-- 加载Query文件-->
<script type="text/javascript"
    src="https://code.jquery.com/jquery-3.2.0.js">
        </script>
<script type="text/javascript">
            $(document).ready(function () {
                  //模拟30000个异步请求,进行并发
                var max = 30000;
                for (var i = 1; i <= max; i++) {
                    //jQuery的post请求,请注意这是异步请求
                    $.post({
                        //请求抢id为1的红包
                        //根据自己请求修改对应的url和大红包编号
                        url: "./userRedPacket/grapRedPacketByRedis.do?redPacketId=8&userId=" + i,
                        //成功后的方法
                        success: function (result) {
                        }
                    });
                }
            });
        </script>
</head>
<body>
</body>
</html>

  最后结果,在3万用户同时争抢2万个红包的场景下,Redis 只需要4秒,乐观锁需要33秒,而悲观锁需要54秒,可见Redis是多么的高效。

  使用Redis的风险在于Redis的不稳定性,因为其事务和存储都存在不稳定的因素,所以更多的时候,应该使用独立的Redis服务器做高并发业务,一方面可以提高Redis的性能,另一个方面即使在高并发的场合,Redis服务器宕机也不会影响现有的其他业务,同时也可以使用备机等设备提高系统的高可用,保证网络的安全稳定。

 

  

posted @ 2018-10-15 22:25  BigJunOba  阅读(1196)  评论(0编辑  收藏  举报