redis中的watch java_Redis(十五)Redis 的一些常用技术(Spring 环境下)

一、Redis 事务与锁机制

ea4971d9a72baedcfc95640c07aad930.png

1.Redis的基础事务

在Redis中开启事务的命令是 multi 命令, 而执行事务的命令是 exec 命令。multi 到 exec 命令之间的 Redis 命令将采取进入队列的形式,直至 exec 命令的出现,才会一次性发送队列里的命令去执行,而在执行这些命令的时候其他客户端就不能再插入任何命令了。

127.0.0.1:6379>multi

OK127.0.0.1:6379>set key1 value1

QUEUED127.0.0.1:6379>get key1

QUEUED127.0.0.1:6379>exec1) OK2) "value1"

如果回滚事务,可以使用 discard 命令取消事务中所有命令,使事务中的方法不会被执行了。

127.0.0.1:6379>multi

OK127.0.0.1:6379>set key1 value1

QUEUED127.0.0.1:6379>get key1

QUEUED127.0.0.1:6379>discard

OK127.0.0.1:6379>exec

(error) ERR EXEC without MULTI

2.在Spring中使用Redis事务

SessionCallback接口可以保证所有的命令都是通过同一个 Redis 连接进行操作的。

public static voidtestTransaction() {

ApplicationContext applicationContext= new ClassPathXmlApplicationContext("applicationContext.xml");

RedisTemplate redisTemplate= applicationContext.getBean(RedisTemplate.class);

SessionCallback callBack= (SessionCallback) (RedisOperations ops) ->{

ops.multi();//开启事务

ops.boundValueOps("key1").set("value1");//注意由于命令只是进入队列,而没有被执行,所以此处采用get命令返回值为null

String value = (String) ops.boundValueOps("key1").get();

System.out.println("value = " +value);//list保存之前进入队列的所有命令的结果

List list = ops.exec();//执行事务//事务结束后,取出value1

value = (String) redisTemplate.opsForValue().get("key1");returnvalue;

};//执行Redis命令

String value =(String) redisTemplate.execute(callBack);

System.out.println(value);

}

返回结果:

value = null

value1

3.Redis 事务回滚的两种情况

命令格式正确,而数据类型错误时,仅回滚数据类型错误的那条命令

127.0.0.1:6379>multi

OK127.0.0.1:6379>set key1 value1

QUEUED127.0.0.1:6379>set key2 value2

QUEUED127.0.0.1:6379>incr key1

QUEUED127.0.0.1:6379>del key2

QUEUED127.0.0.1:6379>exec1) OK2) OK3) (error) ERR value is not an integer or out of range4) (integer) 1

命令格式不正确时,直接回滚所有命令

127.0.0.1:6379>multi

OK127.0.0.1:6379>set key1 value1

QUEUED127.0.0.1:6379>incr

(error) ERR wrong number of argumentsfor 'incr'command127.0.0.1:6379>set key2 value2

QUEUED127.0.0.1:6379>exec

(error) EXECABORT Transaction discarded because of previous errors.127.0.0.1:6379>get key1

(nil)127.0.0.1:6379>get key2

(nil)

4.使用 watch 命令监控事务

在 Redis 中使用 watch 命令可以决定事务是执行还是回滚。一般而言,可以在 multi 命令之前使用 watch 命令监控某些键值对,然后使用 multi 命令开启事务。当Redis 使用 exec 命令执行事务的时候,它首先会去对比被 watch 命令所监控的键值对,如果没有发生变化,那么它会执行事务队列中的命令,提交事务;如果发生变化,那么它不会执行任何事务中的命令,而去事务回滚。无论事务是否回滚,Redis都会去取消执行事务前的watch命令:

5165d57f6277462e08b00d0cebff536b.png

Redis 参考了多线程中使用的 CAS (比较与交换,Compare and Swap)去执行的。当一条线程去执行某些业务逻辑,但是这些业务逻辑操作的数据可能被其他线程共享了,这样会引发多线程中数据不一致的情况。为了克服这个问题,在线程开始时读取这些多线程共享的数据,并将其保存到当前线程的副本中,称为旧值(old value),watch命令就是这样的一个功能。然后,开启线程业务逻辑,由multi命令提供这个功能。在执行更新即exec命令前,比较当前线程副本保存的旧值和当前线程共享的值是否一致,如果不一致,那么该数据已经被其他线程操作过,此次更新失败,事务回滚;否则就认为它没有被其他线程操作过,就执行对应的业务逻辑。在数据高并发环境的操作中,把这样的机制称为乐观锁。

CAS 会产生 ABA 问题,而 Redis不会产生 ABA 问题。

ca7e313b760f98047cd651c655558253.png

产生ABA问题的根本原因就是仅仅只记录一个旧值,解决办法例如有Hibernate中对缓存的持久对象加入字段 version 值,每操作一次持久对象,就令version++,可以解决ABA问题。

Redis多个事务完全可以在非阻塞的多线程环境下并发执行,而且Redis的机制是不会产生ABA问题的。

例如:成功提交事务的例子:

127.0.0.1:6379>flushdb

OK127.0.0.1:6379>set key1 value1

OK127.0.0.1:6379>watch key1

OK127.0.0.1:6379>multi

OK127.0.0.1:6379>set key2 value2

QUEUED127.0.0.1:6379>get key2

QUEUED127.0.0.1:6379>exec1) OK2) "value2"

127.0.0.1:6379>get key1"value1"

127.0.0.1:6379>get key2"value2"

二、流水线(PipeLined)

当需要使用队列批量执行一系列的命令时,Pipelined可以提高系统性能。

Redis执行读/写速度非常快,但是系统的瓶颈往往是在网络通信中的时延:

aa353aff19d4762ab6d4a6cd49f38977.png

为了解决这个问题,可以使用Redis的流水线,Redis的流水线是一种通信协议:

1.使用 Java API

public static voidtestJedisPipeline() {

JedisPool pool=getPool();

Jedis jedis=pool.getResource();long start =System.currentTimeMillis();//开启流水线

Pipeline pipeline =jedis.pipelined();//测试十万条读/写操作

for (int i = 0; i < 100000; i++) {int j = i + 1;

pipeline.set("pipeline_key_" + j, "pipeline_value_" +j);

pipeline.get("pipeline_key_" +j);

}//pipeline.sync();//只执行同步,不返回结果//pipeline.syncAndReturnAll(); 将返回执行过的命令放入List列表中

List result =pipeline.syncAndReturnAll();long end =System.currentTimeMillis();

System.err.println("耗时: " + (end - start) + "毫秒");

}

返回:耗时: 499毫秒

2.在Spring中使用流水线

public static voidtestPipeline() {

ApplicationContext applicationContext= new ClassPathXmlApplicationContext("applicationContext.xml");

RedisTemplate redisTemplate= applicationContext.getBean(RedisTemplate.class);

SessionCallback callBack= (SessionCallback) (RedisOperations ops) ->{for (int i = 0; i < 100000; i++) {int j = i + 1;

ops.boundValueOps("pipeline_key_" + j).set("pipeline_value_" +j);

ops.boundValueOps("pipeline_key_" +j).get();

}return null;

};long start =System.currentTimeMillis();//执行 Redis 的流水线命令

List resultList =redisTemplate.executePipelined(callBack);long end =System.currentTimeMillis();

System.out.println(end-start);

}

返回:511

三、发布订阅

当使用银行卡消费的时候,银行往往会通过微信、短信或者邮件通知用户这笔交易的信息,这便是一种发布/订阅模式。

发布订阅模式首先需要消息源,也就是要有消息发布出来,比如银行通知。首先是银行的记账系统收到了交易的命令,交易成功后,就会把消息发送出来,订阅者就可以接收到这个消息。

发布订阅需要两点:

要有发送的消息渠道,让记账系统能够发送消息

要有订阅者订阅这个渠道的消息

1.Redis中的发布订阅

客户端1监听一个叫做chat的频道:SUBSCRIBE chat

客户端2在chat上发送消息:publish chat “hello”

此时,客户端1就收到了客户端2发送到chat上面的消息:“hello”

2.在Spring环境下使用发布订阅

(1)Spring中,接收者需要实现MessageListener接口,并实现其中的onMessage方法

packagecom.ssm.chapter19.redis.listener;importorg.springframework.data.redis.connection.Message;importorg.springframework.data.redis.connection.MessageListener;importorg.springframework.data.redis.core.RedisTemplate;public class RedisMessageListener implementsMessageListener {privateRedisTemplate redisTemplate;publicRedisTemplate getRedisTemplate() {returnredisTemplate;

}public voidsetRedisTemplate(RedisTemplate redisTemplate) {this.redisTemplate =redisTemplate;

}

@Overridepublic void onMessage(Message message, byte[] bytes) {//获取消息

byte[] body =message.getBody();//使用值反序列化其转换

String msgBody =(String) getRedisTemplate().getValueSerializer().deserialize(body);

System.err.println(msgBody);//获取频道

byte[] channel =message.getChannel();//使用字符串序列化器转换

String channelStr =(String) getRedisTemplate().getStringSerializer().deserialize(channel);

System.err.println(channelStr);//将频道名称的字节数组转换成字符串

String bytesStr = newString(bytes);

System.err.println(bytesStr);

}

}

(2)在Spring 配置文件中配置这个类

(3)还需要配置监听容器RedisMessageListenerContainer可以用于监听Redis的发布订阅消息,指定频道名称为chat

当消息通过chat发送时,就会使用redisMsgListener进行处理。

(4)测试:执行下面的方法后,控制台输出结果为:

public static voidtestPubSub() {

ApplicationContext applicationContext= new ClassPathXmlApplicationContext("applicationContext.xml");

RedisTemplate redisTemplate= applicationContext.getBean(RedisTemplate.class);

String channel= "chat";

redisTemplate.convertAndSend(channel,"I am lazy!!");

}

控制台输出结果为:

I am lazy!!chat

chat

四、超时命令

对于Redis而言,del命令可以删除一些键值对,所以Redis比Java虚拟机更加灵活,与此同时,当内存运行空间满了之后,还可以按照回收机制自动回收一些键值对。

但是,当垃圾进行回收的时候,又有可能执行回收而引发系统停顿,因此选择适当的回收机制和时间将有利于系统性能的提高。

Redis可以给对应的键值设置超时:

842619ac5843c091c2238347ea3b9e63.png

1.在Redis中测试超时命令

127.0.0.1:6379>set key1 value1

OK127.0.0.1:6379>get key1"value1"

127.0.0.1:6379>ttl key1

(integer)-1

127.0.0.1:6379> expire key1 120(integer)1

127.0.0.1:6379>ttl key1

(integer)112

127.0.0.1:6379>ttl key1

(integer)110

127.0.0.1:6379>ttl key1

(integer)110

127.0.0.1:6379>ttl key1

(integer)108

127.0.0.1:6379>ttl key1

(integer)65

127.0.0.1:6379>persist key1

(integer)1

127.0.0.1:6379>persist key1

(integer)0

127.0.0.1:6379>ttl key1

(integer)-1

2.在Spring中使用超时命令

public static voidtestExpire() {

ApplicationContext applicationContext= new ClassPathXmlApplicationContext("applicationContext.xml");

RedisTemplate redisTemplate= applicationContext.getBean(RedisTemplate.class);

redisTemplate.execute((RedisOperations ops)->{

ops.boundValueOps("key1").set("value1");

String keyValue= (String) ops.boundValueOps("key1").get();

Long expSecond= ops.getExpire("key1");

System.err.println(expSecond);boolean b = false;

b= ops.expire("key1", 120L, TimeUnit.SECONDS);

b= ops.persist("key1");

Long l= 0L;

l= ops.getExpire("key1");

Long now=System.currentTimeMillis();

Date date= newDate();

date.setTime(now+ 120000);

ops.expireAt("key", date);return null;

});

}

3.问题:如果key超时了,Redis 会回收key的存储空间吗?

不会。Redis的key超时不会被其自动回收,它只会标识哪些键值对超时了。

这样做的好处是,如果一个很大的键值对超时,必须一个列表或者哈希结构,存在数以百万个元素,要对其回收需要很长时间。如果采用超时回收,则可能产生系统停顿。坏处也很明显,就是超时的键值对会浪费比较多的空间。

Redis 提供两种方式回收超时键值对:

定时回收:在确定的某个时间触发一段代码,回收超时的键值对。定时回收可以完全回收那些超时的键值对,但是缺点也很明显,如果这些键值对比较多,则Redis需要运行较长的时间,从而导致停顿。一般会选择在没有业务发生的时刻触发Redis的定时回收,以便清理超时的键值对。

惰性回收:当一个超时的键,被再次用get命令访问时,将触发Redis将其从内存中情况。优势是可以指定回收超时的键值对,缺点是要执行一个get操作,或者在某些时候,难以判断哪些键值对已经超时。

五、使用Lua语言

Redis 命令的计算能力不算很强大,而使用Lua语言则在很大程度上弥补了 Redis 这个不足。只是在 Redis中,执行 Lua 语言是原子性的,也就是Redis执行Lua的时候是不会被中断的。

Redis支持阆中方式运行Lua,一种是直接输入;另外一种是将 Lua 语言编写成文件。

1.执行输入Lua程序代码

eval lua-script key-num [key1 key2 key3 ...] [value1 value2 value3 ...]

eval:执行Lua语言的命令

Lua-script:代表Lua语言脚本

key-num:代表参数中有多少个key,没有为0

[key1 key2 key3 ...]:以key为参数

[value1 value2 value3 ...]:将这些参数传递给Lua

例如:

127.0.0.1:6379> eval "return 'hello java'" 0

"hello java"

127.0.0.1:6379> eval "redis.call('set',KEYS[1],ARGV[1])" 1 lua-key lua-value

(nil)127.0.0.1:6379> get lua-key"lua-value"

有时可能需要多次执行同一段脚本,在Redis中脚本会通过SHA-1签名算法加密脚本,返回一个标识字符串,可以通过这个字符串执行加密后的脚本。这样的好处是,如果脚本很长,从客户端传输可能需要很长的时间,那么使用标识字符串,则只需要传递32位字符串即可,这样可以提高传输的效率,从而提高性能。

127.0.0.1:6379> script load "redis.call('set',KEYS[1],ARGV[1])"

"7cfb4342127e7ab3d63ac05e0d3615fd50b45b06"

127.0.0.1:6379> evalsha 7cfb4342127e7ab3d63ac05e0d3615fd50b45b06 1 sha-key sha-value

(nil)127.0.0.1:6379> get sha-key"sha-value"

2.在Spring 中使用 Lua 脚本存储简单字符串

public static voidtestLuaScript() {

ApplicationContext applicationContext= new ClassPathXmlApplicationContext("applicationContext.xml");

RedisTemplate redisTemplate= applicationContext.getBean(RedisTemplate.class);

Jedis jedis=(Jedis) redisTemplate.getConnectionFactory().getConnection().getNativeConnection();//执行简单的脚本

String helloJava = (String) jedis.eval("return 'hello java'");

System.out.println(helloJava);//执行带参数的脚本

jedis.eval("redis.call('set',KEYS[1], ARGV[1])", 1, "lua-key", "lua-value");

String luaKey= (String) jedis.get("lua-key");

System.out.println(luaKey);//缓存脚本,返回SHA1签名标识字符串

String sha1 = jedis.scriptLoad("redis.call('set',KEYS[1], ARGV[1])");//执行脚本

jedis.evalsha(sha1, 1, new String[] { "sha-key", "sha-val"});//获取执行脚本后的数据

String shaVal = jedis.get("sha-key");

System.out.println(shaVal);//�关闭连接

jedis.close();

}

3.在Spring中使用Lua脚本存储对象

Spring 提供了 RedisScript 接口和一个实现类 DefaultRedisScript ,通过这个对象就可以通过Lua脚本操作对象。

public static voidtestRedisScript() {

ApplicationContext applicationContext= new ClassPathXmlApplicationContext("applicationContext.xml");

RedisTemplate redisTemplate= applicationContext.getBean(RedisTemplate.class);//定义默认脚本封装类

DefaultRedisScript redisScript = new DefaultRedisScript();//设置脚本

redisScript.setScriptText("redis.call('set', KEYS[1], ARGV[1]) return redis.call('get', KEYS[1])");//定义操作的key列表

List keyList = new ArrayList();

keyList.add("role1");//需要序列化保存和读取的对象

Role role = newRole();

role.setId(1L);

role.setRoleName("role_name_1");

role.setNote("note_1");//获得标识字符串

String sha1 =redisScript.getSha1();

System.out.println(sha1);//设置返回结果类型为Role类型

redisScript.setResultType(Role.class);//使用JdkSerializationRedisSerializer进行序列化

JdkSerializationRedisSerializer serializer = newJdkSerializationRedisSerializer();//执行脚本//DefaultRedisScript接口对象,参数序列化器,结果序列化器,key列表,参数列表

Role obj =(Role) redisTemplate.execute(redisScript, serializer, serializer, keyList, role);//打印返回结果

System.out.println(obj.getId());

}

返回:

731429de653665577edb661a6741c4083e103b771

4.执行Lua文件

新建Lua文件test.lua

redis.call('set', KEYS[1], ARGV[1])

redis.call('set', KEYS[2], ARGV[2])

local n1= tonumber(redis.call('get', KEYS[1]))

local n2= tonumber(redis.call('get', KEYS[2]))if n1 >n2 thenreturn 1endif n1 ==n2 thenreturn 0endif n1

在命令行输入 redis-cli --eval test.lua key1 key2 , 2 4 会返回:2

在 Spring 中,只能通过evalsha的方式执行Lua文件,例如:

public static voidtestLuaFile() {

ApplicationContext applicationContext= new ClassPathXmlApplicationContext("applicationContext.xml");

RedisTemplate redisTemplate= applicationContext.getBean(RedisTemplate.class);//读入文件流

File file = new File("D:\\BaiduNetdiskDownload\\ssm\\Chapter19\\src\\test.lua");byte[] bytes =getFileToByte(file);

Jedis jedis=(Jedis) redisTemplate.getConnectionFactory().getConnection().getNativeConnection();//发送二进制文件给Redis服务器,得到标识数组

byte[] sha1 =jedis.scriptLoad(bytes);//传递参数,执行Lua文件

Object obj = jedis.evalsha(sha1, 2, "key1".getBytes(), "key2".getBytes(), "2".getBytes(), "4".getBytes());

System.out.println(obj);

}/*** 把文件转化为二进制数组

*

*@paramfile

*

*@return二进制数组*/

public static byte[] getFileToByte(File file) {byte[] by = new byte[(int) file.length()];try{

InputStream is= newFileInputStream(file);

ByteArrayOutputStream bytestream= newByteArrayOutputStream();byte[] bb = new byte[2048];intch;

ch=is.read(bb);while (ch != -1) {

bytestream.write(bb,0, ch);

ch=is.read(bb);

}

by=bytestream.toByteArray();

}catch(Exception ex) {

ex.printStackTrace();

}returnby;

}

六、在Spring中使用 Redis 哨兵模式

1.配置文件

主服务器192.168.11.128,两个从服务器192.168.11.129、192.168.11.130。

然后在三台机器上分别启动哨兵服务。

http://www.springframework.org/schema/beans/spring-beans-4.0.xsd">

2.验证哨兵模式

关闭192.168.11.128主服务其上的Redis服务,然后3分钟后,哨兵会进行投票切换新的主机,然后执行下面的方法。

public static voidtestSpringSentinel() {

ApplicationContext ctx= new ClassPathXmlApplicationContext("com/ssm/chapter20/config/spring-cfg.xml");

RedisTemplate redisTemplate= ctx.getBean(RedisTemplate.class);

String retVal= (String) redisTemplate.execute((RedisOperations ops) ->{

ops.boundValueOps("mykey").set("myvalue");

String value= (String) ops.boundValueOps("mykey").get();returnvalue;

});

System.out.println(retVal);

}

七、Spring 缓存机制和Redis的结合

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值