七仔的博客

七仔的博客GithubPages分博

0%

在SSM中使用Redis实现高并发抢座

在SSM中使用Redis实现高并发抢座,写了大概需要的过程,大家可以借鉴借鉴

在SSM中使用Redis实现高并发抢座

一、 Redis的配置

Maven配置:

1
2
3
4
5
6
7
8
9
10
11
<!-- Redis -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
<version>1.8.4.RELEASE</version>
</dependency>

spring-redis.xml配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:cache="http://www.springframework.org/schema/cache"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.2.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-4.2.xsd
http://www.springframework.org/schema/cache
http://www.springframework.org/schema/cache/spring-cache-4.2.xsd">


<!-- 加载Properties文件 -->
<bean id="redisProperties" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="locations">
<list>
<value>classpath:redis.properties</value>
</list>
</property>
<property name="ignoreUnresolvablePlaceholders" value="true" />
</bean>

<!-- redis 相关配置 -->
<bean id="poolConfig" class="redis.clients.jedis.JedisPoolConfig">
<property name="maxIdle" value="$ {redis.maxIdle}" />
<property name="maxWaitMillis" value="$ {redis.maxWait}" />
<property name="testOnBorrow" value="$ {redis.testOnBorrow}" />
</bean>

<bean id="jedisConnectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory"
p:host-name="$ {redis.host}" p:port="$ {redis.port}" p:password="$ {redis.pass}" p:pool-config-ref="poolConfig"/>

<bean id="redisTemplate" class="org.springframework.data.redis.core.StringRedisTemplate">
<property name="connectionFactory" ref="jedisConnectionFactory"/>
<!-- 序列化方式 建议key/hashKey采用StringRedisSerializer。 -->
<property name="keySerializer">
<bean class="org.springframework.data.redis.serializer.StringRedisSerializer"/>
</property>
<property name="hashKeySerializer">
<bean class="org.springframework.data.redis.serializer.StringRedisSerializer"/>
</property>
<property name="valueSerializer">
<bean class="org.springframework.data.redis.serializer.StringRedisSerializer" />
</property>
<property name="hashValueSerializer">
<bean class="org.springframework.data.redis.serializer.StringRedisSerializer"/>
</property>
</bean>

</beans>

redis.properties文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# Redis settings
# server IP
redis.host=127.0.0.1
# server port
redis.port=6379
# server pass
redis.pass=
# use dbIndex
redis.database=0
# 控制一个pool最多有多少个状态为idle(空闲的)的jedis实例
redis.maxIdle=300
# 表示当borrow(引入)一个jedis实例时,最大的等待时间,如果超过等待时间(毫秒),则直接抛出JedisConnectionException;
redis.maxWait=3000
# 在borrow一个jedis实例时,是否提前进行validate操作;如果为true,则得到的jedis实例均是可用的
redis.testOnBorrow=true
# 编码
redis.charset=GBK

二、 在服务器启动前加载用户信息到Redis

(一)、此处实现了在服务器启动之前先加载用户信息到Redis,这里提供实现方法:

1.新建初始化类并完成加载代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;

public class Init implements HttpSessionListener {

static {
//此处是你需要提前运行的代码
}

@Override
public void sessionCreated(HttpSessionEvent arg0) {}

@Override
public void sessionDestroyed(HttpSessionEvent arg0) {}
}

2.在web.xml配置类路径

1
2
3
<listener>
<listener-class>edu.options.init.Init</listener-class>
</listener>

(二)、实现思路

一、使用JDBC查询用户信息的用户名密码/密码MD5值(两个字段可以作为键值),此处不再赘述。

二、使用Jedis加载到Redis(此处提供简单的操作):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
try {
// 连接Redis
Jedis jedis = new Jedis("127.0.0.1", 6379);
// 清空Redis
jedis.flushAll();
for (int i = 1; i <= SEAT_SIZE; i++) {
jedis.hset("options_" + i, "stock", "1");
jedis.hset("options_" + i, "unit_amount_" + i, "1");
}
jedis.hset("options_all", "all", SEAT_SIZE + "");
// 获取学生信息并存储到Redis
Map<String, String> numberAndNames = getStudents();
Set<String> s = numberAndNames.keySet();
for(String number : s){
// 根据得到的键,获取对应的值
String name = numberAndNames.get(number);
jedis.hset(number, "DNUI", name);
}
// 从resource/time.txt将抢座时间加载到Redis
String[] times = Tools.getFile("resource/time.csv").split(",");
jedis.hset("startTime", "TIME", times[0]);
jedis.hset("endTime", "TIME", times[1]);
jedis.hset("createTime", "TIME", times[2]);
} catch (Exception e) {
e.printStackTrace();
}

这里我存储的是学生学号及姓名。

三、具体调用

(一)、此处实现了从resources文件夹出导入Lua脚本,这样可以方便编辑并可以方便以后的改动。这里提供一个函数,可以实现从资源文件夹加载文件(部署到服务器上也可以受用):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* 获取资源文件
* @param fileName 文件名
* @return 文件内容
*/
public static String getFile(String fileName){
InputStream inputStream = Tools.class.getClassLoader().getResourceAsStream(fileName);
ByteArrayOutputStream result = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int length;
String str = "";
try {
if (inputStream != null) {
while ((length = inputStream.read(buffer)) != -1) {
result.write(buffer, 0, length);
}
inputStream.close();
str = result.toString(StandardCharsets.UTF_8.name());
}
} catch (IOException e) {
e.printStackTrace();
}
return str;
}

使用时可以这样获取Lua代码:

1
String luaCode = getFile(“resource/options.lua”);

(二)、Lua文件的编写:

1
2
3
4
5
6
7
8
9
10
11
12
local listKey = 'options_list_'..KEYS[1]
local options = 'options_'..KEYS[1]
local stock = tonumber(redis.call('hget', options, 'stock'))
if stock <= 0 then return 0 end --没有座位或座位已被抢完
stock = stock -1
redis.call('hset', options, 'stock', tostring(stock))
redis.call('rpush', listKey, ARGV[1])
local all = tonumber(redis.call('hget', 'options_all', 'all'))
all = all -1
redis.call('hset', 'options_all', 'all', tostring(all))
if all == 0 then return 2 end --成功抢座并且座位已被抢空
return 1

这是我参考一个抢票系统改的,有不足请指出

(三)、Lua文件的使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
@Autowired
private RedisTemplate redisTemplate;
// 加载Lua脚本
private static final String SCRIPT = Tools.getFile("lua/options.lua");
// 在缓存LUA脚本后,使用该变量保存Redis返回的32位的SHA1编码,使用它去执行缓存的LUA脚本
private String SHA1 = null;
public String options(long seatNumber, long number){
String args = number + "-" + System.currentTimeMillis();
Long result;
// 获取底层Redis操作对象
Jedis jedis = (Jedis) redisTemplate.getConnectionFactory().getConnection().getNativeConnection();
try {
// 如果脚本没有加载过,那么进行加载,这样就会返回一个sha1编码
if (SHA1 == null) {
SHA1 = jedis.scriptLoad(SCRIPT);
}
// 执行脚本,返回结果
Object res = jedis.evalsha(SHA1, 1, seatNumber + "", args);
result = (Long) res;
if(result == 2){
// 设置结束时间为当前时间
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String nowTime = format.format(new Date());
jedis.hset("endTime", "TIME", nowTime);
// 保存数据到数据库
this.redisOptionsService.saveRedisOptions();
}
if(result > 0){
// 删除Redis列表
jedis.del(number + "");
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS");// 设置日期格式
System.out.println(
"学号为" + number + "的同学在" + df.format(new Date()) + "的时间抢到" + seatNumber + "座位");
return "成功占据座位";
}
} finally {
// 确保jedis顺利关闭
if (jedis != null && jedis.isConnected()) {
jedis.close();
}
}
return "此座位已被占据";
}

(四)、抢座完成后的保存:可以看到当抢座完成后会调用this.redisOptionsService.saveRedisOptions();进行数据持久化存储。

这里是大部分实现代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
@Autowired
private RedisTemplate redisTemplate = null; // RedisTemplate工具类
@Async
public void saveRedisOptions(){
System.err.println("开始保存数据");
Long start = System.currentTimeMillis();
List<Seat> seatList = new ArrayList<>();
for (long i = 1; i <= SEAT_NUM; i++) {
seatList.add(getSeatByRedis(i));
}
int count = executeBatch(seatList);
Long end = System.currentTimeMillis();
System.err.println("保存数据结束,耗时" + (end - start) + "毫秒,共" + count + "条记录被保存。");
}

/**
* 获取座位信息(单个座位信息)
* @param seatId 座位id
* @return 单个座位信息
*/
@SuppressWarnings("unchecked")
private Seat getSeatByRedis(Long seatId) {
// 获取列表操作对象
BoundListOperations ops = redisTemplate.boundListOps(PREFIX + seatId);
List userIdList = ops.range(0, 1);
// 保存红包信息
String args = userIdList.get(0).toString();
String[] arr = args.split("-");
long numberId = Long.parseLong(arr[0]);
long time = Long.parseLong(arr[1]);
// 生成抢红包信息
Seat seat = new Seat();
seat.setSeatnumber(seatId);
seat.setNumber(numberId);
seat.setTime(new Timestamp(time));
User user = this.userDao.findByNumber(numberId);
seat.setName(user.getName());
seat.setDepartment(user.getDepartment());
seat.setSex(user.getSex());
return seat;
}

__注意__:saveRedisOptions()前使用了@Async注解进行异步调用来不阻塞响应。

executeBatch()是个调用数据库存储的函数。

此为博主副博客,留言请去主博客,转载请注明出处:https://www.baby7blog.com/myBlog/15.html

欢迎关注我的其它发布渠道