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

  当一个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 assendDate, total,unit_amount as unitAmount, stock, version, note fromT_RED_PACKETwhere 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);// 当前小红包库存大于0if (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 assendDate, total,unit_amount as unitAmount, stock, version, notefromT_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_PACKETset stock = stock - 1,version = version + 1where 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);// 当前小红包库存大于0if (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);// 当前小红包库存大于0if (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;@Autowiredprivate RedisTemplate redisTemplate = null; // RedisTemplate
@Autowiredprivate DataSource dataSource = null; // 数据源
@Override// 开启新线程运行
    @Asyncpublic 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 { ...@Overridepublic 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个数据库记录添加到数据库中保存起来。
    @Autowiredprivate RedisTemplate redisTemplate = null;@Autowiredprivate 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;@Overridepublic 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")@ResponseBodypublic 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服务器宕机也不会影响现有的其他业务,同时也可以使用备机等设备提高系统的高可用,保证网络的安全稳定。

  

转载于:https://www.cnblogs.com/BigJunOba/p/9795169.html

Java Web(1)高并发业务相关推荐

  1. java唯一订单号_java web在高并发和分布式下实现订单号生成唯一的解决方案

    方案一: 如果没有并发,订单号只在一个线程内产生,那么由于程序是顺序执行的,不同订单的生成时间戳正常不同,因此用时间戳+随机数(或自增数)就可以区分各个订单.如果存在并发,且订单号是由一个进程中的多个 ...

  2. Java——使用多线程模拟真实高并发业务并保证安全性(一)

    作者专注于Java.架构.Linux.小程序.爬虫.自动化等技术. 工作期间含泪整理出一些资料,微信搜索[javaUp],回复 [java][黑客][爬虫][小程序][面试]等关键字免费获取资料.技术 ...

  3. oracle rac 高并发性能_高并发业务下 JVM 涉及的垃圾回收与性能问题分析与定位...

    最近好多 Java 的朋友问:"高并发业务场景下,JVM涉及的性能问题好难搞呀--".看来是大家的技术经验相对少了些,拿不准该从哪些地方上手,其实,每个技术人要该懂得怎样更好打造自 ...

  4. 如何处理高并发业务场景

    要具备高并发的经验确实需要有实际项目,因为业务逻辑其实很容易理清,但是要在高并发的情况下如何找到业务繁忙的热点并进行优化,完全只能凭经验. 假如没有靠谱的公司,接触不到高并发的业务场景怎么办? 从处理 ...

  5. 如何掌握java多线程,高并发,大数据方面的技能?

    https://www.zhihu.com/question/27575123 如何掌握java多线程,高并发,大数据方面的技能? 因为想进入互联网公司,然后发现互联网类型的公司问的主要问题都离不开这 ...

  6. 一个WEB网站高并发量的解决方案

    一个WEB网站高并发量的解决方案 参考文章: (1)一个WEB网站高并发量的解决方案 (2)https://www.cnblogs.com/dotnetHui/p/7943605.html 备忘一下.

  7. 【java】动态高并发时为什么推荐重入锁而不是Synchronized?

    1.概述 转载:http://www.dreamwu.com/post-1758.html 这个图画的不错,有助于加深理解. [Java]Synchronized 有几种用法 [java] 从hots ...

  8. 总结-Java多线程与高并发简记

    1.什么是多线程? 一个进程可以开启多个线程,每个线程可以并发/并行执行不同任务. 2.Java多线程实现方式    2.1.继承Thread类    2.2.实现Runnable接口方式实现多线程 ...

  9. java如何解决高并发问题_java怎么处理高并发?

    java处理高并发的方法:1.优化代码,减少不必要的资源浪费:2.把图片与页面进行分离,将图片放到独立的图片服器:3.使用缓存,可以大量减少与数据库的交互,提高性能:4.使用数据库集群:5.进行DB优 ...

最新文章

  1. 基于matlab fdma传输系统设计,基于MATLAB的LTE系统仿真研究
  2. php 时间戳 时区,PHP时间函数 时间戳 设置时区
  3. 字典、列表、元祖、字符串的综合(2)
  4. mysql某个值连续出现的记录_MySQL-面试必备
  5. 集成学习voting Classifier在sklearn中的实现
  6. Spring Boot(3)---Spring Boot启动器Starter详解
  7. 解析HttpURLConnection与代理服务器
  8. 【异常(待解决)】org.apache.http.NoHttpResponseException: api.weixin.qq.com:443 failed to respond
  9. Python 中的 None 与真假
  10. 4.数据结构 --- 串
  11. java通过银行账号获取银行名称
  12. task文件服务器无法反弹,手把手带你玩转NAS 篇二十一:小米Redmi AC2100路由器刷机padavan保姆级教程...
  13. 深度linux如何打开exe文件,在deepin中简单粗暴地执行exe程序
  14. 大数据新闻推送你怎么看_如何看待大数据精准推送,使人的视界越来越窄?
  15. glob patterns
  16. 为啥扫描服务器端口无响应,服务器端口扫描工具
  17. 计算机usb无法识别网络连接,usb无法识别是什么原因 usb无法识别解决方法【详解】...
  18. 微信小程序学习笔记(一)
  19. html5取消下拉菜单,Vue.js实现在下拉列表区域外点击即可关闭下拉列表的功能(自定义下拉列表)...
  20. 全网为数不多清晰可行的在VUE中使用sortable.js实现动态拖拽排序的教程!

热门文章

  1. 网络协议从入门到底层原理(3)网络互联模型、物理层、数据链路层(CSMA/CD协议、Ethernet V2帧、PPP协议)
  2. JZ32变形~剑指 Offer 32 - II. 从上到下打印二叉树 II
  3. 小程序入门学习12--云函数与数据库01
  4. centos6+ 将程序 注册到 service进行启动 停止 重启等:以nginx为例,添加nginx脚本
  5. shell中的正则表达式
  6. 技术一般,却被破格提拔,背后肯定有黑幕?
  7. fgo7.27服务器维护,【公告】更新游戏数据资料(7/27 实施)
  8. 友商对于鸿蒙系统,谷歌新系统上线!鸿蒙面临生死线考验,华为喊话友商,被小米拒绝...
  9. java继承类长方形面积_java_java用接口、多态、继承、类计算三角形和矩形周长及面积的方法,本文实例讲述了java用接口、多 - phpStudy...
  10. Quartus II 9.0sp1之功能仿真