1) Giới thiệu
Chào mọi người! Nhiều người biết Redis là gì và nếu bạn không biết, trang web chính thức có thể cập nhật cho bạn.
Đối với hầu hết Redis là một bộ nhớ cache và đôi khi là một hàng đợi tin nhắn.
Nhưng điều gì sẽ xảy ra nếu chúng ta điên rồ một chút và cố gắng thiết kế toàn bộ ứng dụng chỉ sử dụng Redis làm nơi lưu trữ dữ liệu? Chúng ta có thể giải quyết những nhiệm vụ gì với Redis?
Chúng tôi sẽ cố gắng trả lời những câu hỏi này trong bài viết này.
Những gì chúng ta sẽ không thấy ở đây?
- Mọi chi tiết về cấu trúc dữ liệu Redis sẽ không có ở đây. Bạn nên đọc các bài báo hoặc tài liệu đặc biệt vì mục đích gì.
- Ở đây cũng sẽ không có mã sẵn sàng sản xuất nào mà bạn có thể sử dụng trong công việc của mình.
Chúng ta sẽ thấy gì ở đây?
- Chúng tôi sẽ sử dụng các cấu trúc dữ liệu Redis khác nhau để triển khai các tác vụ khác nhau của ứng dụng hẹn hò.
- Đây sẽ là các ví dụ về mã Kotlin + Spring Boot.
2) Tìm hiểu cách tạo và truy vấn hồ sơ người dùng.
-
Đầu tiên, hãy tìm hiểu cách tạo hồ sơ người dùng với tên, lượt thích của họ, v.v.
Để làm điều này, chúng ta cần một kho khóa-giá trị đơn giản. Làm thế nào để làm nó?
- Đơn giản. Redis có cấu trúc dữ liệu - một hàm băm. Về bản chất, đây chỉ là một bản đồ băm quen thuộc đối với tất cả chúng ta.
Các lệnh ngôn ngữ truy vấn của Redis có thể được tìm thấy ở đây và ở đây.
Tài liệu thậm chí còn có một cửa sổ tương tác để thực hiện các lệnh này ngay trên trang. Và toàn bộ danh sách lệnh có thể được tìm thấy ở đây.
Các liên kết tương tự hoạt động cho tất cả các lệnh tiếp theo mà chúng tôi sẽ xem xét.
Trong mã, chúng tôi sử dụng RedisTemplate hầu như ở mọi nơi. Đây là điều cơ bản để làm việc với Redis trong hệ sinh thái Spring.
Một sự khác biệt so với bản đồ ở đây là chúng tôi chuyển "trường" làm đối số đầu tiên. “Trường” là tên băm của chúng ta.
fun addUser(user: User) {
val hashOps: HashOperations<String, String, User> = userRedisTemplate.opsForHash()
hashOps.put(Constants.USERS, user.name, user)
}
fun getUser(userId: String): User {
val userOps: HashOperations<String, String, User> = userRedisTemplate.opsForHash()
return userOps.get(Constants.USERS, userId)?: throw NotFoundException("Not found user by $userId")
}
Trên đây là một ví dụ về cách nó có thể trông như thế nào trong Kotlin bằng cách sử dụng các thư viện của Spring.
Tất cả các đoạn mã từ bài viết đó bạn có thể tìm thấy trên Github.
3) Cập nhật lượt thích của người dùng bằng danh sách Redis.
-
Tuyệt quá!. Chúng tôi có người dùng và thông tin về lượt thích.
Bây giờ chúng ta nên tìm cách cập nhật lượt thích đó.
Chúng tôi cho rằng các sự kiện có thể xảy ra rất thường xuyên. Vì vậy, hãy sử dụng cách tiếp cận không đồng bộ với một số hàng đợi. Và chúng tôi sẽ đọc thông tin từ hàng đợi theo lịch trình.
- Redis có cấu trúc dữ liệu danh sách với một tập hợp các lệnh như vậy. Bạn có thể sử dụng danh sách Redis cả dưới dạng hàng đợi FIFO và như một ngăn xếp LIFO.
Trong Spring, chúng tôi sử dụng cùng một phương pháp lấy ListOperations từ RedisTemplate.
Chúng ta phải viết về bên phải. Bởi vì ở đây chúng ta đang mô phỏng một hàng đợi FIFO từ phải sang trái.
fun putUserLike(userFrom: String, userTo: String, like: Boolean) {
val userLike = UserLike(userFrom, userTo, like)
val listOps: ListOperations<String, UserLike> = userLikeRedisTemplate.opsForList()
listOps.rightPush(Constants.USER_LIKES, userLike)
}
Bây giờ chúng tôi sẽ thực hiện công việc của mình theo đúng lịch trình.
Chúng tôi chỉ đơn giản là chuyển thông tin từ cấu trúc dữ liệu Redis này sang cấu trúc dữ liệu Redis khác. Điều này là đủ cho chúng tôi như một ví dụ.
fun processUserLikes() {
val userLikes = getUserLikesLast(USERS_BATCH_LIMIT).filter{ it.isLike}
userLikes.forEach{updateUserLike(it)}
}
Cập nhật người dùng thực sự dễ dàng ở đây. Giới thiệu cho HashOperation từ phần trước.
private fun updateUserLike(userLike: UserLike) {
val userOps: HashOperations<String, String, User> = userLikeRedisTemplate.opsForHash()
val fromUser = userOps.get(Constants.USERS, userLike.fromUserId)?: throw UserNotFoundException(userLike.fromUserId)
fromUser.fromLikes.add(userLike)
val toUser = userOps.get(Constants.USERS, userLike.toUserId)?: throw UserNotFoundException(userLike.toUserId)
toUser.fromLikes.add(userLike)
userOps.putAll(Constants.USERS, mapOf(userLike.fromUserId to fromUser, userLike.toUserId to toUser))
}
Và bây giờ chúng tôi hướng dẫn cách lấy dữ liệu từ danh sách. Chúng tôi đang nhận được điều đó từ bên trái. Để lấy một loạt dữ liệu từ danh sách, chúng tôi sẽ sử dụng một range
phương pháp.
Và có một điểm quan trọng. Phương thức phạm vi sẽ chỉ lấy dữ liệu từ danh sách, nhưng không xóa nó.
Vì vậy chúng ta phải sử dụng phương pháp khác để xóa dữ liệu. trim
làm đi. (Và bạn có thể có một số câu hỏi ở đó).
private fun getUserLikesLast(number: Long): List<UserLike> {
val listOps: ListOperations<String, UserLike> = userLikeRedisTemplate.opsForList()
return (listOps.range(Constants.USER_LIKES, 0, number)?:mutableListOf()).filterIsInstance(UserLike::class.java)
.also{
listOps.trim(Constants.USER_LIKES, number, -1)
}
}
Và các câu hỏi là:
- Làm cách nào để chuyển dữ liệu từ danh sách thành nhiều chuỗi?
- Và làm thế nào để đảm bảo dữ liệu sẽ không bị mất trong trường hợp có lỗi? Từ hộp - không có gì. Bạn phải lấy dữ liệu từ danh sách trong một chuỗi. Và bạn phải tự mình xử lý tất cả các sắc thái nảy sinh.
4) Gửi thông báo đẩy cho người dùng bằng pub / sub
-
Cứ tiến về phía trước!
Chúng tôi đã có hồ sơ người dùng. Chúng tôi đã tìm ra cách xử lý luồng lượt thích từ những người dùng này.Nhưng hãy tưởng tượng trường hợp bạn muốn gửi thông báo đẩy cho người dùng vào thời điểm chúng tôi có lượt thích.
Bạn sẽ làm gì?
- Chúng tôi đã có một quy trình không đồng bộ để xử lý lượt thích, vì vậy, hãy xây dựng việc gửi thông báo đẩy vào đó. Tất nhiên, chúng tôi sẽ sử dụng WebSocket cho mục đích đó. Và chúng tôi chỉ có thể gửi nó qua WebSocket nơi chúng tôi nhận được một lượt thích. Nhưng nếu chúng ta muốn thực thi mã dài trước khi gửi thì sao? Hoặc nếu chúng tôi muốn ủy quyền công việc với WebSocket cho một thành phần khác thì sao?
- Chúng tôi sẽ lấy và chuyển lại dữ liệu của mình từ cấu trúc dữ liệu Redis này (danh sách) sang cấu trúc dữ liệu Redis khác (pub / sub).
fun processUserLikes() {
val userLikes = getUserLikesLast(USERS_BATCH_LIMIT).filter{ it.isLike}
pushLikesToUsers(userLikes)
userLikes.forEach{updateUserLike(it)}
}
private fun pushLikesToUsers(userLikes: List<UserLike>) {
GlobalScope.launch(Dispatchers.IO){
userLikes.forEach {
pushProducer.publish(it)
}
}
}
@Component
class PushProducer(val redisTemplate: RedisTemplate<String, String>, val pushTopic: ChannelTopic, val objectMapper: ObjectMapper) {
fun publish(userLike: UserLike) {
redisTemplate.convertAndSend(pushTopic.topic, objectMapper.writeValueAsString(userLike))
}
}
Trình nghe ràng buộc với chủ đề nằm trong cấu hình.
Bây giờ, chúng ta chỉ có thể đưa người nghe của mình vào một dịch vụ riêng biệt.
@Component
class PushListener(val objectMapper: ObjectMapper): MessageListener {
private val log = KotlinLogging.logger {}
override fun onMessage(userLikeMessage: Message, pattern: ByteArray?) {
// websocket functionality would be here
log.info("Received: ${objectMapper.readValue(userLikeMessage.body, UserLike::class.java)}")
}
}
5) Tìm người dùng gần nhất thông qua các hoạt động địa lý.
- Chúng tôi đã hoàn thành với các lượt thích. Nhưng khả năng tìm thấy những người dùng gần nhất với một thời điểm nhất định thì sao.
- GeoOperations sẽ giúp chúng tôi điều này. Chúng tôi sẽ lưu trữ các cặp khóa-giá trị, nhưng bây giờ giá trị của chúng tôi là tọa độ người dùng. Để tìm, chúng tôi sẽ sử dụng
[radius](https://redis.io/commands/georadius)
phương pháp. Chúng tôi chuyển id người dùng để tìm và chính bán kính tìm kiếm.
Hiển thị kết quả trả về bao gồm id người dùng của chúng tôi.
fun getNearUserIds(userId: String, distance: Double = 1000.0): List<String> {
val geoOps: GeoOperations<String, String> = stringRedisTemplate.opsForGeo()
return geoOps.radius(USER_GEO_POINT, userId, Distance(distance, RedisGeoCommands.DistanceUnit.KILOMETERS))
?.content?.map{ it.content.name}?.filter{ it!= userId}?:listOf()
}
6) Cập nhật vị trí của người dùng thông qua các luồng
-
Chúng tôi đã triển khai hầu hết mọi thứ mà chúng tôi cần. Nhưng bây giờ chúng tôi lại gặp phải tình huống khi chúng tôi phải cập nhật dữ liệu có thể sửa đổi nhanh chóng.
Vì vậy, chúng ta phải sử dụng lại hàng đợi, nhưng sẽ rất tuyệt nếu có thứ gì đó có thể mở rộng hơn.
- Luồng Redis có thể giúp giải quyết vấn đề này.
- Có thể bạn biết về Kafka và thậm chí có thể bạn biết về các luồng Kafka, nhưng nó không giống với các luồng Redis. Nhưng bản thân Kafka là một thứ khá giống với Redis stream. Nó cũng là một cấu trúc dữ liệu log trước có nhóm người tiêu dùng và phần bù đắp. Đây là một cấu trúc dữ liệu phức tạp hơn, nhưng nó cho phép chúng ta lấy dữ liệu song song và sử dụng phương pháp phản ứng.
Xem tài liệu luồng Redis để biết chi tiết.
Spring có ReactiveRedisTemplate và RedisTemplate để làm việc với cấu trúc dữ liệu Redis. Sẽ thuận tiện hơn cho chúng tôi khi sử dụng RedisTemplate để ghi giá trị và ReactiveRedisTemplate để đọc. Nếu chúng ta nói về các luồng. Nhưng trong những trường hợp như vậy, không có gì sẽ hoạt động.
Nếu ai đó biết tại sao nó hoạt động theo cách này, vì Spring hoặc Redis, hãy viết trong phần bình luận.
fun publishUserPoint(userPoint: UserPoint) {
val userPointRecord = ObjectRecord.create(USER_GEO_STREAM_NAME, userPoint)
reactiveRedisTemplate
.opsForStream<String, Any>()
.add(userPointRecord)
.subscribe{println("Send RecordId: $it")}
}
Phương pháp lắng nghe của chúng tôi sẽ giống như sau:
@Service
class UserPointsConsumer(
private val userGeoService: UserGeoService
): StreamListener<String, ObjectRecord<String, UserPoint>> {
override fun onMessage(record: ObjectRecord<String, UserPoint>) {
userGeoService.addUserPoint(record.value)
}
}
Chúng tôi chỉ chuyển dữ liệu của mình vào cấu trúc dữ liệu địa lý.
7) Đếm số phiên duy nhất bằng HyperLogLog.
- Và cuối cùng, hãy tưởng tượng rằng chúng ta cần tính xem có bao nhiêu người dùng đã vào ứng dụng mỗi ngày.
- Hơn nữa, hãy nhớ rằng chúng tôi có thể có rất nhiều người dùng. Vì vậy, một lựa chọn đơn giản sử dụng bản đồ băm không phù hợp với chúng tôi vì nó sẽ tiêu tốn quá nhiều bộ nhớ. Làm cách nào chúng ta có thể thực hiện việc này bằng cách sử dụng ít tài nguyên hơn?
- Cấu trúc dữ liệu xác suất HyperLogLog được sử dụng ở đó. Bạn có thể đọc thêm về nó trên trang Wikipedia. Đặc điểm chính là cấu trúc dữ liệu này cho phép chúng tôi giải quyết vấn đề bằng cách sử dụng ít bộ nhớ hơn đáng kể so với tùy chọn có bản đồ băm.
fun uniqueActivitiesPerDay(): Long {
val hyperLogLogOps: HyperLogLogOperations<String, String> = stringRedisTemplate.opsForHyperLogLog()
return hyperLogLogOps.size(Constants.TODAY_ACTIVITIES)
}
fun userOpenApp(userId: String): Long {
val hyperLogLogOps: HyperLogLogOperations<String, String> = stringRedisTemplate.opsForHyperLogLog()
return hyperLogLogOps.add(Constants.TODAY_ACTIVITIES, userId)
}
8) Kết luận
Trong bài viết này, chúng ta đã xem xét các cấu trúc dữ liệu Redis khác nhau. Bao gồm các hoạt động địa lý không quá phổ biến và HyperLogLog.
Chúng tôi đã sử dụng chúng để giải quyết các vấn đề thực tế.
Chúng tôi gần như thiết kế Tinder, có thể ở FAANG sau này)))
Ngoài ra, chúng tôi đã nêu bật các sắc thái và vấn đề chính có thể gặp phải khi làm việc với Redis.
Redis là một bộ lưu trữ dữ liệu rất chức năng. Và nếu bạn đã có nó trong cơ sở hạ tầng của mình, có thể coi Redis như một công cụ để giải quyết các nhiệm vụ khác của bạn với điều đó mà không có những phức tạp không cần thiết.
Tái bút:
Tất cả các ví dụ mã có thể được tìm thấy trên github.
Viết các nhận xét nếu bạn nhận thấy một sai lầm.
Để lại bình luận bên dưới về cách mô tả bằng cách sử dụng một số công nghệ như vậy. Bạn có thích nó hay không?
Và theo dõi tôi tại Twitter:🐦 @ de ____ ro