Redis
应用环境
分布式项目微服务框架下,不同tomcat怎么共享session(session只存在一个tomcat之中),
虽然tomcat本身具有广播的功能,但是由于tomcat每个都要指定其他所有共享的tomcat目标,这种配置很繁琐,而且耗费tomcat性能,运用Redis就像运用一个第三方的存储,存放当前的登录信息,可以有效解决这样的问题
单点登录
一级域名单点登录:不同系统的单点登录,OA系统登录,商城系统登录也有效
二级域名单点登录:一个系统多个功能,其中一个登录了,另外功能也登录状态
NoSql
非关系型数据库,不同于关系型数据库,没有指定列的类型,速度快(无需维护关系)
- 文档型:mongodb
- 键值对型:redis 数据防止在内存,速度快100倍(1s:11万次读,8万次写)
Redis秒杀缓解解析
redis因为其操作内存的原因,运用缓存速度快的特点,用作缓存服务器它可以用于减轻数据库压力.对比memcache,reid可以写入到文件,保证了数据的完整.比如出现秒杀业务的时候,先查询出余量,写入到redis中,每次发生购买,就修改redis的余量信息,当业务压力缓解的时候,再把数据写入到关系数据库(关系数据库可靠)中.
Redis分布式锁
通常java同步锁,只能锁一个java虚拟机的环境中的方法
通过redis锁,可以把这个锁放在redis中,当分布式环境下的该方法要被调用,先查看redis中的锁是或否被使用,再判断是否继续或者等待
Redis安装
-
拷贝文件
-
解压文件
-
tar -zxf redis
-
进入redis复制redis.conf到src
cp redis.conf src/
-
修改src下的redis.conf,将daemonize no改为daemonize yes
vi redis.conf
-
启动redis服务
一般方式启动redis不能ctrl+c,否则会立刻退出,这里通过添加守护进程的方式保证redis长服务,守护进程是daemon yes
./redis-server redis.conf
-
运行客户端
./redis-cli
-
开放端口
firewall-cmd --zone=public --add-port=6379/tcp --permanent firewall-cmd --reload
-
允许远程连接 修改redis.conf,注释 bind和内容
#bind 127.0.0.1
-
如果是测试用,可以将redis设置非保护模式,同时注释密码
protect-mode no
Redis数据类型
String(字符串)
string 是 redis 最基本的类型,你可以理解成与 Memcached 一模一样的类型,一个 key 对应一个value。
Hash(哈希)
Redis hash 是一个键值(key=>value)对集合。
List(列表)
Redis 列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边)。
Set(集合)
Redis的Set是string类型的无序集合。
集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是O(1)。
zset(sorted set:有序集合)
Redis zset 和 set 一样也是string类型元素的集合,且不允许重复的成员。
不同的是每个元素都会关联一个double类型的分数。redis正是通过分数来为集合中的成员进行从小到大的排序。
zset的成员是唯一的,但分数(score)却可以重复。
SpringBoot操作Redis数据
Redis事务
MULTI、EXEC、DISCARD和WATCH命令是Redis事务功能的基础。
Redis事务是一种乐观锁的机制,当事务过程中数据对象被改变,事务直接回滚
过程:用watch监控键值对,开启事务,判断键值对没被改变,那成功修改;如果判断键值对被修改,回滚事务
乐观锁ABA问题:提交事务前,监控事务的过程中,其他连接对该数据进行操作,比如+500,然后又改回来 -500,这种情况,如果以数据变化进行判断是否有无操作,此处将会判别为未被改动,但实际数据是动过的,这时事务会提交成功,就是非安全操作;
redis不会有该aba问题的原因就在于其是通过版本比对,操作数据一次就+1,事务前后版本不同则判别非安全操作,事务不会提交
Redis在Java中的事务操作
由于使用redisTemplate的exec不在同一个连接之中,事务无法提交.所以redis事务应该在execute的回调方法中执行(保证同一连接)
@Test
public void transactionTest() {
redisTemplate.execute(new SessionCallback() {
@Override
public Object execute(RedisOperations redisOperations) throws DataAccessException {
redisOperations.watch("mn");
redisOperations.multi();
redisOperations.opsForValue().get("mn");
redisOperations.opsForValue().set("mn",10000);
//以下exec是以上操作的结果集
List exec = redisOperations.exec();
System.out.println(exec);
return null;
}
});
}
注意redis事务不会因为数据类型转换的异常而回滚,比如set name “xiaoming” ,在事务提交之前进行自增,这时候事务不会回滚
Redis流水线
类似于队列批量协议,因为网络原因,redis单条查询速度远小于性能,所以可以一次发送一组操作命令,加快执行速度
正常速度:
@Test
public void setTest() {
redisTemplate.execute(new SessionCallback() {
@Override
public Object execute(RedisOperations redisOperations) throws DataAccessException {
long preTime = System.currentTimeMillis();
int i = 0;
while (true) {
long newTime = System.currentTimeMillis();
if (newTime - preTime > 1000) {
break;
}
redisTemplate.opsForValue().set("name", i++);
}
//1371次
System.out.println(i+"次");
return null;
}
});
}
pipeLined速度
@Test
public void executePipelined() {
redisTemplate.executePipelined(new SessionCallback() {
@Override
public Object execute(RedisOperations redisOperations) throws DataAccessException {
long preTime = System.currentTimeMillis();
int i = 0;
while (true) {
long newTime = System.currentTimeMillis();
if (newTime - preTime > 1000) {
break;
}
redisTemplate.opsForValue().set("name", i++);
}
//68721次
System.out.println(i+"次");
return null;
}
});
}
Redis发布订阅
开启一个SUBSCRIBE mn
A
另一个PUBLISH mn 1000
B
SUBSCRIBE mn
PUBLISH mn 1000
那么在A中可以收到消息
Redis超时命令
用于防止某些命令执行过程出错,但是一直占用资源的情况,设置超市可以及时回收内存资源
常用超时命令
命令名 | 作用 |
---|---|
persist | 时间永生 |
expire | 设置时间*秒 |
ttl | 查看剩余时间*秒(-1标识永生,-2标识被移除) |
pttl | 查看剩余时间*毫秒(-1标识永生,-2标识被移除) |
redisTemplate.expire("name", 25, TimeUnit.SECONDS);
Redis回收注意
key超时后,仍然不会从内存中移除
- 当调用get xxx时一定会移除超时的key
- 程序可编写一个定时器,定时移除超时的key
- 内存满的时候,再加入新的数据时,根据内存淘汰策略决定时候移除超时的key
redis.conf
持久化配置
快照RDB
将redis的内存结构制作快照(save命令),再进入redis的是恢复快照*默认开启
-
save 900 1 save 300 10 save 60 10000
分别表示900秒内有一个写命令备份快照;表示300秒内有10个写命令备份;表示60秒内有10000个写命令备份
-
stop-writes-on-bgsave-error yes
后台保存出错的时候停止写入操作
-
dbfilename dump.rdb
实际上就是redis的数据快照文件,这个文件如果转移到其他虚拟机,那那台虚拟机就有了被转移目标的redis数据
-
rdbchecksum yes
是否对快照文件进行验证
AOF
AOF值保存增删改命令:类似于mysql的数据日志,只将执行过的写命令依次保存在redis文件中,以后依次执行这些命令即可恢复redis数据*因为每次操作都有文件的读写命令,默认不开启
-
appendonly no
是否启用aof备份,需要就改为yes*默认为no
-
appendonly.aof
redis数据通过aof持久化的文件
-
appendfsync
持久化策略
- always 每次操作
- everysec 美妙
- no 一般情况*默认 (不影响性能)
-
auto-aof-rewrite-percentage 100
指定aof重写文件的条件,100表示文件增长量大于之前100%,则重写,配置为0表示禁用
64M表示文件达到64M,则重新写入新文件
-
aof-load-truncated yes
redis恢复文件是否忽略报错语句*默认yes
内存回收策略
lru:lru和ttl都是不精准的算法,如图,先从所有的key,随机找三个放左边进行查看使用次数少的,然后淘汰它,单这种取样本淘汰的可能不是所有key里面使用最少的,但是如果要找出所有key使用最少,太耗费性能
volatitle-lru
设置时间的按使用少的淘汰allkeys-lru
所有key按使用少的淘汰volatitle-random
随机淘汰设置时间的allkeys-random
随机淘汰所有的*不建议volatitle-ttl
删除存活时间最短的键值对noevition
默认
lua脚本
redis要求2.6以上,lua类似于js,是一个脚本语言,C语言编写,嵌入到redis进行业务逻辑的处理,也可用于nginx
redis是单线程
redis本身就是内存操作,对处理速度已经很快了,多线程模式由于其切换上下文的时间成本,不利于redis的执行速度.单线程的特点使得其执行lua的时候是不会打断的,所以是原子性操作(redis操作要么全部提交要么全部不提交)的
LUA语言基础
CenterOS内置lua
查看版本,有版本号就表示有
lua
基本数据类型
类型 | 解释 |
---|---|
nil | 相当于null |
boolean | true/false |
number | 整型+浮点型 |
string | 字符型(三种表达方式"";’’;[[]] ) |
String
-
字符串赋值
> a = "String1" > b = 'String2' > c = [[String3]] > print a stdin:1: '=' expected near 'a' > print (a) String1 > print (b) String2 > print (c) String3 >
-
字符串拼接
a = "a" .. "b"
LUA中的方法
a = 20
b = 30
function test()
local a = 10
local b = 30
print("a="..a.." b="..b)
end
test()
print ("a="..a.." b="..b)
执行得到
a=10 b=30
a=20 b=30因为后一个print (“a=”…a…" b="…b)得到的是最外面的全局a b,第一个print得到的是本地的a 和 b
基本表达式
条件表达式
age = arg[1]
if(tonumber(age) < 18) then
print("未成年")
else
if(tonumber(age)<60) then
print("中年")
else
print("老年")
end
end
year = arg[1]
if(((year%4==0)and(year%100~=0))or(year%400==0)) then
print("is闰年")
else
print("isnot闰年")
end
数组
array={1,3,5,7,9}
for i=1,#array,1 do
print("数组的值是"..array[i])
end
while循环
i = 0
while(i<10) do
print("i="..i)
i=i+1
end
for循环
for i=0,9,1 do
print("i的值是"..i)
end
冒泡排序
arr={1,2,55,21,34,14,12,51,6}
for i=1,#arr-1,1 do
for j=1,#arr-i,1 do
if(arr[j]>arr[j+1]) then
arr[j],arr[j+1] = arr[j+1],arr[j]
end
end
end
for i=1,#arr,1 do
print(arr[i])
end
Redis使用lua表达式
通过传过来lua脚本文件给redis,给lua执行,lua脚本可以写在配置文件
返回字符
eval "return 'hello world!'" 0
0标识这个脚本有几个key
给name赋值
eval "redis.call('set','name','xiaoming')" 0
取name值
EVAL "return redis.call('get','name')" 0
给hashmap赋值person.name = xiaoming 类似于hset person name xiaoming
eval "redis.call('hset',KEYS[1],ARGV[1],ARGV[2])" 1 person name xiaoming
注意参数名要大写
lua脚本的缓存
有时候为了反复执行同一段脚本,但是脚本内容可能很长,那么传输过程会很耗费性能.缓存可以解决这问题.缓存后,redis使用sha-1签名算法加密脚本,返回一个判别缓存脚本的字符串
-
进入./redis.cli,保存脚本进缓存
script load "redis.call('set',KEYS[1],ARGV[1])"
得到"7cfb4342127e7ab3d63ac05e0d3615fd50b45b06"的使用字符串
-
evalsha调用启动用的脚本字符串
EVALSHA "7cfb4342127e7ab3d63ac05e0d3615fd50b45b06" 1 name hu
Java执行lua脚本
运行使用普通lua脚本
@Test
public void evalScript() {
RedisConnection connection = redisTemplate.getConnectionFactory().getConnection();
String luaStr = "redis.call('set',KEYS[1],ARGV[1])";
//lua方式不会序列化key value
// connection.eval(luaStr.getBytes(), ReturnType.VALUE, 1, "dog".getBytes(), "cat".getBytes());
String luaGetStr = "return redis.call('get',KEYS[1])";
byte[] eval = connection.eval(luaGetStr.getBytes(), ReturnType.VALUE, 1, "dog".getBytes());
System.out.println(new String(eval));
}
运行使用普通lua脚本缓存lua脚本
@Test
public void evalScriptChe() {
RedisConnection connection = redisTemplate.getConnectionFactory().getConnection();
String luaStr = "redis.call('set',KEYS[1],ARGV[1])";
String returnStr = connection.scriptLoad(luaStr.getBytes());
//7cfb4342127e7ab3d63ac05e0d3615fd50b45b06
System.out.println(returnStr);
}
通过缓存脚本返回的身份字符串复调lua脚本
@Test
public void useSriptChe() {
RedisConnection connection = redisTemplate.getConnectionFactory().getConnection();
// String luaCheStr = "7cfb4342127e7ab3d63ac05e0d3615fd50b45b06";
// connection.evalSha(luaCheStr, ReturnType.VALUE, 1, "dog".getBytes(), "cat".getBytes());
String luaStr = "return redis.call('get',KEYS[1])";
byte[] result = connection.eval(luaStr.getBytes(), ReturnType.VALUE, 1, "dog".getBytes());
System.out.println(new String(result));
System.out.println(redisTemplate.opsForValue().get("dog"));
}
以文件形式运行lua文件
@Test
public void runLuaWithFile() throws IOException {
//获取classes编译目录
String path = this.getClass().getResource("/").getPath();
System.out.println(path);
String decodePath = URLDecoder.decode(path);
String luaPath = decodePath + "luaScript.lua";
File file = new File(luaPath);
if (file.exists()) {
System.out.println("文件存在");
}
//将脚本文件转化成byte数组
FileInputStream fileInputStream = new FileInputStream(file);
ByteOutputStream byteOutputStream = new ByteOutputStream();
int len;
byte[] bytes = new byte[1024 * 10];
while ((len = fileInputStream.read(bytes)) != -1) {
byteOutputStream.write(bytes, 0, len);
}
byte[] byteArray = byteOutputStream.toByteArray();
byteOutputStream.close();
//获取redis连接
RedisConnection connection = redisTemplate.getConnectionFactory().getConnection();
//执行脚本
byte[] eval = connection.eval(byteArray, ReturnType.VALUE, 1, "dog".getBytes());
System.out.println(new String(eval));
}
Redis缓存服务器
@RequestMapping("/getAll")
@ResponseBody
public Object getAll(int id) {
String key = "user" + id;
User user = (User) redisTemplate.opsForValue().get(key);
if (user != null) {
user = userDao.selectByPrimaryKey(id);
//重建该缓存
redisTemplate.opsForValue().set(key,user);
redisTemplate.expire(key, 5, TimeUnit.MINUTES);
}
return user;
}
缓存失效的特殊情况
-
某一时刻多个缓存失效,重建缓存
解决办法,超时时间错开
-
某一时刻,某个热门缓存失效,这时候大量数据进入
if (user != null) {}
代码中,都要重建缓存解决办法:加锁,分布式锁,使用redis的
setnx key value
指令
SpringBoot管理Redis缓存
注解 | 意义 |
---|---|
@Cacheable(key = “‘user’”,value = “cache”) | 表示redis服务器有缓存,就会直接当返回值返回,而不是进入方法*用于查询 |
@CachePut(key = “‘user’+#id”,value = “cache”) | 表示每次更新会更新对应的user+i缓存,用于新增和更新 |
@CacheEvict(key = “‘user’+#id”, value = “cache”) | 表示每次操作会删除user+i的缓存,用于删除 |
//查询全部
@Cacheable(key = "'user'",value = "cache")
@Override
public List<User> selectByExample(UserExample example) {
System.out.println("查询了数据库");
return userDao.selectByExample(example);
}
//查询单个
@Cacheable(key = "'user'+#id", value = "cache")
@Override
public User selectByPrimaryKey(Integer id) {
System.out.println("查询了数据库");
return userDao.selectByPrimaryKey(id);
}
/**CachePut一定会添加到缓存中,而且将返回结果放到缓存中,多用于添加和修改的方法
* @param record
* @return
*/
@CachePut(key = "'user'+#id", value = "cache")
@Override
public int updateByPrimaryKey(User record) {
System.out.println("查询了数据库");
return userDao.updateByPrimaryKeySelective(record);
}
@CacheEvict(key = "'user'+#id", value = "cache")
@Override
public int deleteByPrimaryKey(Integer id) {
System.out.println("查询了数据库");
return userDao.deleteByPrimaryKey(id);
}
/**更新一定要删除集合缓存,同时更新该条数据
* @param record
* @return
*/
@CachePut(key = "'user'+#id",value = "cache")
@CacheEvict(key = "'user'", value = "cache")
@Override
public int insert(User record) {
System.out.println("查询了数据库");
return userDao.insert(record);
}
redis集群和集群高可用
主从复制
- 主服务启动后,开始bgsave,同时发送同步命令给从服务器,从服务器不会拒绝写操作,会把命令存入缓冲区,根据数据正常返回还是报错
- 主服务bgsave完毕,发送数据快照给从,从必须舍弃当前数据开始载入快照
- 主服务将之前缓冲区残余的写命令发给从,从完成快照还原,继续完成写,于是主从复制完成
过程:
-
将所有从服务器的redis.conf下修改slaveof主服务器地址
slaveof 192.168.9.128 6379
-
如果安装的主服务的redis有设置
protect-mode yes
,就要设置masterauth root
-
主服务器,查看集群状态
info replication
切换到另一台服务,查找刚才的key
有两台从服务检测到,配置成功
读写分离
一般项目中,读90%,10%写入,把读单独一台,读9台,redis读写分离基于主从复制,由于redis的从服务网不具写入权限,所有的写入操作均由主服务完成,这就是redis读写分离
哨兵模式
1 、不时地监控redis是否按照预期良好地运行;
2、如果发现某个redis节点运行出现状况,能够通知另外一个进程(例如它的客户端);
3、能够进行自动切换。当一个master节点不可用时,能够选举出master的多个slave(如果有超过一个slave的话)中的一个来作为新的master,其它的slave节点会将它所追随的master的地址改为被提升为master的slave的新地址。
4、哨兵为客户端提供服务发现,客户端链接哨兵,哨兵提供当前master的地址然后提供服务,如果出现切换,也就是master挂了,哨兵会提供客户端一个新地址。
在高可用环境下,还可以设置哨兵高可用集群(多哨兵模式),多个哨兵互相通信,当多个哨兵侦查到redis服务主机宕机,哨兵之间会进行投票,选取出一台从服务从任主机,同时用发布订阅信息到每一台redis从机,修改他们的slaveof ip+端口
配置哨兵
-
如果安装的主服务的redis有设置
protect-mode no
,就要设置,取消protected-mode no的注释protected-mode no
-
默认端口26379
-
设置默认主机ip
sentinel monitor mymaster 192.168.9.128 6379 2
2代表只要有2个或者2个以上的哨兵认为主机宕机则选举从机为主机
-
如果设置了主机密码,下面就要把注释去掉,并设置连接名和密码
# sentinel auth-pass <master-name> <password>
-
启动redis哨兵
./redis-sentinel sentinel.conf
-
分别为其他服务器也配置哨兵,方法如上,并分别启动,启动之前务必将26379端口开启
-
sentinel down-after-millseconds
设置哨兵检测redis服务.在这个时间内没有应答任务主观下线,默认30秒 -
sentinel failover-timeout
指定故障切换的时间*毫秒,当超过这个时间则认定为故障,默认3分钟 -
sentinel notification-script
发生redis异常触发的脚本
集群测试
启动原有的集群情况下,新建一个128的连接进入主服务的redis-cli,shutdown其服务
- sdown:当前sentinel实例认为某个redis服务为"不可用"状态. 表示当前128主机宕机
- vote-for-leader:表示开始投票新的redis主机
- odown表示多个sentinel实例都认为master处于"SDOWN"状态,那么此时master将处于ODOWN,ODOWN可以简单理解为master已经被集群确定为"不可用",将会开启failover.
- failover:表示确认为失效待补救状态(上面翻译是:我会在昨天22点之后搞定问题)
- switch-master:转换主机到192.168.9.129
这时候查看130(第二台从服务)的redis.conf
选举之后还是从服务
再查看129的redis.conf
没有slaveof
再看129的redis-cli的replication
role是master
说明哨兵在选举之后,改变了redis.conf这个配置文件
如果这时候重启128
前主机再宕机后,即便重启,它的皇帝宝座也不再是它的了,最终还是老老实实做回老从吧
这样一来,所有的新增操作,都是由主服务操作,读则可以是redis主服务,也可以是从服务达到读写分离
Cluster模式分片集群
通过一个对key的hash函数,对应到的一个对16384取余,获得一个0到16383的值,在通过这个值,确定在哪个redis服务上执行
单点登录
分布式项目中,在登录工程进行登录之后,怎么在其他工程也保持登录状态,我们知道session是可以在同一台tomcat中共享,但是在多个项目之间可以用redis进行共享,通过登录成功把随机的uuid作为key,user对象作为value放在redis中,再取名一个字符串"token"做为key,uuid做为value防止在cookie中
我这里用的是dubbo分布式架构构建
搭建步骤
1.登录进行验证,验证成功生成token并保存redis和cookie
@RequestMapping("/login")
public Object login(User user, HttpServletResponse response) {
UserExample userExample = new UserExample();
UserExample.Criteria criteria = userExample.createCriteria();
criteria.andUsernameEqualTo(user.getUsername());
User userRS = userService.selectByExample(userExample).stream().findFirst().
orElse(null);
if (userRS.getPassword().equals(user.getPassword())) {
String uuid = UUID.randomUUID().toString();
redisTemplate.opsForValue().set(uuid,user);
redisTemplate.expire(uuid, 30, TimeUnit.MINUTES);
Cookie cookie = new Cookie("token",uuid);
//7天有效
cookie.setMaxAge(7 * 24 * 60 * 60);
//设置host
cookie.setDomain("localhost");
/*可能会有跨域的问题,为了防止银行的cookie被其他地址使用,所有本项目的cookie才
能本项目使用,所以要设置setPath("/"); */
cookie.setPath("/");
response.addCookie(cookie);
}
return "redirect:http://localhost:8084/userRedis/";
}
Cookie设置的过程一定要指定路径为"/".因为存在跨域问题
2.在其他工程首页验证cookie
@RequestMapping("getCookie")
@ResponseBody
public Object getCookie(@CookieValue("token") String token) {
return token;
}
CookieValue等同于
Cookie[] cookies = request.getCookies(); for (Cookie cookie : cookies) { if (cookie.getName().equals("token")) { String value = cookie.getValue(); break; } }
required = false表示该参数并不是必须,没有传值的时候为null
跨域问题
1.JS跨域
当不同工程互相ajax访问,比如8084端口访问8085端口的资源,就会发生不同域请求禁止访问的问题,使用jsonp原理:我们知道html加载其他网站的js,是不会发生跨域问题,jsonp相当于把你要访问的内容*(json数据),以加载网络js的方式下载过来,设置回调callback方法可以进行处理
$(function () {
$.ajax({
url:"http://localhost:8085/login/isLogin",
dataType:"jsonp",
});
})
function callback(data) {
if (data != "") {
$("#usernameLB").html(data.username+
"<a href='http://localhost:8085/login/logout'>注销</a>");
}
}
2.Cookie跨域
一般情况下,如果不对cookie设置path的话,默认按照当前设置cookie的项目路径当做了path,为了安全(比如淘宝的cookie不可能让京东去使用),因此其他path的工程就无权使用此cookie,设置跨域如下
cookie.setPath("/")
登录返回登录前页面
思路:把登录的时候前的地址当参数传递给后台
<a href="javascript:login()">登录</a>
function login() {
var url = location.href;
//用于对汉字进行转成unicode,避免乱码
encodeURI.replace("&", "*");
var encodeURI = encodeURI(url);
location.href = "http://localhost:8084/userRedis/lgin?lookUrl="+encodeURI;
}
@RequestMapping("/lgin")
public Object lgin(Model model,String lookUrl) {
model.addAttribute("lookUrl", lookUrl);
return "Login";
}
<form action="http://localhost:8085/login/login" method="get">
<input type="hidden" name="lookUrl" th:value="${lookUrl}"/>
账号<input type="text" name="username"></br>
密码<input type="text" name="password">
<input type="submit" value="提交">
</form>
@RequestMapping("/login")
public Object login(User user, HttpServletResponse response,String lookUrl) {
UserExample userExample = new UserExample();
UserExample.Criteria criteria = userExample.createCriteria();
criteria.andUsernameEqualTo(user.getUsername());
User userRS = userService.selectByExample(userExample).stream().findFirst().orElse(null);
if (userRS.getPassword().equals(user.getPassword())) {
String uuid = UUID.randomUUID().toString();
redisTemplate.opsForValue().set(uuid,user);
redisTemplate.expire(uuid, 30, TimeUnit.MINUTES);
Cookie cookie = new Cookie("token",uuid);
//7天有效
cookie.setMaxAge(7 * 24 * 60 * 60);
//设置host
cookie.setDomain("localhost");
//可能会有跨域的问题,为了防止银行的cookie被其他地址使用,所有本项目的cookie才能本项目使用,所以要设置setPath("/");
cookie.setPath("http://localhost:8084/userRedis/");
response.addCookie(cookie);
}
//前端对url进行&转*,后台转回来,防止在login方法开始的时候就把http://localhost:8085/login/login?lookUrl = http://localhost:8084/userRedis/?id=1&name=2
//中的lookUrl = http://localhost:8084/userRedis/?id=1当成一个参数name当做第二个参数
lookUrl = lookUrl.replace("*", "&");
System.out.println(lookUrl);
if (lookUrl == null) {
lookUrl = "";
}
return "redirect:"+lookUrl;
}
购物车应用
一般购物车只需要放置商品id,数量和用户id,订单里面除了有这些字段,还需要加入购买时的商品图片,商品价格.商品详情.因为商品时刻在变,下单的价格则只需要一个时刻.
-
session缺点:浏览器关闭就没了
-
cookie:pc端的购物车无法在移动端
-
数据库:让数据库压力太大
-
redis:和cookie类似
淘宝购物车:淘宝的购物车就是登录才能添加,
京东的购物车:不需要登录就可以添加. cookie+数据库,不使用redis的原因是有些人喜欢在购物车添加大量商品,这样无端占用了redis的内存空间
秒杀
倒计时
为保证秒杀业务的过程,必须用服务器时间进行参照,可以配置几台时间服务器,专门用来访问获取实际时间,为保证几台服务器的时间同步,要求每过一段时间对官方时间服务进行同步
库存减少策略
- 点击抢购即可减库存
- 付完款才减库存
假设1万人抢5000的库存
public synchronized void addOrder() {
for (int i = 0; i < 10000; i++) {
new Thread() {
@Override
public void run() {
Order order = orderService.selectByPrimaryKey(14);
Integer count = order.getCount();
if (count > 0) {
order.setCount(count - 1);
orderService.updateByPrimaryKey(order);
Miaosha record = new Miaosha();
miaoshaService.insert(record);
}
}
}.start();
}
}
不带任何锁 17秒
却产生了7809个订单,超卖
给程序加同步锁 2分05秒
public synchronized void addOrder() {}
然而执行时间却变得很长
使用悲观锁(for update) 1分35秒 数据可靠
悲观锁就是在操作数据时,认为此操作会出现数据冲突,所以在进行每次操作时都要通过获取锁才能进行对相同数据的操作
排它锁只能 作用在唯一主键或者唯一索引,不然就是表锁
SELECT * FROM `order` where id = 1 for update
排它锁:执行增删改,自带排他锁,如果在select语句后加for update,那么其他线程也不能操作该记录,在悲观锁内,锁住的记录,其他操作(除了select)必须等他事务提交才能继续操作
排它锁行锁:当forupdate作用在为唯一列,则只锁住操作行,其他行
查询
不受影响
使用乐观锁 29秒 没卖完
乐观锁是指操作数据库时(更新操作),想法很乐观,认为这次的操作不会导致冲突,在操作数据时,并不进行任何其他的特殊处理(也就是不加锁),而在进行更新后,再去判断是否有冲突了。
没卖完的原因是:查询数据库发现有人在用,那我不用了,导致商品没卖完,
解决办法
写个循环卖剩下的
首先在数据库表添加version字段
update order set count=count+1,version=version+1 where id = 1 and version=#{version}
直接减库存 33秒 没有超卖,也卖完了
不查询直接减库存,在sql上就做判断
update order set count=count+1 where id = 1 and count >= 1
redis实现秒杀
利用redis访问速度快的优势,再大并发到来的时候,把商品的数量数据准备在redis中,通过lua脚本的原子性对用户的采购暂时保存在redis中,当秒杀结束的时候,同步数据到sql数据库
-
编写lua脚本,要让每次购买,对应商品-1,同时保存秒杀商品整个对象
local num = tonumber(ARGV[1]) local id = KEYS[1] local count = tonumber(redis.call('hget','order'..id,'count')) if count == nil or count < num then return 1 end count = count - num redis.call('hset','order'..id,'count',count) local order = ARGV[2] redis.call('rpush','miaosha'..id,order) if count == 0 then return 2 else return 1 end
-
初始化定义为对象序列化为String类型
@PostConstruct public void init() { redisTemplate.setKeySerializer(new StringRedisSerializer()); redisTemplate.setValueSerializer(new StringRedisSerializer()); redisTemplate.setHashKeySerializer(new StringRedisSerializer()); redisTemplate.setHashValueSerializer(new StringRedisSerializer()); }
-
执行lua脚本(这里省略了lua文件的读取)
for (int i = 0; i < 2000; i++) { final int a = i; new Thread() { @Override public void run() { order.setCount(1); order.setGoodid(a); String miaoshaObject = new Gson().toJson(order); //integer代表返回值类型是一个数字 long result = connection.evalSha(luaCache, ReturnType.INTEGER, 1, (1 + "").getBytes(), (1 + "").getBytes(), miaoshaObject.getBytes()); if (result == 2) { System.out.println("商品秒杀结束,开始同步数据库!"); asyncService.Async(); } connection.close(); } }.start(); } }
-
使用springBoot的方法异步注解,使得方法异步执行,同步数据库,商品添加到数据库
使用方法,现在启动类上标志@EnableAsync,后标注要异步的方法上@Async
/** * 同步调用 */ @Override @Async @Transactional public void Async() { List<String> miaoshaList = redisTemplate.opsForList().range("miaosha1", 0, -1); for (String miaoshaStr : miaoshaList) { Gson gson = new Gson(); TypeToken<Miaosha> typeToken = new TypeToken<Miaosha>(){}; Miaosha miaosha = gson.fromJson(miaoshaStr, typeToken.getType()); miaoshaDao.insert(miaosha); } System.out.println("------------------同步完成------------------"); }